0
2
// The private gallery flow, end to end, in a browser: OAuth into a PDS that
// serves permissioned spaces, publish a gallery into a space, then read it back
// and confirm the photo bytes arrive through grain rather than the CDN.
//
// Needs the dev stack and its accounts:
// docker compose up -d && ./seeds/pdsjs-accounts.sh
// npx playwright test
//
// Skipped when those accounts are absent, so a checkout without the stack still
// runs the suite.
import { existsSync, readFileSync } from "node:fs";
import { expect, test } from "@playwright/test";
const CREDS = "data/pdsjs/credentials-spacehost.json";
const PASSWORD = "dev-password";
test.skip(!existsSync(CREDS), "needs ./seeds/pdsjs-accounts.sh");
const author = JSON.parse(readFileSync(CREDS, "utf8")) as {
did: string;
handle: string;
pdsUrl: string;
};
test("publish a private gallery and read it back", async ({ page }) => {
test.setTimeout(120_000);
// 1. Log in. The DID, not the handle: handle resolution runs through the dev
// relay, which has never heard of an account on a pds.js instance.
await page.goto(`/oauth/login?handle=${encodeURIComponent(author.did)}`);
// pds.js knows which account is being authorized from the request, so its
// consent screen asks only for the password. "Authorize" exactly — there is
// an "Authorize with a passkey" button beside it.
await expect(page.getByRole("heading", { name: /sign in to authorize/i })).toBeVisible({
timeout: 30_000,
});
await page.getByRole("textbox").first().fill(PASSWORD);
await page.getByRole("button", { name: "Authorize", exact: true }).click();
await page.waitForURL((url) => url.port === "3000", { timeout: 60_000 });
// 2. Publish a gallery into a space.
// networkidle, because the file below is handed to the input programmatically
// and a change event dispatched before Svelte has hydrated is dropped with
// nothing to observe.
await page.goto("/private/create", { waitUntil: "networkidle" });
// Retried rather than waited on: the file is handed to the input
// programmatically, and a change event dispatched before Svelte has attached
// its handler is dropped with nothing to observe. Polling stops at the first
// thumbnail, so this adds one photo, not one per attempt.
//
// By test id, not by tag: the layout carries file inputs of its own.
await expect
.poll(
async () => {
await page.getByTestId("private-photos").setInputFiles("seeds/images/forest.jpg");
return page.locator(".thumb img").count();
},
{ timeout: 60_000, intervals: [500, 1000, 2000] },
)
.toBeGreaterThan(0);
await page.getByPlaceholder("Sunday at the coast").fill("Browser test gallery");
await page.getByRole("button", { name: /create private gallery/i }).click();
// 3. Land on the gallery and read it back out of the space.
await page.waitForURL(/\/private\/did(:|%3A)/i, { timeout: 60_000 });
await expect(page.getByText("Private", { exact: false }).first()).toBeVisible();
// 4. The photo. Served by grain from the space, never cached anywhere in
// between — so the response has to be a real image and say so.
// By what the src has to be, not by a layout class — the assertion below is
// about the photo coming through the space endpoint, and that shouldn't
// break when the page is restyled.
const image = page.locator('img[src*="/space-blob"]').first();
await expect(image).toBeVisible({ timeout: 30_000 });
const src = await image.getAttribute("src");
const response = await page.request.get(src!);
expect(response.status()).toBe(200);
expect(response.headers()["content-type"]).toMatch(/^image\//);
expect(response.headers()["cache-control"]).toContain("no-store");
expect((await response.body()).byteLength).toBeGreaterThan(1000);
// The browser decoded it, not just the request client.
await expect
.poll(async () => image.evaluate((el: HTMLImageElement) => el.naturalWidth), {
timeout: 15_000,
})
.toBeGreaterThan(0);
// 5. And it is findable again. Nothing indexes a space, so a gallery missing
// from this list is a gallery that only its URL can reach.
await page.goto("/settings/account", { waitUntil: "networkidle" });
await expect(page.getByRole("link", { name: "Browser test gallery" }).first()).toBeVisible({
timeout: 30_000,
});
});