test: add functional portal suite

This commit is contained in:
bisco
2026-06-24 09:25:28 +02:00
parent 4886516aa3
commit 1e3685bab8
12 changed files with 358 additions and 4 deletions
+3
View File
@@ -0,0 +1,3 @@
node_modules/
test-results/
playwright-report/
+13
View File
@@ -0,0 +1,13 @@
FROM mcr.microsoft.com/playwright:v1.61.1-noble
WORKDIR /tests
RUN chown pwuser:pwuser /tests
COPY --chown=pwuser:pwuser package.json package-lock.json ./
RUN npm ci --no-audit --no-fund
COPY --chown=pwuser:pwuser . .
USER pwuser
CMD ["npm", "test"]
+78
View File
@@ -0,0 +1,78 @@
{
"name": "azionelab-functional-tests",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "azionelab-functional-tests",
"version": "0.1.0",
"devDependencies": {
"@playwright/test": "1.61.1"
}
},
"node_modules/@playwright/test": {
"version": "1.61.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
"integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.61.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.61.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.61.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.61.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "azionelab-functional-tests",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"test": "playwright test"
},
"devDependencies": {
"@playwright/test": "1.61.1"
}
}
+22
View File
@@ -0,0 +1,22 @@
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
timeout: 30_000,
expect: {
timeout: 5_000,
},
fullyParallel: false,
forbidOnly: true,
retries: 0,
workers: 1,
reporter: "line",
outputDir: "test-results",
use: {
baseURL: process.env.BASE_URL ?? "http://azionelab.org",
browserName: "chromium",
screenshot: "only-on-failure",
trace: "retain-on-failure",
video: "off",
},
});
+155
View File
@@ -0,0 +1,155 @@
import { expect, test } from "@playwright/test";
const sectionIds = [
"inizio",
"laboratorio",
"maestro",
"lezioni",
"spettacoli",
"galleria",
"contatti",
];
test("renders the seeded single page in the required order", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveTitle(/Azione!Lab/);
await expect(page.getByRole("heading", { level: 1 })).toHaveText(
"Il teatro diventa presenza.",
);
await expect(page.getByText("Ernesto Estatico")).toBeVisible();
await expect(
page.getByText("Via dell'Epomeo 9999, Napoli", { exact: true }),
).toBeVisible();
await expect(page.locator(".feature-card")).toHaveCount(3);
await expect(page.locator(".show-card")).toHaveCount(2);
await expect(page.locator(".gallery-item")).toHaveCount(4);
await expect(
page.getByRole("link", { name: "Vieni a conoscerci" }).first(),
).toHaveAttribute("href", "#contatti");
await expect(page.getByRole("link", { name: "Guarda la galleria" })).toHaveAttribute(
"href",
"#galleria",
);
const verticalPositions = await page.evaluate(
(ids) =>
ids.map((id) => {
const element = document.getElementById(id);
if (!element) throw new Error(`Missing section #${id}`);
return element.getBoundingClientRect().top + window.scrollY;
}),
sectionIds,
);
expect(verticalPositions).toEqual([...verticalPositions].sort((a, b) => a - b));
});
test("exposes usable contact actions", async ({ page }) => {
await page.goto("/#contatti");
await expect(page.getByRole("link", { name: /Scrivi una mail/ })).toHaveAttribute(
"href",
"mailto:ciao@laboratorioteatrale.it",
);
await expect(page.getByRole("link", { name: /Chiama/ })).toHaveAttribute(
"href",
"tel:+393331234567",
);
await expect(page.getByRole("link", { name: /WhatsApp/ })).toHaveAttribute(
"href",
"https://wa.me/393331234567",
);
});
test("supports mobile navigation without horizontal overflow", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/");
const menu = page.locator("details.mobile-menu");
await menu.locator("summary").click();
await expect(menu).toHaveAttribute("open", "");
await menu.getByRole("link", { name: "Le lezioni" }).click();
await expect(page).toHaveURL(/#lezioni$/);
const dimensions = await page.evaluate(() => ({
clientWidth: document.documentElement.clientWidth,
scrollWidth: document.documentElement.scrollWidth,
}));
expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth + 1);
});
test("provides semantic landmarks, image alternatives and keyboard access", async ({
page,
}) => {
await page.goto("/");
await expect(page.getByRole("banner")).toHaveCount(1);
await expect(page.getByRole("main")).toHaveCount(1);
await expect(page.getByRole("contentinfo")).toHaveCount(1);
await expect(page.getByRole("heading", { level: 1 })).toHaveCount(1);
const imageAlternatives = await page.locator("img").evaluateAll((images) =>
images.map((image) => image.getAttribute("alt")?.trim() ?? ""),
);
expect(imageAlternatives.length).toBeGreaterThan(0);
expect(imageAlternatives.every(Boolean)).toBe(true);
const images = page.locator("img");
for (let index = 0; index < await images.count(); index += 1) {
const image = images.nth(index);
await image.scrollIntoViewIfNeeded();
await expect
.poll(() => image.evaluate((element) => element.naturalWidth))
.toBeGreaterThan(0);
}
await page.keyboard.press("Tab");
await expect(page.getByRole("link", { name: "Vai al contenuto" })).toBeFocused();
});
test("returns the aggregate API contract and serves CMS media", async ({ request }) => {
const healthResponse = await request.get("/health/");
expect(healthResponse.ok()).toBe(true);
expect(await healthResponse.json()).toEqual({ status: "ok" });
const response = await request.get("/api/site/home/");
expect(response.ok()).toBe(true);
const payload = await response.json();
expect(Object.keys(payload).sort()).toEqual([
"feature_cards",
"gallery_items",
"homepage",
"lesson_info",
"settings",
"shows",
"teacher",
]);
expect(payload.settings.laboratory_name).toBe("Azione!Lab");
expect(payload.feature_cards).toHaveLength(3);
expect(payload.shows).toHaveLength(2);
expect(payload.gallery_items).toHaveLength(4);
const mediaPath = new URL(payload.homepage.hero_image.url).pathname;
const mediaResponse = await request.get(mediaPath);
expect(mediaResponse.ok()).toBe(true);
expect(mediaResponse.headers()["content-type"]).toMatch(/^image\//);
});
test("routes Wagtail admin and rejects unknown virtual hosts", async ({
page,
playwright,
}) => {
await page.goto("/admin/");
await expect(page).toHaveURL(/\/admin\/login\/\?next=\/admin\/$/);
await expect(page.locator('input[name="username"]')).toBeVisible();
await expect(page.locator('input[name="password"]')).toBeVisible();
const invalidHostContext = await playwright.request.newContext({
baseURL: process.env.INVALID_HOST_URL ?? "http://proxy",
});
const invalidHostResponse = await invalidHostContext.get("/");
expect(invalidHostResponse.status()).toBe(404);
await invalidHostContext.dispose();
});