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
387 lines
13 KiB
TypeScript
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");
|
|
});
|