Skip to content

chore(deps): upgrade leptos 0.7 -> 0.8 (resolves wasm-streams dup)#387

Open
intendednull wants to merge 14 commits intomainfrom
chore/leptos-0-8-upgrade
Open

chore(deps): upgrade leptos 0.7 -> 0.8 (resolves wasm-streams dup)#387
intendednull wants to merge 14 commits intomainfrom
chore/leptos-0-8-upgrade

Conversation

@intendednull
Copy link
Copy Markdown
Owner

Summary

Bumps leptos from 0.7.8 to 0.8.19 in crates/web/Cargo.toml.

Why

CI Browser tests have been silently broken for weeks. Two transitive crates pulled different wasm-streams versions, causing a duplicate-symbol link error in the wasm test binary:

  • wasm-streams 0.4.2 <- server_fn 0.7.8 <- leptos 0.7.8
  • wasm-streams 0.5.0 <- reqwest 0.13.2 <- iroh-relay

The link error was masked by a missing pipefail in CI's bash -e {0} shell (a separate fix on ui/phase-3a-composer adds pipefail, surfacing the failure). We can't easily drop the iroh side, so the fix is to bump Leptos: server_fn 0.8.x uses wasm-streams 0.5, aligning both sides.

After this lands, ui/phase-3a-composer will rebase on main and its 39 browser tests will run on CI for the first time.

Breaking changes from 0.7 -> 0.8

None applied to this crate. The Leptos 0.8 release notes flag a few areas (LocalResource no longer wrapping in SendWrapper, server-fn error type changes, axum 0.8) but none affected our CSR-only web crate. cargo build, cargo check --target wasm32-unknown-unknown, cargo clippy, and cargo test all pass with zero source changes — only crates/web/Cargo.toml and Cargo.lock differ.

Verification

wasm-streams deduplication confirmed:

$ cargo tree -i wasm-streams --target wasm32-unknown-unknown -p willow-web
wasm-streams v0.5.0
├── reqwest v0.13.2
│   ├── iroh v0.98.1
│   │   ├── iroh-gossip v0.98.0
│   │   │   └── willow-network
│   │   │       ├── willow-client
│   │   │       │   └── willow-web
│   │   │       └── willow-web
│   │   └── willow-network (*)
│   └── iroh-relay v0.98.0
│       └── iroh v0.98.1 (*)
└── server_fn v0.8.12

Single version 0.5.0, sourced by both reqwest and server_fn.

Before this PR: two versions (0.4.2 and 0.5.0) pulled in for the wasm32 target.

Test plan

  • cargo build --workspace
  • cargo check --target wasm32-unknown-unknown -p willow-web
  • cargo check --target wasm32-unknown-unknown --workspace --exclude willow-relay --exclude willow-worker --exclude willow-replay --exclude willow-storage --exclude willow-agent
  • cargo check --tests --target wasm32-unknown-unknown -p willow-web (test binary compiles)
  • just clippy — zero warnings
  • just test — all native tests pass
  • just fmt — no diff
  • cargo tree -i wasm-streams --target wasm32-unknown-unknown -p willow-web reports a single version
  • CI Browser tests job goes green (the load-bearing verification — only observable on CI)

🤖 Generated with Claude Code

intendednull and others added 10 commits April 26, 2026 03:53
server_fn 0.8 uses wasm-streams 0.5, aligning with reqwest's
wasm-streams 0.5 (pulled by iroh-relay). This kills the
duplicate-symbol link error that has been silently breaking
CI Browser tests under bash -e (no pipefail).

After this lands, ui/phase-3a-composer rebases on main and
its 39 browser tests will run on CI for the first time.

Verified: cargo tree -i wasm-streams --target wasm32-unknown-unknown
returns a single version (0.5.x).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Steps with `id:` set on this runner inherit a shell of `bash -e {0}`
(no pipefail), so `cmd 2>&1 | tee /tmp/log` masks the upstream exit
code — `tee` always returns 0 and the step is reported as success
even when the actual command died.

Discovered while auditing browser test runs: ui/phase-3a-composer
shows `error: linking with rust-lld failed` followed by a green
conclusion, and the same is true for recent main-branch runs. The
duplicate-symbol link error from two `wasm-streams` versions has
been live for some time but invisible because the step "passes."

Fix is mechanical: force `bash --noprofile --norc -eo pipefail {0}`
on every tee-piped step (Clippy, Test, Browser tests, Playwright
E2E) so the upstream exit code surfaces.

