Files
wehub-resource-sync d718c5a372
CI / compile_and_lint (push) Failing after 0s
CI / docker_build (linux/amd64, -linux-amd64-duckdb, duckdb) (push) Failing after 0s
CI / docker_build (linux/arm64, -linux-arm64, minimal) (push) Failing after 2s
CI / docker_build (linux/arm64, -linux-arm64-duckdb, duckdb) (push) Failing after 1s
CI / docker_build (linux/amd64, minimal) (push) Failing after 1s
CI / test (, sqlite, sqlite::memory:) (push) Has been skipped
CI / test (mssql, mssql, mssql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (mysql, mysql, mysql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (oracle, oracle, Driver=Oracle 21 ODBC driver;Dbq=//127.0.0.1:1521/FREEPDB1;Uid=root;Pwd=Password123!) (push) Has been skipped
CI / test (postgres, odbc, Driver=PostgreSQL Unicode;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!, true) (push) Has been skipped
CI / test (postgres, postgres, postgres://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / playwright (push) Has been skipped
CI / docker_build (linux/arm/v7, -linux-arm-v7, minimal) (push) Failing after 0s
CI / hurl_examples (push) Failing after 8s
deploy website / deploy_official_site (push) Failing after 1s
CI / hurl (${{ matrix.example }}) (push) Has been skipped
CI / docker_push (duckdb) (push) Has been cancelled
CI / docker_push (minimal) (push) Has been cancelled
CI / windows_test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:57 +08:00

387 lines
13 KiB
TypeScript

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 = '<svg><text y="20">Hello World</text></svg>';
// @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<HTMLSelectElement>(
'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");
});