The keepsake download navigates a hidden, same-origin iframe (deliberately: a top-level navigation to a 404/429 would unload the PWA). Caddy stamped a site-wide `X-Frame-Options: DENY` that also covered the proxied `/api/*`. Blink hands a `Content-Disposition: attachment` response to the download manager at the network layer, so Chromium never noticed. WebKit enforces XFO on the frame navigation first and aborts the load — so on iOS Safari, the app's primary platform, tapping Download did nothing at all, silently. Carve the two export endpoints out to SAMEORIGIN, which still blocks cross-origin framing. Implemented as two disjoint matchers rather than an override: Caddy applies the FIRST `header` directive outermost, so it wins on write and a later, more specific `header` is silently ignored (verified against the running test stack). Also close the test gap that let this ship: - `06-export` ran on chromium-desktop only; add it to `webkit-iphone`, the only engine that enforces XFO on the download frame. - No test in the suite ever clicked a download button — every archive assertion used Node `fetch`, which has no frame and no XFO enforcement. Add a spec that clicks it and awaits a real `download` event. Verified falsifiable: with the blanket DENY reinstated it fails and reports the WebKit refusal as the cause. - Fix `ExportPage`'s card-scoped locators, which matched nothing: the cards carry `class="card p-5"` (a Tailwind `@apply` component class), never the `rounded-xl` the page object looked for. This had left the "shows enabled download buttons" test red on main. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
61 lines
2.7 KiB
TypeScript
61 lines
2.7 KiB
TypeScript
import type { Page, Locator } from '@playwright/test';
|
|
|
|
/**
|
|
* Page object for `/export` (see [frontend/src/routes/export/+page.svelte]).
|
|
*
|
|
* The two archive buttons are BOTH labelled exactly "Download" — the format is carried by
|
|
* the surrounding card ("ZIP-Archiv" / "HTML-Viewer"), not by the accessible name. Earlier
|
|
* versions of this file looked for `/zip.*herunter/i` and `/^herunterladen$/i`, which match
|
|
* nothing on this page; any test asserting `not.toBeVisible()` on those passed no matter
|
|
* what the page rendered. Locators here must match the real UI, so scope by card.
|
|
*
|
|
* "Herunterladen" IS a real label — but only inside the HTML-guide confirm modal, which is
|
|
* a different element entirely.
|
|
*/
|
|
export class ExportPage {
|
|
readonly page: Page;
|
|
/** Empty state shown while the host has not released the export yet. */
|
|
readonly notAvailableBanner: Locator;
|
|
/** Any "Download" button on the page (both cards). Useful for absence assertions. */
|
|
readonly downloadButtons: Locator;
|
|
readonly zipDownloadButton: Locator;
|
|
readonly htmlDownloadButton: Locator;
|
|
/** Confirm button inside the "Hinweis zum HTML-Viewer" modal. */
|
|
readonly htmlGuideModalContinue: Locator;
|
|
|
|
constructor(page: Page) {
|
|
this.page = page;
|
|
this.notAvailableBanner = page.getByText('Export noch nicht verfügbar');
|
|
this.downloadButtons = page.getByRole('button', { name: 'Download', exact: true });
|
|
this.zipDownloadButton = this.cardButton('ZIP-Archiv');
|
|
this.htmlDownloadButton = this.cardButton('HTML-Viewer');
|
|
this.htmlGuideModalContinue = page
|
|
.getByRole('dialog')
|
|
.getByRole('button', { name: /^herunterladen$/i });
|
|
}
|
|
|
|
/**
|
|
* The "Download" button inside the card whose heading is `heading`.
|
|
*
|
|
* Scoped to the card element rather than "any div containing the heading" — the latter
|
|
* also matches the page wrapper, which contains BOTH cards' buttons.
|
|
*
|
|
* The scope class is `div.card` (see the ZIP/HTML cards in
|
|
* frontend/src/routes/export/+page.svelte). It was previously `div.rounded-xl`, which
|
|
* matched NOTHING: `.card` is a Tailwind `@apply` component class
|
|
* (frontend/src/lib/styles/components.css) so the DOM only ever carries `class="card p-5"`
|
|
* — and the utility it applies is `rounded-2xl` anyway. Both card-scoped locators were
|
|
* therefore dead, which is why the "shows enabled download buttons" test was red.
|
|
*/
|
|
private cardButton(heading: string): Locator {
|
|
return this.page
|
|
.locator('div.card')
|
|
.filter({ has: this.page.getByRole('heading', { name: heading, exact: true }) })
|
|
.getByRole('button', { name: 'Download', exact: true });
|
|
}
|
|
|
|
async goto() {
|
|
await this.page.goto('/export');
|
|
}
|
|
}
|