Files
EventSnap/e2e/page-objects/export-page.ts
fabi 137c4ee8a1 fix(export): let the keepsake download through X-Frame-Options on iOS
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>
2026-07-27 21:10:26 +02:00

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');
}
}