fix(web-app-peertube): repair the SeaweedFS object-store e2e test for PeerTube v8.2.1

The test timed out: the publish-button locator no longer matched the v8.2.1
manage-video wizard, and the inline 78-byte MP4 stub was structurally invalid,
so PeerTube rejected the upload as "Video file unreadable" and left the Save
button disabled.

- test-seaweedfs.js: target the v8.2.1 save control (.save-button > button),
  upload PeerTube's real video_short.mp4 fixture, and assert a version-tolerant
  saved marker.
- add fixture roles/web-app-peertube/files/playwright/fixtures/video_short.mp4.
- test-e2e-playwright: stage role-local files/playwright/fixtures/ next to the
  tests (the *.js glob ships neither binaries nor subdirectories), so specs load
  a fixture via path.join(__dirname, "fixtures", <name>).
- docs: document the fixtures/ folder and the per-scenario test-*.js split in the
  playwright spec contract and the runner README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 4.8 committed 2026-06-17 11:30:36 +02:00
1 parent 4efe7b0dfc
commit 3b60b36fb2
5 files changed
+25 -35

No files matched your search

@@ -10,6 +10,8 @@ For the env contract see [Agent `playwright.env.j2`](../../../../agents/files/ro
- The spec MUST be at `roles/<role>/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("./<helper>")` 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/<role>/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", "<name>")`.
- `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 🚶
+1 -1
View File
@@ -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/<application_id>`
- 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
@@ -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
@@ -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 });
}