Fix a test, avoid link nesting in article cards

This commit is contained in:
Jon Staab 2026-08-22 10:55:06 -07:00
parent 08821d27f4
commit 2edcfd9a79
7 changed files with 46 additions and 36 deletions

View file

@ -111,7 +111,7 @@ const openMessageMenu = (page: Page, text: string) =>
const openEventMenu = (card: Locator) => card.locator(".join").getByRole("button").last().click() const openEventMenu = (card: Locator) => card.locator(".join").getByRole("button").last().click()
const articleCard = (page: Page, title: string) => const articleCard = (page: Page, title: string) =>
page.getByRole("link", {name: /Written by/}).filter({hasText: title}) page.locator('[data-component="ArticleItem"]').filter({hasText: title})
// A FieldInline puts its control in the div immediately after its label, which is how one row of // A FieldInline puts its control in the div immediately after its label, which is how one row of
// the hosting card is told apart from the others in the same grid. // the hosting card is told apart from the others in the same grid.

View file

@ -101,10 +101,12 @@ const expectReactionRoundTrip = async (page: Page, scope: Locator, opener: Locat
const shortDate = (seconds: number) => const shortDate = (seconds: number) =>
new Intl.DateTimeFormat(undefined, {dateStyle: "short"}).format(new Date(seconds * 1000)) new Intl.DateTimeFormat(undefined, {dateStyle: "short"}).format(new Date(seconds * 1000))
const articleCards = (page: Page) => page.getByRole("link", {name: /Written by/}) // The card is a div carrying an overlay link, so it is found by its component rather than by a
// role — its own contents include a profile button and the room and action links.
const articleCards = (page: Page) => page.locator('[data-component="ArticleItem"]')
const openArticle = (page: Page, title: string) => const openArticle = (page: Page, title: string) =>
articleCards(page).filter({hasText: title}).getByText(title, {exact: true}).click() articleCards(page).filter({hasText: title}).getByRole("link", {name: title, exact: true}).click()
test("US-037 write and publish an article", async ({seed, as}) => { test("US-037 write and publish an article", async ({seed, as}) => {
const scenario = await seed(({relay, user, at}) => { const scenario = await seed(({relay, user, at}) => {
@ -570,11 +572,11 @@ test("US-041 publish an article from a room", async ({seed, as}) => {
const card = articleCards(page).filter({hasText: "Repotting in Winter"}) const card = articleCards(page).filter({hasText: "Repotting in Winter"})
await expect(card).toBeVisible() await expect(card).toBeVisible()
await expect(card.getByRole("link", {name: "#Lounge"})).toBeVisible() await expect(card.getByRole("link", {name: /#\s*Lounge/})).toBeVisible()
await openArticle(page, "Repotting in Winter") await openArticle(page, "Repotting in Winter")
const badge = page.getByRole("link", {name: "Posted in #Lounge"}) const badge = page.getByRole("link", {name: /Posted in #\s*Lounge/})
await expect(badge).toBeVisible() await expect(badge).toBeVisible()

View file

@ -254,7 +254,7 @@ test("US-056 autocomplete a mention or a room reference", async ({seed, as}) =>
await editor.press("Enter") await editor.press("Enter")
await expect(timeline(page).getByRole("link", {name: "#General"})).toHaveAttribute( await expect(timeline(page).getByRole("link", {name: /#\s*General/})).toHaveAttribute(
"href", "href",
roomPath(space.url, "general"), roomPath(space.url, "general"),
) )

View file

@ -387,7 +387,7 @@ test("US-071 content posts show delivery status in place", async ({seed, as}) =>
await writeArticle(alice, "Signals in the Noise", "Everything worth hearing is quiet.") await writeArticle(alice, "Signals in the Noise", "Everything worth hearing is quiet.")
const article = alice.getByRole("link", {name: /Written by/}).filter({hasText: "Signals"}) const article = alice.locator('[data-component="ArticleItem"]').filter({hasText: "Signals"})
await expect(alice.getByRole("heading", {name: "Write an Article"})).toHaveCount(0) await expect(alice.getByRole("heading", {name: "Write an Article"})).toHaveCount(0)
@ -412,7 +412,7 @@ test("US-071 content posts show delivery status in place", async ({seed, as}) =>
await alice.goto(`${spacePath(quiet)}/articles`) await alice.goto(`${spacePath(quiet)}/articles`)
await writeArticle(alice, "Into the Void", "Nobody is listening.") await writeArticle(alice, "Into the Void", "Nobody is listening.")
const stuck = alice.getByRole("link", {name: /Written by/}).filter({hasText: "Into the Void"}) const stuck = alice.locator('[data-component="ArticleItem"]').filter({hasText: "Into the Void"})
await expect(stuck.getByText("Sending...")).toBeVisible() await expect(stuck.getByText("Sending...")).toBeVisible()
await expect(stuck.getByRole("button", {name: "Cancel"})).toBeVisible() await expect(stuck.getByRole("button", {name: "Cancel"})).toBeVisible()
@ -497,7 +497,7 @@ test("US-072 a deleted post is marked deleted", async ({seed, as}) => {
// A post whose relay never answered, deleted while it is still marked failed. // A post whose relay never answered, deleted while it is still marked failed.
await writeArticle(alice, "Into the Void", "Nobody is listening.") await writeArticle(alice, "Into the Void", "Nobody is listening.")
const stuck = alice.getByRole("link", {name: /Written by/}).filter({hasText: "Into the Void"}) const stuck = alice.locator('[data-component="ArticleItem"]').filter({hasText: "Into the Void"})
await expect(stuck.getByText("Failed to send!")).toBeVisible() await expect(stuck.getByText("Failed to send!")).toBeVisible()

View file

@ -24,9 +24,14 @@
const image = article.image() const image = article.image()
</script> </script>
<Link <div data-component="ArticleItem" class="cv relative w-full card card-interactive">
class="cv flex flex-col gap-2 card card-interactive w-full" <!-- An overlay rather than a wrapper: the card carries a profile button and the room and action
href={makeArticlePath(url, getAddress(event))}> links, and none of those may sit inside an anchor. -->
<Link
class="absolute inset-0 rounded-2xl"
href={makeArticlePath(url, getAddress(event))}
aria-label={title || "Untitled"} />
<div class="pointer-events-none relative flex flex-col gap-2">
{#if image} {#if image}
<img src={image} alt="" class="h-40 w-full rounded-2xl object-cover" /> <img src={image} alt="" class="h-40 w-full rounded-2xl object-cover" />
{/if} {/if}
@ -42,11 +47,13 @@
expandMode="inline" expandMode="inline"
minLength={100} minLength={100}
maxLength={300} /> maxLength={300} />
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row"> <div
class="pointer-events-auto flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
<span class="whitespace-nowrap py-1 text-sm opacity-75"> <span class="whitespace-nowrap py-1 text-sm opacity-75">
Written by Written by
<ProfileLink pubkey={event.pubkey} {url} /> <ProfileLink pubkey={event.pubkey} {url} />
</span> </span>
<ArticleActions showRoom showActivity {url} {event} /> <ArticleActions showRoom showActivity {url} {event} />
</div> </div>
</Link> </div>
</div>

View file

@ -204,7 +204,7 @@
{@html renderAsHtml(parsed)} {@html renderAsHtml(parsed)}
<button <button
type="button" type="button"
class="text-sm underline" class="pointer-events-auto text-sm underline"
onclick={stopPropagation(preventDefault(expand))}> onclick={stopPropagation(preventDefault(expand))}>
Read more Read more
</button> </button>

View file

@ -11,7 +11,7 @@
replaceState = false, replaceState = false,
...restProps ...restProps
}: { }: {
children: Snippet children?: Snippet
href: string href: string
external?: boolean external?: boolean
replaceState?: boolean replaceState?: boolean
@ -19,6 +19,7 @@
class?: string class?: string
style?: string style?: string
"data-tip"?: string "data-tip"?: string
"aria-label"?: string
} = $props() } = $props()
const go = (e: Event) => { const go = (e: Event) => {