Files
buzz/desktop/playwright.config.ts
Kalvin C 1fc6a71ef9 feat(desktop): add dismissible banner pointing to the new buzz app (#9379)
## Summary

The Buzz 1.0 desktop app now lives in
[block/buzz-app](https://github.com/block/buzz-app). This adds a banner
to the 0.X desktop app that tells users, and lets them download the new
app's installer without leaving the app.

- A full-width banner sits under the top chrome: "A new Buzz app is
here! This version is being retired, so grab the new one whenever you
get a chance."
- **Upgrade** downloads the installer for this machine inside the app. A
dialog shows the progress, then the saved file's name and folder with
**Show in Finder** (worded per platform) and **Done**. The file goes to
the Downloads folder and is revealed in the file manager. It is never
opened, mounted or run.
- **Cancel**, Escape or the dialog's close button stops a running
download and removes the partial file.
- When no installer can be resolved for this machine (unsupported
platform, or the manifest fetch fails or is slow), **Upgrade** opens
`https://buzz.xyz` in the browser instead.
- **More info** opens the [upgrade
guide](https://github.com/block/buzz-app/blob/main/docs/upgrading.md) in
the browser.
- Dismissing (×, labelled "Remind me later") stores a timestamp in
`localStorage`. The banner is read once per mount, so it comes back on
the next launch (or community switch) once 24h have passed.
- It is hidden in the huddle room window.

## Demo


https://github.com/user-attachments/assets/1f1916e8-f32d-4ec0-8e60-fe7c85dd4a57

Recorded on macOS (Apple Silicon) from a native debug build with the
real Rust backend: a full download, **Done**, then a second download
cancelled part-way.

- The network is throttled through a local proxy so the progress stays
visible. Unthrottled, the installer arrives in about two seconds.
- The app was launched with a scratch home folder, so the path in the
dialog is a scratch path, and the Finder window that the reveal opens is
left out of the recording.
- Presses are accessibility presses, marked with an orange ring. The OS
cursor is not shown.

## How the download works

- **Which installer:** a Tauri command reports this install's key
(`macos-arm64`, `macos-x64`, `windows-x64`, `linux-appimage` or
`linux-deb`). The webview reads the matching entry from the [buzz-app
download
manifest](https://raw.githubusercontent.com/block/buzz-app/download-feed/downloads.json)
once per banner mount.
- **Validation:** Rust re-validates the URL on every call. It must be
`https://github.com/block/buzz-app/releases/download/<tag>/<file>`, with
a restricted tag and file-name character set and the extension this
machine installs. The webview names a URL and nothing else.
- **Redirects:** a dedicated HTTPS-only client follows at most 5
redirects, and only to `github.com`, `objects.githubusercontent.com` or
`release-assets.githubusercontent.com`.
- **Limits:** 512 MiB, 15 s to connect, 30 s for headers, 30 s without a
byte, 60 min in total. One download at a time.
- **Saving:** the body is written to `<name>.<random>.part` in the
Downloads folder, then renamed to `<name>` (or `<name> (1)` and so on)
without ever replacing an existing file. The partial file is removed on
failure, on cancel and at app exit. A partial left by a crash or
force-quit is removed on the next launch.
- **Downloaded-from-the-internet marking:** macOS gets the
`com.apple.quarantine` attribute a browser would write, and Windows gets
the Mark of the Web (`Zone.Identifier`). If the mark cannot be written,
the download fails and the file is not kept.
- **Failures:** the dialog says what went wrong in plain words.
Transient failures lead with **Try again**. Failures a retry cannot fix
lead with **Download in browser**, which opens the validated installer
URL.

## Testing

- Unit: `deprecationBannerVisibility.test.mjs`,
`newAppInstaller.test.mjs` (manifest and URL rules),
`newAppInstallerDownload.test.mjs` (dialog state), and a jsdom test of
the download hook.
- Rust: `new_app_installer_tests.rs` covers URL validation, the redirect
policy, limits, naming without overwrite, cancel and shutdown cleanup,
launch cleanup, and the quarantine and Mark of the Web values. CI runs
this crate's tests on Linux and on `windows-latest`, so the Mark of the
Web test writes a real alternate data stream there. The quarantine test
runs on macOS only, which CI builds but does not test.
- Smoke: `tests/e2e/deprecation-banner.spec.ts` drives the banner, the
download dialog, each failure's actions, cancel and the keyboard paths
against the mock bridge.
- Native, macOS arm64 debug build: the download was saved with
`com.apple.quarantine` set and revealed in Finder; cancel removed the
partial; the download kept running while the window was hidden; SIGTERM
and a menu quit mid-download removed the partial; after a force kill the
next launch removed it.

Not verified by hand:

- The Windows and Linux flows in a running app, including SmartScreen on
Windows.
- The Gatekeeper prompt when opening the downloaded DMG, and the macOS
Downloads-folder consent prompt, which need a signed build.

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

---------

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Matt Toohey <contact@matttoohey.com>
2026-11-13 19:08:25 +11:00

228 lines
8.4 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",
"**/deprecation-banner.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",
"**/relay-entry-guidance.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",
},
});