import { test, expect, type Page } from "@playwright/test"; import { loginAs } from "../fixtures/login.js"; const PASSWORD = process.env.TEST_ACCOUNT_PASSWORD || "e2e-test-password"; async function addFirstSearchResult(page: Page, query: string): Promise { await page.goto("/add"); await page.fill(".autocomplete input", query); await page.waitForSelector(".suggestions li", { timeout: 15_000 }); await page.click(".suggestions li >> nth=0"); const saved = page.waitForResponse( (r) => r.url().endsWith("/xrpc/dev.mokkenstorm.crate.shelf.addEntry") && r.request().method() === "POST", { timeout: 15_000 }, ); await page.click("text=SAVE TO CRATE"); await saved; } // The catalog.edit inbox's full loop: amending someone else's adopted // record proposes a correction in the proposer's own repo (existing amend // flow), and the record's original author can review and apply it from // their inbox, which mints the superseding release and re-points their // entry — without the proposal record itself ever leaving the proposer's // repo. test("alice proposes a correction on bob's record; bob reviews and applies it", async ({ browser, }) => { const bob = await browser.newContext(); const bobPage = await bob.newPage(); await loginAs(bobPage, "bob.test", PASSWORD); await addFirstSearchResult(bobPage, "boards of canada music has the right to children"); // Constellation indexes off the local jetstream firehose — not instant. await bobPage.waitForTimeout(3000); const alice = await browser.newContext(); const alicePage = await alice.newPage(); await loginAs(alicePage, "alice.test", PASSWORD); await addFirstSearchResult(alicePage, "boards of canada music has the right to children"); // Alice's copy adopted bob's canonical release, so amending it is a // foreign-record amend: it proposes rather than supersedes. await alicePage.goto("/"); await alicePage.click(".cover-card"); await alicePage.click("text=AMEND"); const releasedField = alicePage .locator(".amend-panel .field", { hasText: "RELEASED" }) .locator("input"); await releasedField.fill("1998-REMASTER"); const proposed = alicePage.waitForResponse( (r) => r.url().endsWith("/xrpc/dev.mokkenstorm.crate.shelf.amendEntry") && r.request().method() === "POST", { timeout: 15_000 }, ); await alicePage.click("text=PUBLISH AMENDMENT"); await proposed; // Constellation indexes the new catalog.edit backlink off the same firehose. await alicePage.waitForTimeout(3000); await bobPage.goto("/inbox"); await bobPage.waitForSelector(".proposal-card__title", { timeout: 15_000 }); await expect(bobPage.locator(".proposal-card__by").first()).toContainText( "proposed by @alice.test", ); // The old value is whatever pressing Discogs matched (live data), so only // the proposed side is a stable assertion. await expect(bobPage.locator(".diff-row__new").first()).toContainText( "1998-REMASTER", ); const applied = bobPage.waitForResponse( (r) => r.url().endsWith("/xrpc/dev.mokkenstorm.crate.catalog.applyEditProposal") && r.request().method() === "POST", { timeout: 15_000 }, ); await bobPage.click(".proposal-actions button:has-text('APPLY')"); await applied; await expect(bobPage.locator(".applied-strip")).toContainText( "Applied: minted a superseding release for", ); await alice.close(); await bob.close(); });