Files
buzz/desktop/playwright.config.ts
Logan Johnson 5ea5ae1727 fix(desktop): always flash a linked message in the channel timeline (#8660)
## Problem

When you follow a link to a message, the channel timeline should scroll
to it and flash the row for about 2 seconds. On `main` the flash often
does not appear:

| How you reach the message | Flash on `main` |
|---|---|
| Same channel, row already on screen | yes |
| Same channel, row loaded but off screen | **no** |
| Same channel, row in older history not loaded yet | yes |
| From another channel, row near the bottom | **no** |
| From another channel, row further up | **no**, and the timeline stays
at the bottom |

The main timeline is always virtualized. `useAnchoredScroll` had three
ways to scroll to a target there. Only one of them, "the row is already
fully on screen", fired the flash. In the other cases the scroll
returned without the flash, and the follow-up target effect marked the
target handled without checking the result, so the flash never fired.

Opening a link from another channel had a second problem. The row could
look on screen while the channel's first bottom pin was still in flight
in Virtua. The shortcut used a direct DOM `scrollTo`, which does not
cancel that pin, so Virtua kept going to the bottom and took the target
with it. Top-level message links also open the thread panel, and that
resize settled the timeline at the bottom again.

A third problem also exists on `main`. Centering a row near the top of
the loaded history loads an older page. Opening a channel also starts a
refresh of the newest channel window, and that refresh then drops the
older pages from the head of the list. A head trim was not passed to
Virtua as a `shift`, so the rows above the reader vanished with no
compensation and the view fell to the bottom about 0.3–0.8 s after the
flash started.

The deep-link e2e test stopped checking the flash in #5306, so CI did
not catch this.

## Change

- In virtualized mode, every target scroll now goes through Virtua's
`scrollToIndex` (centered). That replaces any bottom pin still in
flight. Smooth scrolling is kept only when the row is already in the
viewport.
- Every target scroll that Virtua accepts now fires the flash. The
highlight is keyed by message id, so a row that Virtua mounts after the
scroll still shows it.
- The scroll returns `true` once Virtua accepts the target and `false`
while the target is not loaded yet. The follow-up target effect and the
search retry now just read that value. This removes the "phase 1"
special case and the duplicate DOM lookup.
- A target scroll also clears the virtualizer's at-bottom state, so a
resize right after it (for example the thread panel opening) does not
pin back to the bottom.
- Only an accepted target scroll retires bottom intent. It also cancels
the hook's queued second mount-to-bottom frame. A target that is not
loaded yet leaves the opening bottom pin alone while it retries.
- `didPrependVirtualizedTimeline` now also admits a pure head trim (the
new keys are an exact suffix of the previous keys), so Virtua's `shift`
keeps the reading row in place when a window refresh drops older pages.
The oldest loaded day has no divider while more history exists, so a
head trim stays an exact suffix even when it cuts inside a day.

The non-virtualized path (thread panel) is unchanged.

## Screenshots

Before and after, from another channel, for a link to a row further up:
https://github.com/block/buzz/pull/8660#issuecomment-6481877304

## Testing

Evidence at head `653d89766`:

- New `desktop/tests/e2e/message-target-highlight.spec.ts`: a 5-row
table with the cases above. Each case asserts that the target row is on
screen **and** has the flash class at the same time, and that the row is
still on screen 2.2 s later, after the flash ends. 15/15 passed (3 runs
each).
  - On unfixed `main`, 3 of the 5 cases fail the flash check.
- With only the head-trim change reverted, "same channel, loaded off
screen" and "other channel, further up" fail the 2.2 s check.
- `useAnchoredScroll.lifecycle.test.mjs`: an on-screen virtual target
scrolls through the virtualizer, makes no direct DOM write, retires
bottom intent and does not re-pin later. New: a target that is not
loaded yet keeps the bottom pin. It fails with the old cancel order.
- `virtualizedTimelineItems.test.mjs`: new rows for a head trim inside
the oldest day and at its boundary (admitted, with the size cache
aligned), and for a trim plus append and a middle removal (rejected).
- `pnpm test` (6837 + 161 pass), `pnpm check` (tsc via pre-push, biome,
px-text, pubkey truncation).
- These e2e specs, run together: message-target-highlight,
scroll-history, virtualization, timeline-no-shift, unread-pill,
overscroll-boundary, dm-history-live-regression,
live-broadcast-reply-timeline, message-copy-link, thread-focus-mode,
thread-follow, thread-reply-anchor-roleplay, thread-unread, channels,
messaging. 257 passed, 0 failed.

Later commits at `b8e172672` add no production behavior. They move one
helper above `computeAnchor`'s doc comment, and add a lifecycle test
(from Cerveau's probe): a target that loads before the queued second
mount frame is not scrolled back to the bottom. That test fails without
the mount-frame cancellation. Pre-push `pnpm test` passed at
`b8e172672`.

Adversarial review by Auto (virtualizer and callers) and Cerveau
(lifecycle, tests and thread panel). Their must-fix findings are fixed
above, and both approved `653d89766`.

Follow-up, not in this PR: after a jump to a row at the very bottom,
centering is clamped, so no scroll event fires and the at-bottom state
stays false until the next scroll. `main` has the same gap for
`isAtBottom`.

Not tested yet in the native app.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cerveau <fc91c5708d48aecea993d9219505dc841bc7b0222b7ed325b8edfd026bfb43b5@buzz.block.builderlab.xyz>
2026-10-29 12:10:58 -04:00

226 lines
8.3 KiB
TypeScript

import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests/e2e",
timeout: 30_000,
retries: process.env.CI ? 2 : 0,
workers: 1,
reporter: [
["list"],
["html", { open: "never", outputFolder: "playwright-report" }],
],
use: {
baseURL: "http://127.0.0.1:4173",
screenshot: "only-on-failure",
trace: "on-first-retry",
video: "retain-on-failure",
},
projects: [
{
name: "smoke",
testMatch: [
"**/smoke.spec.ts",
"**/mock-subscription-readiness.spec.ts",
"**/owned-agent-discovery.spec.ts",
"**/thread-head-stale-edit.spec.ts",
"**/thread-follow.spec.ts",
"**/sidebar-offcanvas-rail.spec.ts",
"**/tooltip-semantics.spec.ts",
"**/search-scope-screenshots.spec.ts",
"**/onboarding-docked-cta-screenshots.spec.ts",
"**/identity-key-help.spec.ts",
"**/exact-key-profile.spec.ts",
"**/key-import-reveal.spec.ts",
"**/navigation.spec.ts",
"**/channels.spec.ts",
"**/channel-shared-header-backdrop.spec.ts",
"**/auxiliary-pane-close-visibility.spec.ts",
"**/channel-composer-overflow.spec.ts",
"**/badge.spec.ts",
"**/channel-browser.spec.ts",
"**/channel-add-screenshots.spec.ts",
"**/add-community-screenshots.spec.ts",
"**/hosted-communities-settings-screenshots.spec.ts",
"**/invites-settings-screenshots.spec.ts",
"**/messaging.spec.ts",
"**/bestie.spec.ts",
"**/message-feedback-snapshots.spec.ts",
"**/message-copy-link.spec.ts",
"**/custom-emoji.spec.ts",
"**/profile-custom-emoji-status.spec.ts",
"**/custom-emoji-ui.spec.ts",
"**/channel-mute.spec.ts",
"**/channel-star.spec.ts",
"**/channel-controls.spec.ts",
"**/channel-activity-popover.spec.ts",
"**/active-turn-resilience.spec.ts",
"**/agent-control-regressions.spec.ts",
"**/profile-active-turn.spec.ts",
"**/config-bridge-screenshots.spec.ts",
"**/observer-feed-screenshots.spec.ts",
"**/core-memory-screenshots.spec.ts",
"**/activity-scope-label-screenshots.spec.ts",
"**/welcome-agent-modal-screenshots.spec.ts",
"**/local-archive-screenshots.spec.ts",
"**/voice-settings.spec.ts",
"**/voice-note.spec.ts",
"**/agent-readiness-screenshots.spec.ts",
"**/agent-error-state-screenshots.spec.ts",
"**/edit-agent.spec.ts",
"**/doctor-cta-screenshots.spec.ts",
"**/pubkey-display-screenshots.spec.ts",
"**/file-attachment.spec.ts",
"**/image-attachment-gallery.spec.ts",
"**/composer-image-draw.spec.ts",
"**/video-attachment.spec.ts",
"**/spoiler.spec.ts",
"**/composer-link-shortcut.spec.ts",
"**/entity-link-recipient-cards.spec.ts",
"**/composer-selection-formatting.spec.ts",
"**/composer-tooltip-dismiss.spec.ts",
"**/mentions.spec.ts",
"**/mention-spacing.spec.ts",
"**/mention-clipboard.spec.ts",
"**/cloud-provenance.spec.ts",
"**/mention-recipients.spec.ts",
"**/remote-owned-mentions.spec.ts",
"**/forum-agent-invitation.spec.ts",
"**/team-mentions.spec.ts",
"**/persistent-agent-audience.spec.ts",
"**/relay-reconnect.spec.ts",
"**/relay-reconnect-affordance.spec.ts",
"**/workflows.spec.ts",
"**/workflow-reaction-picker.spec.ts",
"**/workflow-local-controls.spec.ts",
"**/workflow-title-stability.spec.ts",
"**/identity-archive.spec.ts",
"**/identity-archive-hide.spec.ts",
"**/relay-connectivity.spec.ts",
"**/unread-pill.spec.ts",
"**/sidebar-more-unread-overlap.spec.ts",
"**/sidebar-snapshot.spec.ts",
"**/home-collapsed-top-chrome.spec.ts",
"**/top-chrome-zoom-clearance.spec.ts",
"**/thread-unread.spec.ts",
"**/thread-load-failure.spec.ts",
"**/project-conversation-load-failure.spec.ts",
"**/huddle-thread-load-failure.spec.ts",
"**/workspace-rail.spec.ts",
"**/community-rail.spec.ts",
"**/boot-splash.spec.ts",
"**/thread-reply-anchor-roleplay.spec.ts",
"**/threadpane-ultrawide.spec.ts",
"**/thread-focus-mode.spec.ts",
"**/animated-avatar.spec.ts",
"**/reminders.spec.ts",
"**/reminder-click-repro.spec.ts",
"**/virtualization.spec.ts",
"**/scroll-history.spec.ts",
"**/message-target-highlight.spec.ts",
"**/channel-dense-second-reach.spec.ts",
"**/channel-window-mock-paging.spec.ts",
"**/channel-head-restart.spec.ts",
"**/live-broadcast-reply-timeline.spec.ts",
"**/markdown-parse-cache.spec.ts",
"**/markdown-tables.spec.ts",
"**/overscroll-boundary.spec.ts",
"**/terminal-wheel.spec.ts",
"**/cold-switch-longtask.perf.ts",
"**/timeline-no-shift.spec.ts",
"**/human-edit-agent-content.spec.ts",
"**/empty-edit-delete.spec.ts",
"**/reaction-order.spec.ts",
"**/reaction-names.spec.ts",
"**/inbox-reactions.spec.ts",
"**/inbox-edit.spec.ts",
"**/send-channel-binding.spec.ts",
"**/project-cold-start.spec.ts",
"**/project-commit-detail.spec.ts",
"**/project-empty-state-alignment.spec.ts",
"**/project-inbox.spec.ts",
"**/projects-v3-screenshots.spec.ts",
"**/project-issue-comments.spec.ts",
"**/project-pr-review.spec.ts",
"**/persona-model-combobox-screenshots.spec.ts",
"**/drafts-screenshots.spec.ts",
"**/drafts-all-fix-screenshots.spec.ts",
"**/inbox-refactor-screenshots.spec.ts",
"**/inbox-title-overlap.spec.ts",
"**/message-author-overlap.spec.ts",
"**/buzz-theme-screenshots.spec.ts",
"**/appearance-previews.spec.ts",
"**/channel-sort.spec.ts",
"**/identity-lost.spec.ts",
"**/deep-link-invite.spec.ts",
"**/invite-link-copy.spec.ts",
"**/global-agent-config-screenshots.spec.ts",
"**/doctor-states.spec.ts",
"**/onboarding-avatar-skip.spec.ts",
"**/onboarding-backup.spec.ts",
"**/onboarding-agent-defaults.spec.ts",
"**/nostr-bind.spec.ts",
"**/mobile-pairing-qr.spec.ts",
"**/profile-nsec-reveal.spec.ts",
"**/profile-backup-settings.spec.ts",
"**/signout-confirmation.spec.ts",
"**/settings-section-layout.spec.ts",
"**/experimental-features.spec.ts",
"**/agent-provider-dropdowns.spec.ts",
"**/agent-lifecycle-feedback.spec.ts",
"**/agent-access-warning.spec.ts",
"**/edit-agent-run-on.spec.ts",
"**/claude-effort-picker.spec.ts",
"**/inbox-live-update.spec.ts",
"**/mesh-compute.spec.ts",
"**/observer-archive-policy.spec.ts",
"**/harness-management.spec.ts",
"**/harness-catalog-screenshots.spec.ts",
"**/inline-custom-harness.spec.ts",
"**/where-to-run-config.spec.ts",
"**/huddle-transcription.spec.ts",
"**/agent-numeric-tuning.spec.ts",
"**/needs-restart-screenshots.spec.ts",
"**/team-catalog-screenshots.spec.ts",
],
use: {
...devices["Desktop Chrome"],
},
},
{
name: "integration",
testMatch: [
"**/agents.spec.ts",
"**/agent-availability.spec.ts",
"**/agent-snapshot-recipient.spec.ts",
"**/onboarding.spec.ts",
"**/stream.spec.ts",
"**/integration.spec.ts",
"**/dm-double-notification.spec.ts",
"**/profile.spec.ts",
"**/sidebar.spec.ts",
"**/sidebar-relay-card.spec.ts",
"**/tokens.spec.ts",
"**/persona-env-vars.spec.ts",
"**/persona-sync.spec.ts",
"**/team-snapshot.spec.ts",
"**/team-catalog.spec.ts",
"**/agents-everywhere.live.spec.ts",
"**/relay-restart.live.spec.ts",
"**/parity-ancestor-island.spec.ts",
],
use: {
...devices["Desktop Chrome"],
},
expect: {
timeout: process.env.CI ? 15_000 : 10_000,
},
},
],
webServer: {
command: "python3 -m http.server 4173 -d dist",
cwd: ".",
reuseExistingServer: !process.env.CI,
url: "http://127.0.0.1:4173",
},
});