Note: this commit will likely turn Browser tests RED on this PR —
that's the correct outcome. The dep skew between
`wasm-streams 0.4.2` (Leptos -> server_fn) and
`wasm-streams 0.5.0` (iroh -> reqwest) needs a separate fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two latent issues surfaced once the wasm-pack test binary actually ran
to completion (CI was previously masking the link error so this whole
mod was effectively unmonitored).

1. `foundation.css` ships an `@import url('https://fonts.googleapis.com/...')`
   for Fraunces/Plex/JetBrains. Headless Firefox under wasm-pack has no
   network access and the @import stalls/cancels the entire stylesheet,
   leaving every `:root` custom property unresolved. Strip @import lines
   before injecting the CSS into `<style>` so token resolution works
   offline. The fonts are irrelevant to token assertions.

2. `style.css` redeclared `--focus-ring: var(--focus-ring, …)` on the
   same `:root` selector that already inherits the token from
   `foundation.css`. A same-selector self-reference is a custom-property
   dependency cycle, which CSS resolves to the guaranteed-invalid value
   — blanking `--focus-ring` everywhere it's consumed (focus outlines,
   button/state styling). Drop the redundant declaration; foundation.css
   owns the token.

The bug also explains why focus rings on the deployed app may have looked
weaker than the spec since phase-0 — same-selector cycles are silent,
they just produce empty values.

Restores all three `foundation_tokens` browser tests:
- foundation_palette_tokens_defined
- legacy_bg_main_aliases_bg_0
- data_accent_swap_changes_moss_2

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…te contract

Phase 2a (commit 77ce56e) shipped two ad-hoc spans for the message-row
inline queue hint:

    <span class="queue-note queue-note--pending"> queued · will send on reconnect </span>
    <span class="queue-note queue-note--late">    sent earlier · arrived now    </span>

Phase 2b (commits 357e128 + 9535748) replaced both with a shared
`<InlineQueueNote>` component that consumes spec-pinned copy from
`sync_queue_copy` and renders `.inline-note.inline-note--{queued,inbound-held}`.
The Phase 2a tests were never updated to follow that refactor; with the
CI mask now lifted they fail because the old class names + the legacy
`queued · will send on reconnect` copy no longer exist.

Update both row tests to assert the live contract:

- `queue_note_late_renders_hint` now queries `.inline-note.inline-note--inbound-held`.
- `queue_note_pending_renders_hint_and_badge` queries `.inline-note.inline-note--queued`
  and asserts the spec copy from `sync_queue_copy::msg_note_queued("Mira")`
  (`queued · will send when Mira reachable`, per
  `docs/specs/2026-04-19-ui-design/sync-queue.md` §Copy / msg_note_queued_peer).

`.queued-badge` + `.message--pending` checks are unchanged — those classes
still flow from `message.rs` as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n frames

The 6 reconnection-toast / welcome-back-banner tests pre-set
`device_online = false`, mount the component, then schedule the
`device_online → true` flip inside `request_animation_frame`. They
relied on `tick().await; tick().await;` (each a `setTimeout(0)`) being
enough wall-clock time for that RAF callback to fire before the
assertions ran.

That assumption is wrong under wasm-pack's headless Firefox harness.
Every `#[wasm_bindgen_test]` runs in the same browser tab, so prior
tests leave RAF callbacks, gloo timers, and forgotten leptos owners
behind. Under enough load the next test's RAF can sit in the queue
past two `setTimeout(0)` resolutions, leaving the toast/banner unmounted
when the test asserts. Locally this manifested as a ~⅓ flake on
`reconnection_toast_dismiss_button_hides_toast` and
`reconnection_toast_fires_after_60s_offline`; on CI the same flake
sometimes hit the welcome-back-banner pair.

Add `await_animation_frame()` — a Future that resolves when the
browser actually dispatches the next RAF — and inject one between the
two `tick()` calls in every transition-driving test. Now the sequence
is deterministic:

  tick                     // initial Effect run with prev=true, online=false
  await_animation_frame    // fence — the queued RAF closure has fired
  tick                     // reactive flush of the false→true transition

Three sequential phase_2b runs and two full-suite runs (304 tests each)
go green after this change. No production code touched — the bug is
purely a test-harness synchronization gap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "server gear" button was renamed to the "grove header" during the
UI redesign; production code in channel_sidebar.rs:266-270 renders the
button with `class="grove-header sidebar-header"` and
`aria-label="grove menu"`, with no `.server-gear-btn` anywhere in
crates/web/src/.

