diff --git a/e2e/harness/app/boot.ts b/e2e/harness/app/boot.ts index 206fdd4c..8ac87b5e 100644 --- a/e2e/harness/app/boot.ts +++ b/e2e/harness/app/boot.ts @@ -1,5 +1,5 @@ import type {BrowserContext} from "@playwright/test" -import {MINUTE, int, ms} from "@welshman/lib" +import {ms} from "@welshman/lib" import type {TrustedEvent} from "@welshman/util" import type {TestUser} from "../keys" import {injectEvents, injectSession} from "./session" @@ -76,9 +76,18 @@ export const boot = async ( // is the first point at which the app is running. With a session injected, wait for the signed-in // nav instead. A session the app rejected renders the landing dialog, and failing on that here // reads far better than the assertions it would break later. - await page - .locator(user ? ".primary-nav" : ".fl") - .waitFor({state: "attached", timeout: ms(int(1, MINUTE))}) + const shell = page.locator(user ? ".primary-nav" : ".fl") + + for (let attempt = 0; ; attempt++) { + try { + await shell.waitFor({state: "attached", timeout: ms(15)}) + break + } catch (e) { + if (attempt === 3) throw e + + await page.reload() + } + } // src/app/env.ts reads every VITE_ value as it is imported, so by now the app has resolved them // against either the env above or .env's real relays. diff --git a/e2e/specs/admin.spec.ts b/e2e/specs/admin.spec.ts index fee8bc18..5d83fd0f 100644 --- a/e2e/specs/admin.spec.ts +++ b/e2e/specs/admin.spec.ts @@ -144,11 +144,14 @@ test("US-092 edit a space's profile and featured content", async ({seed, as}) => await expect(editor).toBeVisible() - // The icon picker's file input is hidden inside its own label, so the chooser is opened by - // clicking the label rather than by writing to the input. + await editor.getByRole("button", {name: "Add an image"}).click() + + // The upload's file input is hidden inside its own label, so the chooser is opened by clicking + // the label rather than by writing to the input. Picking a file dismisses the picker. + const picker = dialog(admin, "Add an image") const chooser = admin.waitForEvent("filechooser") - await editor.locator('label:has(input[type="file"])').click() + await picker.locator('label:has(input[type="file"])').click() await (await chooser).setFiles(ICON) await expect(editor.getByText("Selected:")).toBeVisible() diff --git a/e2e/specs/articles-threads.spec.ts b/e2e/specs/articles-threads.spec.ts index 0ff1a72e..631d4ca2 100644 --- a/e2e/specs/articles-threads.spec.ts +++ b/e2e/specs/articles-threads.spec.ts @@ -43,7 +43,7 @@ const modal = (page: Page, title: string) => const composerForm = (page: Page) => page.locator("form").filter({has: page.locator(".note-editor")}) -const editorOf = (scope: Locator) => scope.locator(".note-editor [contenteditable=true]") +const editorOf = (scope: Locator | Page) => scope.locator(".note-editor [contenteditable=true]") const pageBar = (page: Page) => page.locator('[data-component="PageBar"]') @@ -146,17 +146,17 @@ test("US-037 write and publish an article", async ({seed, as}) => { await pageBar(page).getByRole("button", {name: "Write"}).click() - const composer = modal(page, "Write an Article") - const title = composer.getByPlaceholder("What is this article about?") - const body = editorOf(composer) + const publish = pageBar(page).getByRole("button", {name: "Publish"}) + const title = page.getByPlaceholder("Title", {exact: true}) + const body = editorOf(page) - await expect(composer).toBeVisible() + await expect(title).toBeVisible() - await composer.getByRole("button", {name: "Publish Article"}).click() + await publish.click() await expect(page.getByRole("alert")).toContainText("Please provide a title for your article.") await title.fill("Half Baked") - await composer.getByRole("button", {name: "Publish Article"}).click() + await publish.click() await expect(page.getByRole("alert")).toContainText("Please write something for your article.") await body.pressSequentially("Only the beginning.") @@ -164,8 +164,8 @@ test("US-037 write and publish an article", async ({seed, as}) => { // Neither refusal put anything on the wire. expect(publishedEvents(page, LONG_FORM)).toEqual([]) - await composer.getByRole("button", {name: "Go back"}).click() - await expect(page.getByRole("heading", {name: "Write an Article"})).toHaveCount(0) + await page.goBack() + await expect(articleCards(page)).toHaveCount(1) // Reopening picks up where the composer was closed rather than starting over. await pageBar(page).getByRole("button", {name: "Write"}).click() @@ -173,12 +173,16 @@ test("US-037 write and publish an article", async ({seed, as}) => { await expect(body).toContainText("Only the beginning.") await title.fill("Signals in the Noise") - await composer.getByRole("button", {name: "Publish Article"}).click() + await publish.click() + + // Publishing lands on the article itself rather than back on the list. + await expect(page.getByRole("heading", {name: "Signals in the Noise"}).first()).toBeVisible() + expect(publishedEvents(page, LONG_FORM)).toHaveLength(1) + + await page.goto(`${spacePath(url)}/articles`) - await expect(page.getByRole("heading", {name: "Write an Article"})).toHaveCount(0) await expect(articleCards(page)).toHaveCount(2) await expect(articleCards(page).first()).toContainText("Signals in the Noise") - expect(publishedEvents(page, LONG_FORM)).toHaveLength(1) // A published article leaves the composer empty for the next one. await pageBar(page).getByRole("button", {name: "Write"}).click() @@ -560,15 +564,14 @@ test("US-041 publish an article from a room", async ({seed, as}) => { await openComposeMenu(page) await page.getByRole("button", {name: "Write an Article"}).click() - const composer = modal(page, "Write an Article") + await page.getByPlaceholder("Title", {exact: true}).fill("Repotting in Winter") + await editorOf(page).pressSequentially("Wait for a warm week.") + await pageBar(page).getByRole("button", {name: "Publish"}).click() - await composer.getByPlaceholder("What is this article about?").fill("Repotting in Winter") - await editorOf(composer).pressSequentially("Wait for a warm week.") - await composer.getByRole("button", {name: "Publish Article"}).click() - - await expect(page.getByRole("heading", {name: "Write an Article"})).toHaveCount(0) + await expect(page.getByRole("heading", {name: "Repotting in Winter"}).first()).toBeVisible() // The room hears about the article without alice posting it a second time. + await page.goto(roomPath(url, "lounge")) await expect(page.getByText("Repotting in Winter")).toBeVisible() await page.goto(`${spacePath(url)}/articles`) diff --git a/src/lib/components/IconInput.svelte b/src/lib/components/IconInput.svelte index c0f116ba..32b3e68d 100644 --- a/src/lib/components/IconInput.svelte +++ b/src/lib/components/IconInput.svelte @@ -68,6 +68,7 @@