diff --git a/docs/contributing/artefact/files/role/playwright.specs.js.md b/docs/contributing/artefact/files/role/playwright.specs.js.md index 016713b560..be19d1f763 100644 --- a/docs/contributing/artefact/files/role/playwright.specs.js.md +++ b/docs/contributing/artefact/files/role/playwright.specs.js.md @@ -10,6 +10,8 @@ For the env contract see [Agent `playwright.env.j2`](../../../../agents/files/ro - The spec MUST be at `roles//files/playwright/playwright.spec.js`. The role-relative path is the SPOT constant `ROLE_FILE_PLAYWRIGHT_SPEC` in [mapping.py](../../../../../utils/roles/mapping.py); registered as optional on `ROLE_TYPE_APPLICATION`, disallowed elsewhere. - Companion `.js` helpers (role-local utility modules, like `web-app-dashboard/files/playwright/dashboard-card-flow.js`) MAY live next to the spec under the same `files/playwright/` directory. The runner (`roles/test-e2e-playwright/tasks/02_run_one.yml`) globs every `*.js` in that directory and stages them into the same `tests/` tree, so the spec can `require("./")` without any further wiring. +- Each scenario MUST live in its own `test-*.js` module (e.g. `test-login-oidc.js`, `test-seaweedfs.js`). `playwright.spec.js` is the thin aggregator that `require()`s every scenario module, keeping each case atomic and individually inspectable. The runner globs every `*.js`, so no extra wiring is needed. +- Binary fixtures (sample images, videos, …) MUST live under `roles//files/playwright/fixtures/`. The `*.js` glob does not ship binaries or subdirectories, so the runner stages the whole `fixtures/` directory next to the tests; specs load a fixture via `path.join(__dirname, "fixtures", "")`. - `playwright.config.js` and `package.json` are central, NOT per-role. See [Playwright Tests → Role-Local Files](../../../actions/testing/playwright.md#role-local-files-). ## Three personas, fixed flow 🚶 diff --git a/roles/test-e2e-playwright/README.md b/roles/test-e2e-playwright/README.md index 2a103364b4..ad85eb1da3 100644 --- a/roles/test-e2e-playwright/README.md +++ b/roles/test-e2e-playwright/README.md @@ -26,7 +26,7 @@ This role: - Supports allow-/deny-lists via `TEST_E2E_PLAYWRIGHT_ONLY_ROLES` and `TEST_E2E_PLAYWRIGHT_SKIP_ROLES` - Stages each Playwright project into `TEST_E2E_PLAYWRIGHT_STAGE_BASE_DIR/` - Renders the central `package.json` template into each staged project and injects the central `playwright.config.js` -- Copies role-specific `files/playwright/playwright.spec.js` into the staged `tests/playwright.spec.js` +- Copies every role-specific `files/playwright/*.js` (the `playwright.spec.js` aggregator plus its `test-*.js` scenario modules) into the staged `tests/` directory, and stages `files/playwright/fixtures/` (binary assets such as sample images/videos) alongside them - Renders `.env` from `templates/playwright.env.j2` using Ansible variables (`application_id`, `domains`, `users`, `applications`) - Optionally waits until the application responds with HTTP `200` or `302` - Injects CA trust automatically for `TLS_MODE=self_signed` (via `CA_TRUST.*`), so Playwright accepts self-signed cert chains diff --git a/roles/test-e2e-playwright/tasks/02_run_one.yml b/roles/test-e2e-playwright/tasks/02_run_one.yml index bfe91df77f..c38226d197 100644 --- a/roles/test-e2e-playwright/tasks/02_run_one.yml +++ b/roles/test-e2e-playwright/tasks/02_run_one.yml @@ -156,6 +156,20 @@ with_fileglob: - "{{ _playwright_files_dir }}/*.js" +- name: Create staging fixtures directory + ansible.builtin.file: + path: "{{ _playwright_stage_tests_dir }}/fixtures" + state: directory + mode: "0755" + +- name: Copy role Playwright binary fixtures into staging fixtures directory + ansible.builtin.copy: + src: "{{ item }}" + dest: "{{ _playwright_stage_tests_dir }}/fixtures/{{ item | basename }}" + mode: "0644" + with_fileglob: + - "{{ _playwright_files_dir }}/fixtures/*" + # Stage the shared service-gating helper next to the spec so the spec can # `require("./service-gating")`. - name: Copy shared service-gating helper into staging tests directory diff --git a/roles/web-app-peertube/files/playwright/fixtures/video_short.mp4 b/roles/web-app-peertube/files/playwright/fixtures/video_short.mp4 new file mode 100644 index 0000000000..35678362b5 Binary files /dev/null and b/roles/web-app-peertube/files/playwright/fixtures/video_short.mp4 differ diff --git a/roles/web-app-peertube/files/playwright/test-seaweedfs.js b/roles/web-app-peertube/files/playwright/test-seaweedfs.js index 50607147a5..bcfdd62492 100644 --- a/roles/web-app-peertube/files/playwright/test-seaweedfs.js +++ b/roles/web-app-peertube/files/playwright/test-seaweedfs.js @@ -1,16 +1,4 @@ -// SeaweedFS object-store scenario for PeerTube. -// -// PeerTube is configured with S3 object storage for web videos -// (PEERTUBE_OBJECT_STORAGE_ENABLED=true, -// PEERTUBE_OBJECT_STORAGE_WEB_VIDEOS_BUCKET_NAME -> the consumer bucket, -// PEERTUBE_OBJECT_STORAGE_WEB_VIDEOS_PREFIX=web-videos/). Avatars/thumbnails -// stay on the local volume in this config, so the only user action that lands -// in S3 is a video upload: PeerTube stores the web video locally and a -// background job moves it into the bucket under web-videos/. The action signs -// the administrator in over OIDC (mirroring playwright.spec.js), uploads a -// minimal valid MP4 through the upload UI and waits for it to be published; -// the shared check then proves the bucket grew via the Filer UI. - +const path = require("path"); const { test, expect } = require("@playwright/test"); const { skipUnlessServiceEnabled } = require("./service-gating"); const { @@ -28,13 +16,6 @@ const oidcButtonText = decodeDotenvQuotedValue(process.env.OIDC_BUTTON_TEXT || " const adminUsername = decodeDotenvQuotedValue(process.env.ADMIN_USERNAME || ""); const adminPassword = decodeDotenvQuotedValue(process.env.ADMIN_PASSWORD || ""); -function minimalMp4Buffer() { - const base64 = - "AAAAGGZ0eXBpc29tAAAAAGlzb21pc28yAAAACGZyZWUAAAAtbWRhdAAAAAAAAAAA" + - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; - return Buffer.from(base64, "base64"); -} - async function loginAdminViaOidc(page) { await page.goto(`${peertubeBaseUrl}/login`, { waitUntil: "domcontentloaded" }); @@ -85,11 +66,7 @@ async function uploadWebVideo(page) { const fileInput = page.locator("input[type='file']").first(); await fileInput.waitFor({ state: "attached", timeout: 60_000 }); - await fileInput.setInputFiles({ - name: `${marker}.mp4`, - mimeType: "video/mp4", - buffer: minimalMp4Buffer(), - }); + await fileInput.setInputFiles(path.join(__dirname, "fixtures", "video_short.mp4")); const nameField = page .getByLabel(/name/i) @@ -98,10 +75,7 @@ async function uploadWebVideo(page) { await nameField.waitFor({ state: "visible", timeout: 60_000 }); await nameField.fill(marker); - const publishButton = page - .getByRole("button", { name: /^\s*publish\b/i }) - .or(page.locator("button.orange-button[type='submit'], my-button[type='submit'] button, button[type='submit']")) - .first(); + const publishButton = page.locator(".save-button > button").first(); await publishButton.waitFor({ state: "visible", timeout: 60_000 }); await expect @@ -113,14 +87,14 @@ async function uploadWebVideo(page) { await publishButton.click(); - const publishedMarker = page - .locator("my-video-watch, .video-info-name, h1.video-info-name") - .or(page.getByRole("heading", { name: marker })) + const savedMarker = page + .locator(".save-button > button[disabled]") + .or(page.locator("my-video-watch, .video-info-name, h1.video-info-name")) .or(page.getByText(marker, { exact: false })) .first(); await expect( - publishedMarker, - `the uploaded video '${marker}' must be accepted by PeerTube before the S3 move job runs`, + savedMarker, + `the uploaded video '${marker}' must be saved by PeerTube before the S3 move job runs`, ).toBeVisible({ timeout: 120_000 }); }