import Router from "@koa/router"; import axios from "axios"; import Koa, { Context } from "koa"; import { Page } from "playwright"; import { Controls, Fields, Form, Mountable } from "../index.js"; import { mount } from "../mount.js"; import { locator_is_visible } from "../test_utils/locator_is_visible.js"; import { getBrowser } from "../utils/browser-creator.js"; import { assertThrowsAsync } from "../utils/utils.js"; import { FormDataValue } from "./form-types.js"; import getPort from "get-port"; import { TextBasedSimpleField } from "./fields/simple-form-field.js"; import { expect as PlaywritghtExpect } from "@playwright/test"; const fields = { text: new Fields.TextBasedSimpleField(true), }; export function form_factory(canAccessFun?: Mountable["canAccess"]): Form { const result = new (class extends Form { getFields = () => fields; submitButtonText = "Submit"; getControls = () => [ new Controls.SimpleInput(fields.text, { label: "This is a test:", type: "password", }), ]; async onSubmit() { return; } })(); if (canAccessFun) result.canAccess = canAccessFun; return result; } const port = await getPort(); console.info("Using port " + port + " for form.test.ts"); describe("form test", () => { let page: Page; before(async () => { const browser = await getBrowser(); const context = await browser.newContext(); page = await context.newPage(); }); describe("basic tests", async () => { let server: ReturnType; before(async () => { const app = new Koa(); const router = new Router(); mount(router, "/", form_factory(), true); app.use(router.routes()).use(router.allowedMethods()); server = app.listen(port); await page.goto(`http://localhost:${port}`); }); after(async () => { server.close(); }); it("does not allow to submit an empty form when there's a required field", async () => { await page.getByRole("button", { name: "Submit", exact: true }).click(); await assertThrowsAsync(async () => { return page.getByText("Done").click({ timeout: 500 }); }); }); it("allows to submit a form when all required fields have a value", async () => { await page.getByPlaceholder("password").click(); await page.getByPlaceholder("password").fill("testpasswd"); await page.getByRole("button", { name: "Submit", exact: true }).click(); await page.getByText("Done").click(); }); it("does not allow submitting an empty form by circumventing HTML-based validation", async () => { const res_axios = await axios.post( `http://localhost:${port}`, { text: "", }, { validateStatus: (status: number) => { if (status == 422) return true; return false; }, } ); if (!res_axios.data.includes("Some fields are invalid")) { throw new Error("when sending a empty request with axios, the error didnt appear"); } }); }); describe("canAccess tests", async () => { let server: ReturnType; afterEach(async () => { server.close(); }); it("allows visit when configured when canAccess returns true", async () => { const app = new Koa(); const router = new Router(); mount( router, "/", form_factory( async (_ctx: Koa.Context): Promise<{ canAccess: boolean; message: string }> => { return { canAccess: true, message: "" }; } ), true ); app.use(router.routes()).use(router.allowedMethods()); server = app.listen(port); const response = await page.goto(`http://localhost:${port}`); if (response?.status() != 200) { throw new Error(`Should return 200 status and it returns ${response?.status()}`); } }); describe("declines access when canAccess returns false", async () => { const app = new Koa(); const router = new Router(); before(async () => { mount( router, "/", form_factory( async ( ctx: Koa.Context ): Promise<{ canAccess: boolean; message: string }> => { return { canAccess: false, message: "" }; } ), true ); app.use(router.routes()).use(router.allowedMethods()); }); beforeEach(async () => { server = app.listen(port); }); it("prevents the form from rendering", async () => { const response = await page.goto(`http://localhost:${port}`); if (response?.status() != 403) { throw new Error( `Should return 403 status and it returns ${response?.status()}` ); } }); it("does not allow submitting of the form through axios", async () => { await axios.post( `http://localhost:${port}`, { text: "sample", }, { validateStatus: (status: number) => { if (status == 403) return true; return false; }, } ); }); }); it("passes the context to canAccess (false case)", async () => { const app = new Koa(); const router = new Router(); mount( router, "/", form_factory( async (ctx: Koa.Context): Promise<{ canAccess: boolean; message: string }> => { return ctx.$context && ctx.$context.user_id ? { canAccess: true, message: "" } : { canAccess: false, message: "" }; } ), true ); app.use(router.routes()).use(router.allowedMethods()); server = app.listen(port); const response = await page.goto(`http://localhost:${port}`); if (response?.status() != 403) { throw new Error(`Should return 403 status and it returns ${response?.status()}`); } }); it("passes the context to canAccess (true case)", async () => { const app = new Koa(); const router = new Router(); router.use(async (ctx: Context, next: any) => { ctx.$context = { user_id: "miguel", } as any; await next(); }); mount( router, "/", form_factory( async (ctx: Koa.Context): Promise<{ canAccess: boolean; message: string }> => { return ctx.$context && ctx.$context.user_id ? { canAccess: true, message: "" } : { canAccess: false, message: "" }; } ), true ); app.use(router.routes()).use(router.allowedMethods()); server = app.listen(port); const response = await page.goto(`http://localhost:${port}`); if (response?.status() != 200) { throw new Error(`Should return 200 status and it returns ${response?.status()}`); } }); }); describe("validation e2e", async () => { describe("validation message", async () => { let server: ReturnType; before(async () => { const app = new Koa(); const router = new Router(); mount( router, "/", new (class extends Form { getFields = () => fields; submitButtonText = "Submit"; getControls = () => [ new Controls.SimpleInput(fields.text, { label: "This is a test:", type: "password", }), ]; async onSubmit() { return; } async validateValues( ctx: Koa.Context, data: Record ): Promise<{ valid: boolean; error: string }> { if (data.text === "incorrect") return { valid: false, error: "Incorrect input", }; return { valid: true, error: "" }; } })(), true ); app.use(router.routes()).use(router.allowedMethods()); server = app.listen(port); await page.goto(`http://localhost:${port}`); }); after(async () => { server.close(); }); it("shows up when incorrect input is given", async () => { await page.getByPlaceholder("password").click(); await page.getByPlaceholder("password").fill("incorrect"); await page.getByRole("button", { name: "Submit" }).click(); if (!(await locator_is_visible(page.getByText("Incorrect input")))) throw new Error("validation message doens't show up when input is incorrect"); }); it("doesn't show when correct input is given", async () => { await page.getByPlaceholder("password").click(); await page.getByPlaceholder("password").fill("correct"); await page.getByRole("button", { name: "Submit" }).click(); await assertThrowsAsync(async () => { return page.getByText("Incorrect input").click({ timeout: 500 }); }); }); }); describe("field specific validation message", async () => { let server: ReturnType; before(async () => { const app = new Koa(); const router = new Router(); const fields = { text: new Fields.EmailField(true), }; mount( router, "/", new (class extends Form { getFields = () => fields; submitButtonText = "Submit"; getControls = () => [ new Controls.SimpleInput(fields.text, { label: "This is a test:", type: "text", }), ]; async onSubmit() { return; } })(), true ); app.use(router.routes()).use(router.allowedMethods()); server = app.listen(port); await page.goto(`http://localhost:${port}`); }); after(async () => { server.close(); }); it("shows up when incorrect input is given", async () => { await page.getByPlaceholder("text").click(); await page.getByPlaceholder("text").fill("notanemail"); await page.getByRole("button", { name: "Submit" }).click(); await page.getByText("Please enter a proper email address").click(); }); it("doesn't show up when correct input is given", async () => { await page.getByPlaceholder("text").click(); await page.getByPlaceholder("text").fill("yes@an.email"); await page.getByRole("button", { name: "Submit" }).click(); await assertThrowsAsync(async () => { return page .getByText("Please enter a proper email address") .click({ timeout: 500 }); }); }); }); }); describe("getInitialValues tests", async () => { let server: ReturnType; afterEach(async () => { server.close(); }); it("fills the form with values from getInitialValues", async () => { const app = new Koa(); const router = new Router(); const fields = { title: new TextBasedSimpleField(true), }; mount( router, "/", new (class extends Form { getFields = () => fields; getControls = () => [new Controls.SimpleInput(fields.title)]; async getInitialValues() { return { title: "Hello" }; } onSubmit() { return null; } })(), true ); app.use(router.routes()).use(router.allowedMethods()); server = app.listen(port); const response = await page.goto(`http://localhost:${port}`); if (response?.status() != 200) { throw new Error(`Should return 200 status and it returns ${response?.status()}`); } await PlaywritghtExpect(page.getByLabel("title")).toHaveValue("Hello"); }); }); });