import { expect, type Page, test } from "@playwright/test"; const BASE = "http://localhost:8080/"; test("Open documentation", async ({ page }) => { await page.goto(BASE); // Expect a title "to contain" a substring. await expect(page).toHaveTitle(/SQLPage.*/); // open the submenu await page.getByText("Documentation", { exact: true }).first().click(); const components = ["form", "map", "chart", "button"]; for (const component of components) { await expect( page.getByRole("link", { name: component }).first(), ).toBeVisible(); } }); test("chart", async ({ page }) => { await page.goto(`${BASE}/documentation.sql?component=chart#component`); await expect(page.getByText("Loading...")).not.toBeVisible(); await expect(page.locator(".apexcharts-canvas").first()).toBeVisible(); }); test("chart supports hiding legend", async ({ page }) => { await page.goto(`${BASE}/documentation.sql?component=chart#component`); const expensesChart = page.locator(".card", { has: page.getByRole("heading", { name: "Expenses" }), }); await expect(expensesChart.locator(".apexcharts-canvas")).toBeVisible(); await expect(expensesChart.locator(".apexcharts-legend")).toBeHidden(); }); test("map", async ({ page }) => { await page.goto(`${BASE}/documentation.sql?component=map#component`); await expect(page.getByText("Loading...")).not.toBeVisible(); await expect(page.locator(".leaflet-marker-icon").first()).toBeVisible(); }); test("form example", async ({ page }) => { await page.goto(`${BASE}/examples/multistep-form`); // Single selection matching the value or label await page.getByLabel("From").selectOption("Paris"); await page.getByText("Next").click(); await page.getByLabel(/\bTo\b/).selectOption("Mexico"); await page.getByText("Next").click(); await page.getByLabel("Number of Adults").fill("1"); await page.getByText("Next").click(); await page.getByLabel("Passenger 1 (adult)").fill("John Doe"); await page.getByText("Book the flight").click(); await expect(page.getByText("John Doe").first()).toBeVisible(); }); test("File upload", async ({ page }) => { await page.goto(`${BASE}/your-first-sql-website`); await page.getByRole("button", { name: "Examples", exact: true }).click(); await page.getByText("File uploads").click(); const my_svg = 'Hello World'; // @ts-ignore const buffer = Buffer.from(my_svg); await page.getByLabel("Picture").setInputFiles({ name: "small.svg", mimeType: "image/svg+xml", buffer, }); await page.getByRole("button", { name: "Upload picture" }).click(); await expect( page.locator("img[src^=data]").first().getAttribute("src"), ).resolves.toBe(`data:image/svg+xml;base64,${buffer.toString("base64")}`); }); test("Authentication example", async ({ page }) => { await page.goto(`${BASE}/examples/authentication/login.sql`); await expect(page.locator("h1", { hasText: "Authentication" })).toBeVisible(); const usernameInput = page.getByLabel("Username"); const passwordInput = page.getByLabel("Password"); const loginButton = page.getByRole("button", { name: "Log in" }); await expect(usernameInput).toBeVisible(); await expect(passwordInput).toBeVisible(); await expect(loginButton).toBeVisible(); await usernameInput.fill("admin"); await passwordInput.fill("admin"); await loginButton.click(); await expect(page.getByText("You are logged in as admin")).toBeVisible(); }); test("table filtering", async ({ page }) => { await page.goto(`${BASE}/documentation.sql?component=table`); const tableSection = page.locator(".card", { has: page.getByRole("cell", { name: "Chart", exact: true }), }); const searchInput = tableSection.getByPlaceholder("Search…"); await searchInput.fill("chart"); const chartCell = tableSection.getByRole("cell", { name: "Chart" }); await expect(chartCell).toBeVisible(); await expect(chartCell).toHaveClass(/\b_col_name\b/); await expect(chartCell).toHaveCSS("vertical-align", "middle"); await expect( tableSection.getByRole("cell", { name: "Table" }), ).not.toBeVisible(); }); test("table sorting", async ({ page }) => { await page.goto(`${BASE}/documentation.sql?component=table`); const tableSection = page.locator(".table-responsive", { has: page.getByRole("cell", { name: "31456" }), }); // Test numeric sorting on id column await tableSection.getByRole("button", { name: "id" }).click(); let ids = await tableSection.locator("td.id").allInnerTexts(); let numericIds = ids.map((id) => Number.parseInt(id)); const sortedIds = [...numericIds].sort((a, b) => a - b); expect(numericIds).toEqual(sortedIds); // Test reverse sorting await tableSection.getByRole("button", { name: "id" }).click(); ids = await tableSection.locator("td.id").allInnerTexts(); numericIds = ids.map((id) => Number.parseInt(id)); const reverseSortedIds = [...numericIds].sort((a, b) => b - a); expect(numericIds).toEqual(reverseSortedIds); // Test amount in stock column sorting await tableSection.getByRole("button", { name: "Amount in stock" }).click(); const amounts = await tableSection.locator("td.Amount").allInnerTexts(); const numericAmounts = amounts.map((amount) => Number.parseInt(amount.replace(/[^0-9]/g, "")), ); const sortedAmounts = [...numericAmounts].sort((a, b) => a - b); expect(numericAmounts).toEqual(sortedAmounts); }); async function checkNoConsoleErrors(page: Page, component: string) { const errors: string[] = []; page.on("console", (msg) => { if (msg.type() === "error") { errors.push(msg.text()); } }); await page.goto(`${BASE}/documentation.sql?component=${component}`); await page.waitForLoadState(); expect(errors).toHaveLength(0); } test("no console errors on table page", async ({ page }) => { await checkNoConsoleErrors(page, "table"); }); test("no console errors on chart page", async ({ page }) => { await checkNoConsoleErrors(page, "chart"); }); test("no console errors on map page", async ({ page }) => { await checkNoConsoleErrors(page, "map"); }); test("no console errors on card page", async ({ page }) => { await checkNoConsoleErrors(page, "card"); }); test("CSP issues unique nonces per request", async ({ page }) => { const csp1 = await (await page.goto(BASE)).headerValue( "content-security-policy", ); const csp2 = await (await page.reload()).headerValue( "content-security-policy", ); expect(csp1, `check if ${csp1} != ${csp2}`).not.toEqual(csp2); }); test("form component documentation", async ({ page }) => { await page.goto(`${BASE}/component.sql?component=form`); // Find the form that contains radio buttons for component selection const componentForm = page.locator("form", { has: page.getByRole("radio", { name: "Chart" }), }); // the form should be visible await expect(componentForm).toBeVisible(); // Check that "form" is the first and default selected option const mapRadio = componentForm.getByRole("radio", { name: "Map" }); await expect(mapRadio).toHaveValue("map"); await expect(mapRadio).toBeChecked(); // Select "Chart" option and submit await componentForm.getByLabel("Chart").click({ force: true }); await componentForm.getByRole("button", { name: "Submit" }).click(); // Verify we're on the chart documentation page await expect( page.getByRole("heading", { name: /chart/i, level: 1 }), ).toBeVisible(); }); test("form select combines initial options with remote search results", async ({ page, }) => { await page.goto(`${BASE}/component.sql?component=form`); const select = page .locator( 'select[data-options_source="examples/from_component_options_source.sql"]', ) .first(); await expect(select).toBeAttached(); await page.waitForFunction( () => !!document.querySelector( 'select[data-options_source="examples/from_component_options_source.sql"]', )?.tomselect, ); const initialState = await select.evaluate((element) => { const tomselect = element.tomselect; return { value: tomselect.getValue(), labels: Object.fromEntries( Object.entries(tomselect.options).map(([value, option]) => [ value, option.label, ]), ), }; }); expect(initialState).toEqual({ value: "form", labels: { form: "Form" }, }); await select.evaluate((element) => element.tomselect.focus()); await page.keyboard.type("form"); await page.waitForResponse((response) => response .url() .includes("examples/from_component_options_source.sql?search=form"), ); await expect .poll(async () => select.evaluate((element) => ({ value: element.tomselect.getValue(), formLabel: element.tomselect.options.form?.label, })), ) .toEqual({ value: "form", formLabel: "form", }); await select.evaluate((element) => element.tomselect.setTextboxValue("")); await page.keyboard.type("map"); await page.waitForResponse((response) => response .url() .includes("examples/from_component_options_source.sql?search=map"), ); await expect .poll(async () => select.evaluate((element) => ({ value: element.tomselect.getValue(), formLabel: element.tomselect.options.form?.label, mapLabel: element.tomselect.options.map?.label, })), ) .toEqual({ value: "form", formLabel: "form", mapLabel: "map", }); }); test("modal", async ({ page }) => { await page.goto(`${BASE}/documentation.sql?component=modal#component`); // get the button that opens the modal const openButton = page.getByRole("button", { name: "Open a simple modal" }); await openButton.click(); const modal = page.getByRole("dialog", { label: "A modal box" }); await expect(modal).toBeVisible(); // close the modal await page.keyboard.press("Escape"); await expect(modal).not.toBeVisible(); await openButton.click(); await expect(modal).toBeVisible(); await modal.getByRole("button", { label: "Close" }).first().click(); await expect(modal).not.toBeVisible(); }); test("table action buttons - edit_url and delete_url", async ({ page }) => { await page.goto(`${BASE}/documentation.sql?component=table`); const tableSection = page.locator(".table-responsive", { has: page.getByRole("cell", { name: "PharmaCo" }), }); const editButton = tableSection.getByTitle("Edit").first(); await expect(editButton).toBeVisible(); await expect(editButton).toHaveAttribute("href", /action=edit&update_id=\d+/); const deleteButton = tableSection.getByTitle("Delete").first(); await expect(deleteButton).toBeVisible(); await expect(deleteButton).toHaveAttribute( "href", /action=delete&delete_id=\d+/, ); }); test("table action buttons - custom_actions", async ({ page }) => { await page.goto(`${BASE}/documentation.sql?component=table`); const tableSection = page.locator(".table-responsive", { has: page.getByRole("cell", { name: "PharmaCo" }), }); const historyButton = tableSection .getByTitle("View Standard History") .first(); await expect(historyButton).toBeVisible(); await expect(historyButton).toHaveAttribute( "href", /action=history&standard_id=\d+/, ); }); test("table action buttons - _sqlpage_actions", async ({ page }) => { await page.goto(`${BASE}/documentation.sql?component=table`); const tableSection = page.locator(".table-responsive", { has: page.getByRole("cell", { name: "PharmaCo" }), }); const pdfButtons = tableSection.getByTitle("View Presentation"); await expect(pdfButtons.first()).toBeVisible(); await expect(pdfButtons).toHaveCount(3); const firstPdfButton = pdfButtons.first(); await expect(firstPdfButton).toHaveAttribute( "href", "https://sql-page.com/pgconf/2024-sqlpage-badass.pdf", ); const setInUseButton = tableSection.getByTitle("Set In Use"); await expect(setInUseButton).toBeVisible(); await expect(setInUseButton).toHaveAttribute( "href", /action=set_in_use&standard_id=32/, ); const retireButton = tableSection.getByTitle("Retire Standard"); await expect(retireButton).toBeVisible(); await expect(retireButton).toHaveAttribute( "href", /action=retire&standard_id=33/, ); }); test("table action buttons - disabled action", async ({ page }) => { await page.goto(`${BASE}/documentation.sql?component=table`); const tableSection = page.locator(".table-responsive", { has: page.getByRole("cell", { name: "PharmaCo" }), }); const viewPresentationButtons = tableSection.getByTitle("View Presentation"); await expect(viewPresentationButtons).toHaveCount(3); const actionColumnButtons = tableSection.locator( "td._col_Action a[data-action='Action']", ); await expect(actionColumnButtons).toHaveCount(3); const emptyActionButton = actionColumnButtons.last(); await expect(emptyActionButton).toHaveAttribute("href", "null"); await expect(emptyActionButton).toHaveAttribute("title", "Action"); });