From 024d0529ea693ebcf6a74b5a49389866f77bd241 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Thu, 15 Oct 2026 18:15:10 -0700 Subject: [PATCH] test: cover chat facet $type end to end The web spec reads the record the send path POSTs and requires the facet $type the place lexicon defines: that is where the missing one was observable. The maestro flow round-trips a message with a URL on Android. --- .maestro/logged-in/chat-facets.yaml | 45 +++++++++++++++++ js/e2e-web/flows/chat-facets.spec.ts | 75 ++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 .maestro/logged-in/chat-facets.yaml create mode 100644 js/e2e-web/flows/chat-facets.spec.ts diff --git a/.maestro/logged-in/chat-facets.yaml b/.maestro/logged-in/chat-facets.yaml new file mode 100644 index 000000000..7a8525749 --- /dev/null +++ b/.maestro/logged-in/chat-facets.yaml @@ -0,0 +1,45 @@ +appId: ${APP_ID} +--- +# A message whose text contains a URL has to be accepted, round-tripped +# through the PDS and firehose, and rendered again on relaunch: the facets +# the send path detects and stamps travel with the record. The runner +# configures the harness and signs in before running any logged-in flow. +- launchApp +- extendedWaitUntil: + visible: + id: "home-stream-card" + timeout: 30000 +- tapOn: + id: "home-stream-card" +# chat needs the stream's context, which arrives over its websocket along +# with this system message +- extendedWaitUntil: + visible: + text: ".*e2e test stream.*" + timeout: 30000 +# resend a send the page wasn't ready for, as in chat-reply +- retry: + maxRetries: 3 + commands: + - tapOn: + id: "chat-input" + - eraseText: 50 + - inputText: "read the https://dist.town/release.software docs" + - pressKey: Enter + - extendedWaitUntil: + visible: + # the row's text is the whole line, "@: " + text: ".*: read the https://dist.town/release.software docs" + timeout: 15000 +- takeScreenshot: chat-facets-sent +# Relaunch, so the message comes from the node's chat history: the chat box +# echoes sends optimistically, and only a record that round-tripped through +# the PDS and firehose can show up here. +- launchApp +- tapOn: + id: "home-stream-card" +- extendedWaitUntil: + visible: + text: ".*: read the https://dist.town/release.software docs" + timeout: 30000 +- takeScreenshot: chat-facets-history diff --git a/js/e2e-web/flows/chat-facets.spec.ts b/js/e2e-web/flows/chat-facets.spec.ts new file mode 100644 index 000000000..1e4d47d37 --- /dev/null +++ b/js/e2e-web/flows/chat-facets.spec.ts @@ -0,0 +1,75 @@ +import { expect, test } from "@playwright/test"; +import { loginThroughPds } from "./login"; + +// A message's link facets have to be typed with the place lexicon, since +// that is what place.stream.richtext.facet requires and what anything +// validating a chat record holds them to. The mobile counterpart is +// .maestro/logged-in/chat-facets.yaml. See flows/login.ts for the OAuth +// machinery; it needs the harness's HTTPS mode. +const HTTPS_URL = process.env.SERVER_HTTPS_URL; + +test.skip(!HTTPS_URL, "harness started without its HTTPS hostnames"); + +test("chat-facets: send a link and check the facets it writes", async ({ + page, +}) => { + await loginThroughPds(page); + + await page.goto(`${HTTPS_URL}/`); + await page.getByTestId("home-stream-card").first().click(); + // chat needs the streamer's profile, which arrives over the stream's + // websocket along with this system message + await expect( + page.getByText("Now streaming - e2e test stream").first(), + ).toBeVisible({ timeout: 30_000 }); + + const chatInput = page.getByPlaceholder("Type a message..."); + const marker = `link facet check ${Date.now()}`; + const link = "https://dist.town/release.software"; + const message = `${marker}: read the ${link} docs`; + const messageCreateResponse = page.waitForResponse((response) => { + const request = response.request(); + return ( + request.method() === "POST" && + new URL(response.url()).pathname === + "/xrpc/com.atproto.repo.createRecord" && + request.postData()?.includes(message) === true + ); + }); + // a send the page wasn't ready for clears the box and is dropped; resend + await expect(async () => { + await chatInput.fill(message); + await chatInput.press("Enter"); + await expect(page.getByText(marker).first()).toBeVisible({ + timeout: 10_000, + }); + }).toPass({ timeout: 45_000 }); + + const response = await messageCreateResponse; + expect( + response.ok(), + `chat record creation returned ${response.status()}`, + ).toBe(true); + // detectFacets leaves link facets untyped and types mention facets as + // app.bsky's, and a record carrying either is rejected by anything + // validating it against place.stream.richtext.facet. + expect(response.request().postDataJSON()).toMatchObject({ + record: { + text: message, + facets: [ + { + $type: "place.stream.richtext.facet", + features: [{ $type: "app.bsky.richtext.facet#link", uri: link }], + }, + ], + }, + }); + + // The box shows sends optimistically, so reload: only the node's chat + // history can put the message back, and it only has what the firehose + // brought back from the PDS. + await page.reload(); + await expect(page.getByText(marker).first()).toBeVisible({ + timeout: 30_000, + }); +}); -- 2.54.0