The e2e tests + helpers kept the old selector. CI hid the failure
because Browser tests + E2E both ran under bash -e (no pipefail) and
the | tee pipe masked the exit code. With the pipefail fix on this
PR, 7 mobile-chrome E2E tests fail on the same locator timeout.

Switch to the semantic [aria-label="grove menu"] selector — robust
to class drift, works on both desktop and mobile shells.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`e2e/cross-browser-sync.spec.ts` launches both Chromium and Firefox to
verify cross-browser P2P connectivity, but `scripts/setup-e2e.sh` only
installed Chromium. Firefox-launching tests failed with:

    browserType.launch: Executable doesn't exist at
    /home/runner/.cache/ms-playwright/firefox-1509/firefox/firefox

Mirror the existing Chromium install block — same filesystem guard so
re-runs skip the download. Skip `--with-deps` for the same reason as
Chromium (sudo prompt is non-interactive in CI sandboxes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `.net-status-footer` selector no longer exists in production code —
`grep -r net-status-footer crates/web/src` is empty. The mobile branch of
the same OR-locator (`.mobile-top-bar`) still works, so only the desktop
project (which renders the desktop shell) was failing.

The test's intent is "the app shell rendered after server creation,
proving the network came up enough for the client to join a server and
mount the channel surface." On desktop the always-mounted equivalent is
`.main-pane-header` (the channel header). Use it.

Reachability and queue depth indicators (`.relay-signal-button`,
`.offline-strip`) live inside the sync-queue panel which only mounts on
demand, so neither is a stable "app loaded" proxy at this point.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The members pane is closed by default. `setupTwoPeers` opens it only
briefly to wait for display-name sync, then `closeMemberList` collapses
it again — `.right-rail` switches `data-open` to "false" and the
`match which.get()` branch unmounts MemberList entirely (right_rail.rs).
Counting `.member-item` against the closed pane returned 0, failing
the `toBeGreaterThanOrEqual(2)` assertion.

Open the pane explicitly via `openMemberList` and poll the count
instead of a fixed `waitForTimeout(1000)` so we don't race against
member-sync completion. After `kickPeer` toggles the pane during its
own flow, re-open it before re-counting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three Playwright tests timed out waiting for channels to gossip through
the relay after a `joinViaInvite`:

- `multi-peer-sync.spec.ts:48` (desktop+mobile) — pre-existing channels
  visible after join. Setup pre-creates two channels, then peer 2 joins.
  Three sequential `toBeVisible({ timeout: 30_000 })` assertions on top
  of fresh-start + invite + join can total > 120 s on CI.
- `multi-peer-mobile.spec.ts:43` — `setupTwoPeers` failed inside
  `joinViaInvite` waiting for `.channel-item` (20 s). First-channel
  arrival via gossip can take 30+ s when the relay is recovering from
  the previous serial test's teardown.

Bump the post-join `.channel-item` wait inside `joinViaInvite` from 20 s
to 60 s so the helper itself doesn't flake. Bump per-describe timeout
on the two affected specs from 120 s to 180 s for the compounded budget.

These are timing-only fixes — no behavioural change. Selectors,
production code, and assertions stay identical. The fast happy path
(first-channel visible immediately) still resolves immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@intendednull
Copy link
Copy Markdown
Owner Author

e2e is still failing, plz fix

intendednull and others added 4 commits April 26, 2026 18:29
…ation_frame

`Closure::once_into_js` enforces FnOnce at the JS boundary (throws on
second invocation), and the `js_sys::Promise` constructor body is
synchronous, so the take-once guard around `resolve` is unreachable.
Sibling helper `request_animation_frame` ten lines above already uses
the simple form — this aligns the two.

Verified via `cargo check --tests --target wasm32-unknown-unknown`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…bility assertion

Deep-review of PR #387 identified two real regressions and one weakened
test that the prior timeout-bump commit (7f88280) had been masking.

Root cause 1 — strict-mode violation on grove-menu selector. Earlier
commit a625105 swapped `.server-gear-btn` → `[aria-label="grove menu"]`
but `e2e/cross-browser-sync.spec.ts:60,136` use the bare unscoped form.
The grove header renders both in the desktop sidebar and inside
`.mobile-home`, so the bare locator matches multiple elements and
Playwright's strict mode throws instantly — no timeout absorbs that.
Scope both call sites with `${visibleShell(page)}`.

Root cause 2 — display name lost in setupTwoPeers. `getPeerId(page2)`
auto-advanced past welcome step 1 with no name supplied, so the joiner
broadcasts "anonymous" and downstream lookups for `.member-item` text
"Bob" never resolve. Thread `displayName` through `getPeerId` →
`advancePastNameStep` so step 1 commits with the intended name.

Also flip the membership-sync wait in setupTwoPeers from warn-and-continue
to throw — silent fallback hid the very regression we were debugging
(per CLAUDE.md "don't swallow errors").

Worker-nodes test — restore real relay-reachability assertion. The
`.net-status-footer` selector deleted in an earlier UI redesign was
replaced (commit 99298c9) with `.main-pane-header`, which is the
channel banner — it does not contain network indicators despite the
comment claiming otherwise. Real reachability flows through
`RelaySignalButton` (`relay_signal_button.rs:48-49` — `--ok` modifier
is set only when `Network::relay_status` reports `Reachable` from the
live iroh handshake), which is rendered only inside `<SyncQueueView>`
when `app.queue.open == true`. Add an `openSyncQueue` helper that
opens the panel via the command palette, then assert
`.relay-signal-button.relay-signal-button--ok:visible`. Mobile shell
does not yet expose the palette, so the relay assertion is desktop-
only; the shell smoke check still runs in both projects.

While wiring this up, found a latent UI bug: `app.rs` was passing no
`on_open_sync_queue` callback to `<CommandPalette>`, so the palette
catalog had the action but clicking it was a silent no-op. Add the
one-line callback that flips `app_state.queue.open`.

Revert the timeout bumps from 7f88280:
- helpers.ts post-join channel waits 60_000 → 20_000 (×2)
- multi-peer-{mobile,sync}.spec.ts test budget 180_000 → 120_000

The membership-sync `waitFor` in setupTwoPeers stays at 60_000 because
it is the one assertion in the helper that genuinely depends on cross-
peer gossip delivery — and now that it throws instead of warning, the
larger ceiling protects CI cold-start without re-introducing silent
flake.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…els test

Same anti-pattern as the prior commit fixed in `setupTwoPeers`:
`getPeerId(page2)` advances past welcome step 1 with no name, so the
subsequent `joinViaInvite(page2, _, 'Bob')` cannot commit the name —
step 1 has already been completed. The test's assertions only check
channel names, so this didn't break the test's behaviour, but the
'Bob' arg to `joinViaInvite` was silently dead. Pass the name into
`getPeerId` so the joiner actually identifies as Bob, matching the
pattern in `setupTwoPeers`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bring in 15 commits of e2e/UI fixes that landed on main while this PR
was iterating. Notably c7c9285 (toast timer cancellation) and a16506e
(profile keydown guard + cfg(test) gate) which can affect E2E timing
and unrelated-component event handling.
intendednull pushed a commit that referenced this pull request Apr 30, 2026
This reverts commit b34d820.

Reverting because the new test-hooks-guard CI job hits a pre-existing
wasm-streams duplicate-symbol link error (issue tracked by PR #387):

  rust-lld: error: duplicate symbol: __wbg_intounderlyingbytesource_free
  rust-lld: error: duplicate symbol: __wbindgen_describe_intounderlyingbytesource_*
  ...

Two transitive crates pull conflicting versions:
  wasm-streams 0.4.2 <- server_fn 0.7.8 <- leptos 0.7.8
  wasm-streams 0.5.0 <- reqwest 0.13.2 <- iroh-relay

Trunk's prod build (cargo build --release --target wasm32-unknown-unknown
on the bin target) fails to link. The existing 'browser' CI job hits the
same bug but masks it via 'wasm-pack test ... | tee' under bash -e
without pipefail (tee's exit 0 swallows wasm-pack's failure). My new
step is a single command with no pipe -> bash -e propagates the failure
-> CI red.

Won't mask via tee; per CLAUDE.md 'No swallowing errors'. #490 has to
wait for #387 (leptos 0.7 -> 0.8 upgrade collapses the wasm-streams
duplication) to land. Filing follow-up issue noting the dependency
chain. Leaving the eslint CI wiring (#491) and the js_sys::eval safe-DOM
swap (#425) in place; both are unaffected by the wasm-streams bug.

Refs #490, #387
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant