mirror of
https://github.com/block/buzz.git
synced 2026-11-10 11:21:05 +00:00
test(desktop): wait on real readiness signals in audience and community-rail specs (#9277)
## Summary
Split out of the `flakey-monday` flake-fix series. Three Desktop E2E
tests in `persistent-agent-audience.spec.ts` and
`community-rail.spec.ts` acted on signals that could arrive before the
app state they implied (a logged command before the submit lock
rendered, a URL check that only held before a view transition landed).
They now wait on the readiness signals the product actually exposes. No
product code changes.
Commits (cherry-picked from `flakey-monday`, original messages kept):
- `test(desktop): wait for composer focus and send cycles in audience
specs`
- `test(desktop): assert the kept remembered route after the community
switch`
- `test(desktop): assert Home after repairing the remembered channel`
### Root cause and fix per test
**`a manual mention persists when automatic mentions are enabled`**
(`desktop/tests/e2e/persistent-agent-audience.spec.ts`):
`locator.fill("follow up")` failed because `message-input` was not
editable.
- Root cause (from the code and CI symptom; not reproduced locally): the
test sends with a 1.5 s mock send delay, during which the composer is
submit-locked and `contenteditable="false"`. The lock is React state and
renders a frame after Enter. The test's last two checks before the fill
could both pass before that frame: the outgoing-mention poll (the e2e
bridge logs a command when invoked, before the delay) and the
`contenteditable="true"` wait. Playwright's `fill` rejects a
`contenteditable="false"` element outright instead of waiting.
- Fix: after Enter, wait for the full send cycle: `data-submit-locked`
"true", `contenteditable` "false", `data-submit-locked` "false",
`contenteditable` "true". Only then assert on the editor. Existing
assertions are unchanged.
**`the mention setting is reachable and operable by keyboard`** (same
spec): the thread composer's mention autocomplete never became visible.
- Root cause (likely; not reproduced locally): opening a thread by URL
mounts both the channel composer and the thread composer.
`useComposerAutofocus` focuses whichever mounts first and TipTap defers
`focus()` by a frame. A click and fill that land before that deferred
focus lose it, and the focus-gated mention menu unmounts.
- Fix: a `readyThreadComposerInput()` helper waits for the thread editor
to be editable and for exactly one `message-input` to hold focus, so the
app's own autofocus has landed. Both tests use it.
**`community rail > does not repair a remembered channel until live
validation succeeds`** (`desktop/tests/e2e/community-rail.spec.ts`):
`expect(page).not.toHaveURL(/#\/channels\/general$/)` failed right after
the click on community B.
- Root cause (confirmed; 21/40 failures at 14 workers on the old spec):
`switchCommunity` runs inside a view transition whose callback writes
B's remembered channel into the hash and then activates B, a few frames
after the click returns. By design the URL is then `#/channels/general`,
which `AppShell` keeps until a live channel read succeeds and only then
repairs to Home. The inverted check from 631b05c88 passed only when its
first poll beat the view transition.
- Fix: wait for B's rail button to have `aria-current="true"` (switch
finished), then assert the URL is `#/channels/general`, the kept route.
After live validation repairs storage to Home, assert the Home route
`/#\/$/` (the follow-up commit; `not.toHaveURL(general)` would have
passed for any route, and `AppShell`'s repair calls `goHome({ replace:
true })`).
### Related issue
N/A (flake fixes; none found).
### Testing
Human testing is still required. This PR does not carry
`buzz-review-completed`; the author has not yet run these specs
themselves on this branch.
#### Verification
Verified locally on the `flakey-monday` branch before the split
(Playwright smoke project, e2e build):
- Audience spec: neither flake reproduced before the fix, even with CPU
throttled 4x and 8x. After the fix, both tests 40 repeats each at 10
workers: 80/80; whole spec: 35/35.
- Community-rail test: 40 repeats at 14 workers: 40/40; whole spec:
27/27 at 4 workers. The Home assertion was falsified: with the repair
temporarily sending the app to `#/channels/random`, the new assertion
failed and the previous `not.toHaveURL` check passed.
- The four desktop flake tests from the series together, 10 repeats each
at 8 workers: 40/40.
- Two untouched tests in these specs are pre-existing flakes a baseline
build fails as often, and are out of scope: community-rail "enters a
remembered channel before live validation completes" and
persistent-agent-audience "the mention button preserves manual
rementions after undo until explicitly readdressed".
Nothing was re-run on this split branch: the workstation was overloaded,
so no tests, builds or linters were executed after cherry-picking. CI on
this PR is the verification for the split branch.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: mtoohey <mtoohey@squareup.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -934,7 +934,16 @@ test.describe("community rail", () => {
|
||||
),
|
||||
).toBe(1);
|
||||
await page.getByTestId(`community-rail-button-${COMMUNITY_B.id}`).click();
|
||||
await expect(page).not.toHaveURL(/#\/channels\/general$/);
|
||||
// The switch runs inside a view transition, so its update (which writes the
|
||||
// remembered channel into the route, then activates the community) lands
|
||||
// frames after the click returns. Wait for it to finish before judging the
|
||||
// route; checking the URL right after the click only passes when it has not
|
||||
// run yet. The remembered route is kept until the live read validates it,
|
||||
// so the channel absent from the cached list is not repaired away.
|
||||
await expect(
|
||||
page.getByTestId(`community-rail-button-${COMMUNITY_B.id}`),
|
||||
).toHaveAttribute("aria-current", "true");
|
||||
await expect(page).toHaveURL(/#\/channels\/general$/);
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate((communityId) => {
|
||||
@@ -1021,6 +1030,8 @@ test.describe("community rail", () => {
|
||||
}, COMMUNITY_B.id),
|
||||
)
|
||||
.toEqual({ kind: "home" });
|
||||
// Live validation succeeded, so the missing channel is now repaired to Home.
|
||||
await expect(page).toHaveURL(/#\/$/);
|
||||
});
|
||||
|
||||
test("does not restore a remembered destination on cold boot", async ({
|
||||
|
||||
@@ -90,6 +90,23 @@ function threadComposer(page: Page) {
|
||||
return page.getByTestId("thread-composer-overlay");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the thread composer's editor once the app has finished placing
|
||||
* focus on its own. Opening a thread by URL mounts the channel and thread
|
||||
* composers, and whichever mounts first autofocuses itself a frame later
|
||||
* (TipTap's `focus()` is deferred). A click that lands before that frame is
|
||||
* undone by the late autofocus, which blurs the editor the test just typed in
|
||||
* and unmounts its focus-gated mention menu.
|
||||
*/
|
||||
async function readyThreadComposerInput(page: Page) {
|
||||
const input = threadComposer(page).getByTestId("message-input");
|
||||
await expect(input).toHaveAttribute("contenteditable", "true");
|
||||
await expect(page.locator('[data-testid="message-input"]:focus')).toHaveCount(
|
||||
1,
|
||||
);
|
||||
return input;
|
||||
}
|
||||
|
||||
async function readComposerCaret(input: Locator) {
|
||||
return input.evaluate((element) => {
|
||||
const selection = window.getSelection();
|
||||
@@ -833,7 +850,7 @@ test("the mention setting is reachable and operable by keyboard", async ({
|
||||
await openThread(page);
|
||||
|
||||
const mainComposer = threadComposer(page);
|
||||
const mainInput = mainComposer.getByTestId("message-input");
|
||||
const mainInput = await readyThreadComposerInput(page);
|
||||
await mainInput.click();
|
||||
await mainInput.fill("@Mor");
|
||||
const list = mainComposer.getByTestId("mention-autocomplete");
|
||||
@@ -934,7 +951,7 @@ test("a manual mention persists when automatic mentions are enabled", async ({
|
||||
await openThread(page);
|
||||
|
||||
const composer = threadComposer(page);
|
||||
const input = composer.getByTestId("message-input");
|
||||
const input = await readyThreadComposerInput(page);
|
||||
await input.fill("@Mor");
|
||||
await expect(composer.getByTestId("mention-autocomplete")).toBeVisible();
|
||||
await input.press("Tab");
|
||||
@@ -987,6 +1004,18 @@ test("a manual mention persists when automatic mentions are enabled", async ({
|
||||
await input.type("hello");
|
||||
await input.press("Enter");
|
||||
|
||||
// The editor locks one frame after Enter (isSending disables it, blurring
|
||||
// it) and unlocks when the delayed send settles. Wait for that whole cycle
|
||||
// before asserting on the editor; its focus and editability checks all hold
|
||||
// vacuously in the frame before the lock lands.
|
||||
const composerForm = composer.getByTestId("message-composer");
|
||||
await expect(composerForm).toHaveAttribute("data-submit-locked", "true");
|
||||
await expect(input).toHaveAttribute("contenteditable", "false");
|
||||
await expect(composerForm).toHaveAttribute("data-submit-locked", "false", {
|
||||
timeout: 5_000,
|
||||
});
|
||||
await expect(input).toHaveAttribute("contenteditable", "true");
|
||||
|
||||
await expect(input).toHaveText("@Morgarita ", { timeout: 2_500 });
|
||||
await expect(input.locator("[data-placeholder]")).toHaveCount(0);
|
||||
await expect(input).toBeFocused();
|
||||
|
||||
Reference in New Issue
Block a user