diff --git a/.changeset/held-frame-verdict-parity.md b/.changeset/held-frame-verdict-parity.md new file mode 100644 index 0000000000..b8760437bd --- /dev/null +++ b/.changeset/held-frame-verdict-parity.md @@ -0,0 +1,5 @@ +--- +"@solidjs/signals": patch +--- + +A render effect in a branch an action holds (` 0}>` opened by the action's write) no longer runs ahead of that branch's landing when it reads `latest(y)` inline, while the same read through a memo waited: a member of the frame a held pass built is the hold's own work, so neither form seats it as the hold's verdict reader, and memo wrapping no longer changes what it shows. diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md index 5668d65508..8ff43f4be8 100644 --- a/packages/signals/docs/RULES-INDEX.md +++ b/packages/signals/docs/RULES-INDEX.md @@ -73,7 +73,7 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul | A26 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:111` | — | action-await-contract.test.ts×2 fuzz-findings-l2.test.ts×1 posture-store-parity.test.ts×2 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 visibility-oracle.test.ts×1 | [ruled 2026-07-17] An ambient transaction window is one flush; parking is flush-driven — (**ruled 2026-07-17**, #2913; **enforcement hardened 2026-08-31**, #3141 — parking is flush-driven, and a trans… | | A27 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:303` | — | loading-value.test.ts×2 visibility-oracle.states.ts×18 visibility-oracle.test.ts×1 | [ruled 2026-08-10] The commit-#0 loading window is loading-class and verdict-quiet — (**ruled 2026-08-10**) **The commit-#0 loading window is loading-class and verdict-quiet.** A node born committed v… | | A28 | ruled, mechanism landed | `docs/SPEC-ASYNC-SEMANTICS.md:95` | constants.ts×1 core.ts×12 lanes.ts×2 scheduler.ts×3 seat.ts×3 types.ts×1 verdict.ts×2 store.ts×5 | continuation-sibling-reader-4146.test.ts×1 createOptimistic.test.ts×5 fuzz-findings-l2.test.ts×6 held-derivation-not-a-proposal-3612.test.ts×1 issue-3800-repro.test.ts×1 latest-held-till-flush.test.ts×1 latest-memo-inline-parity.test.ts×1 posture-store-parity.test.ts×5 question-scoped-pending.test.ts×3 snapshot-derived-store-rows.test.ts×1 createOptimisticStore.test.ts×10 optimistic-draft-visibility-3665.test.ts×5 optimistic-list-mutation-matrix.harness.ts×1 optimistic-list-mutation-matrix.test.ts×2 shallow.test.ts×1 woken-transaction-adopts-staged-bump.test.ts×1 treeshake.test.ts×2 verdict-contract.test.ts×1 visibility-oracle-store.states.ts×8 visibility-oracle.states.ts×8 | [ruled, mechanism landed 2026-09-15] A write becomes visible at flush — to every channel — (**ruled 2026-09-08**; supersedes the #2922 mid-tick pull) **A write becomes visible at flush — to every chan… | -| A29 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:119` | boundaries.ts×1 action.ts×1 constants.ts×1 core.ts×14 effect.ts×1 lanes.ts×1 scheduler.ts×4 signals.ts×1 store.ts×1 | adoption-unchanged-key-read-3706.test.ts×9 body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 boundary-not-born-held-3540.test.ts×4 createProjection.draft-lifetime-3585.test.ts×1 direct-commit-readers-posture.test.ts×1 fallback-born-creator-4428.test.ts×1 fuzz-findings-l2.test.ts×6 held-branch-verdict-reader-4010.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 held-truth-lane-only.test.ts×3 l2-contract.test.ts×1 latest-held-till-flush.test.ts×2 loading-fallback-in-flush-3540.test.ts×6 loading-on-frame-following-3540.test.ts×1 mount-over-foreign-hold-3761.test.ts×1 optimistic-mount-nested-memo-3835.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×3 posture-born-held-and-observation.test.ts×1 posture-store-parity.test.ts×6 derived-presence-async-3726.test.ts×3 optimistic-untracked-reads-f3-f5.test.ts×1 store-unchanged-read-independent-write-3688.test.ts×1 tick-scoped-pass-transaction.test.ts×2 treeshake.test.ts×3 verdict-born-into-other-flush-4426.test.ts×1 verdict-mount-first-pass-3851.test.ts×2 verdict-mount-loading-flush-loop.test.ts×2 visibility-oracle-store.states.ts×5 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-09-13 (#3408)] A tracked read served a live transaction's staged value enters that transaction — A tracked computation served a node's staged `_pendingValue` — a value a … | +| A29 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:119` | boundaries.ts×1 action.ts×1 constants.ts×1 core.ts×14 effect.ts×1 lanes.ts×1 scheduler.ts×4 signals.ts×1 store.ts×1 | adoption-unchanged-key-read-3706.test.ts×9 body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 boundary-not-born-held-3540.test.ts×4 createProjection.draft-lifetime-3585.test.ts×1 direct-commit-readers-posture.test.ts×1 fallback-born-creator-4428.test.ts×1 fuzz-findings-l2.test.ts×6 held-branch-verdict-reader-4010.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 held-frame-verdict-parity.test.ts×1 held-truth-lane-only.test.ts×3 l2-contract.test.ts×1 latest-held-till-flush.test.ts×2 loading-fallback-in-flush-3540.test.ts×6 loading-on-frame-following-3540.test.ts×1 mount-over-foreign-hold-3761.test.ts×1 optimistic-mount-nested-memo-3835.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×3 posture-born-held-and-observation.test.ts×1 posture-store-parity.test.ts×6 derived-presence-async-3726.test.ts×3 optimistic-untracked-reads-f3-f5.test.ts×1 store-unchanged-read-independent-write-3688.test.ts×1 tick-scoped-pass-transaction.test.ts×2 treeshake.test.ts×3 verdict-born-into-other-flush-4426.test.ts×1 verdict-mount-first-pass-3851.test.ts×2 verdict-mount-loading-flush-loop.test.ts×2 visibility-oracle-store.states.ts×5 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-09-13 (#3408)] A tracked read served a live transaction's staged value enters that transaction — A tracked computation served a node's staged `_pendingValue` — a value a … | | A30 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:259` | async.ts×3 attribution.ts×1 constants.ts×1 core.ts×1 effect.ts×1 lanes.ts×1 scheduler.ts×5 projection.ts×1 | async-landing-deps-3461.test.ts×3 fuzz-findings-l2.test.ts×3 held-conditional-effect.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 ispending-in-boundary-on-3528.test.ts×1 lane-frame-deferred-run-3662.test.ts×1 lane-frame-held-lane-3662.test.ts×1 posture-born-held-and-observation.test.ts×1 projection-peek-loop-4342.test.ts×1 treeshake.test.ts×2 write-proposals-3494.test.ts×2 zombie-rerun-after-commit-3546.test.ts×2 | [ruled 2026-09-13 (#3410)] A memo's dependencies are the committed frame's until the frame is replaced — A pass that _staged_ its value has not replaced the committed frame, so the committed value sti… | | A31 | live | `docs/SPEC-ASYNC-SEMANTICS.md:133` | boundaries.ts×1 core.ts×1 lanes.ts×1 verdict.ts×2 | fuzz-findings-l2.test.ts×6 ispending-combined-atomic-3442.test.ts×1 probe-pulled-pass-4429.test.ts×1 unowned-memo-ispending-reader-4366.test.ts×1 | [live 2026-09-14 (#3442)] A memo computes under its own lane posture, never its puller's — A memo's value is one shared slot every reader sees, so its pass runs under the lane posture the memo itself … | | A32 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:141` | core.ts×4 lanes.ts×1 seat.ts×1 store.ts×1 | visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 | [ruled 2026-09-14] Children-forbidden readers see the frame, not the graph — `createTrackedEffect` and `onSettled` callbacks are effect-phase code that runs after the frame is decided. They read the f… | diff --git a/packages/signals/src/core/lanes.ts b/packages/signals/src/core/lanes.ts index 515d3580ce..483e06fa22 100644 --- a/packages/signals/src/core/lanes.ts +++ b/packages/signals/src/core/lanes.ts @@ -286,7 +286,17 @@ export function enterLane(l: Transaction, c: Computed): void { // does not break it out — it stays the frame's, as the same read inline // does (verdict.ts `markVerdictReader`, `route`; D-25, D-26). An // effect's own reads do not count (its `_type`, 1–3, carries neither bit). - if (!(verdict && creatorReads(c, (c as any)._type || f))) { + // Nor by a member of the frame a held pass of that lane's hold built, + // on any later run (CONFIG_HELD_FRAME, `bornInto`): born in the hold's + // world, it is the hold's own work and its run is the frame's, at the + // landing (D-25, D-27) — as `route` keeps it for the inline `latest()`. + if ( + !( + verdict && + (creatorReads(c, (c as any)._type || f) || + (c._config & CONFIG_HELD_FRAME && bornInto(c, resolveTx(l._parent!)))) + ) + ) { setPassLane(l); // The pass read a held write before its first lane read (it joined the // frame's future as mainline would): lane work sees the screen — the @@ -713,17 +723,11 @@ export function laneRead(c: Computed | null, el: Signal | Computed seat.world) seat.world = seat.ownWorld; // Display-ahead (today `route`): the pass is the hold's verdict reader // unless already seated, or already the frame's (it read the future or a - // staging: world > S). - if (p.raise & MARK_AH && p.tx !== null && !seated(seat) && seat.ownWorld === WORLD_S) + // staging: world > S), or a member of the frame a held pass of that hold + // built (the held-frame clause; D-25: born in the hold's world, it reads + // plainly — a probe does not rescue it; D-27: its runs are the frame's, + // which commits with the hold). + if ( + p.raise & MARK_AH && + p.tx !== null && + !seated(seat) && + seat.ownWorld === WORLD_S && + !(seatBornInto(seat, p.tx) && seat.node!._config & CONFIG_HELD_FRAME) + ) seat.vhold = resolveTx(p.tx); if (p.raise & RAISE_VSEAT && p.tx !== null) { // (A lane the pass sat in links to the verdict lane, `enterLane`.) @@ -718,9 +727,15 @@ export function slot( // verdict lane's — the pass is the hold's verdict reader, unless its // branch exists only in the future (`creatorReads`: a pass whose // running creators, or itself when a memo, read the future or a - // staging — D-25, D-26, #4341). + // staging — D-25, D-26, #4341), or a member of the frame a held pass + // of the lane's hold built (the held-frame clause, D-25 D-27). const enter = vlane - ? inSeat || !(seat!.creatorsWorld > WORLD_S || (!isEffect && seat!.ownWorld > WORLD_S)) + ? inSeat || + !( + seat!.creatorsWorld > WORLD_S || + (!isEffect && seat!.ownWorld > WORLD_S) || + (seatBornInto(seat, l._parent!) && c!._config & CONFIG_HELD_FRAME) + ) ? RAISE_VSEAT : 0 : RAISE_LANE; @@ -1272,28 +1287,6 @@ const known: KnownDifference[] = [ flushTransaction !== null && seatBornInto(seat, txOf(el!)) && resolveTx(flushTransaction) !== resolveTx(txOf(el!)) - }, - { - // laneRead's D-28 reset compares verdict lanes by identity: a lane whose - // parent merged into the seat's hold reads as another hold's. - issue: 4427, - kind: "slot", - row: "V6", - expected: SLOT_LV, - actual: SLOT_P, - when: (c, el, seat) => { - const l = el!._x?._transaction; - return ( - (c as any)._type === EFFECT_RENDER && - seat.vhold !== null && - !!l && - l._lane && - laneOf(l) === null && - l._parent !== null && - resolveTx(l._parent) !== l._parent && - resolveTx(l._parent) === resolveTx(seat.vhold) - ); - } } ]; const knownHits = new Map(); diff --git a/packages/signals/src/core/verdict.ts b/packages/signals/src/core/verdict.ts index 6138eba09c..aee6285166 100644 --- a/packages/signals/src/core/verdict.ts +++ b/packages/signals/src/core/verdict.ts @@ -14,6 +14,7 @@ import { CONFIG_GUESS, CONFIG_ASYNC, CONFIG_HELD, + CONFIG_HELD_FRAME, CONFIG_INPUTS_PUBLISHED, CONFIG_OVERRIDE, CONFIG_STAGED_ERROR, @@ -62,6 +63,7 @@ import { joinFuture, passLane, queuePendingNode, + resolveTx, setPassLane, stagedReaders, staleReader, @@ -94,10 +96,18 @@ function watchVerdict(c: Computed, unanswered = false): void { /** The reading pass is the holder's verdict lane's — unless already lane * work, or already the transaction's (it derives from the future — a held * node's, or the staging of a write this flush may hold — and the verdict - * does not rescue it: its result shows at the landing, re-derived). A lane + * does not rescue it: its result shows at the landing, re-derived), or a + * member of the frame a held pass of that hold built (CONFIG_HELD_FRAME, + * `bornInto`): the hold's own work, born in its world, reads plainly and + * its run is the frame's (D-25, D-27) — the same leaf reading a memo of the + * `latest()` waits for the landing too (`enterLane`; E2, #4284). A lane * read, for `recompute`'s tail (REACTIVE_LANE_READ). */ function route(c: Computed, t: Transaction): void { - if (!passLane && !(c._flags & (REACTIVE_JOINED | REACTIVE_STAGED_READ))) + if ( + !passLane && + !(c._flags & (REACTIVE_JOINED | REACTIVE_STAGED_READ)) && + !(c._config & CONFIG_HELD_FRAME && bornInto(c, resolveTx(t))) + ) setPassLane(verdictLane(t)); c._flags |= REACTIVE_LANE_READ; } diff --git a/packages/signals/tests/held-frame-verdict-parity.test.ts b/packages/signals/tests/held-frame-verdict-parity.test.ts new file mode 100644 index 0000000000..74123e6ea2 --- /dev/null +++ b/packages/signals/tests/held-frame-verdict-parity.test.ts @@ -0,0 +1,181 @@ +/** + * E2 / #4284 (memo wrapping and grouping never change the result) for a + * leaf in a held frame. ` 0}>` opened by an action holding + * `y`: the branch is that hold's frame (CONFIG_HELD_FRAME — a held pass + * built it), never on screen until the landing. A leaf in it reading the + * hold's display-ahead value — `latest(y)` inline, or a memo of it (a + * verdict-lane node) — re-runs on the hold's next step. + * + * Before, the inline leaf's `latest(y)` seated it as the hold's verdict + * reader (`route`), so its run went out with the verdict lane's reveal, + * while the same read through a memo was served the memo's staging as the + * hold's own work (`laneRead`'s L7' arm) and its run waited for the + * landing: `[10, 20]` beside `[10]`. A member of the frame a held pass of + * the hold built is the hold's own work, born in its world: it reads + * plainly and a probe does not rescue it (D-25, as #4426 for its reads); + * its runs are the frame's, which commits with the hold (D-27's joined + * pass, A29 ruling A). Every form now waits for the landing. + * + * The two-hold shape (the #4455 review): A holds `x`, `m = memo(latest(x) + * * 10)` is A's verdict lane's, B holds `y`, and `k` reads both — A merges + * into B. The leaf `[latest(y), m()]` inline, through a memo, or split into + * two leaves shows the same at every step. + */ +import { describe, expect, it } from "vitest"; +import { + action, + createMemo, + createRenderEffect, + createRoot, + createSignal, + flush, + latest +} from "../src/index.js"; + +const tick = async () => { + for (let i = 0; i < 6; i++) await Promise.resolve(); + flush(); +}; + +describe("a held frame's leaf over the hold's display-ahead value", () => { + async function single(form: "inline" | "memo inside" | "memo outside") { + const [y, setY] = createSignal(0); + let b1!: () => void, b2!: () => void; + const actB = action(function* () { + setY(1); + yield new Promise(r => (b1 = r)); + setY(2); + yield new Promise(r => (b2 = r)); + }); + const leaf: unknown[] = []; + let shown: unknown; + createRoot(() => { + const outside = createMemo(() => latest(y) * 10); + const when = createMemo(() => y() > 0); + const content = createMemo(() => { + if (!when()) return "off"; + const read = + form === "inline" + ? () => latest(y) * 10 + : form === "memo inside" + ? createMemo(() => latest(y) * 10) + : outside; + createRenderEffect(read, v => void leaf.push(v)); + return "on"; + }); + createRenderEffect(content, v => { + shown = v; + }); + }); + flush(); + const steps: unknown[] = []; + actB(); + await tick(); + steps.push([shown, ...leaf]); + b1(); + await tick(); + await tick(); + steps.push([shown, ...leaf]); + b2(); + await tick(); + await tick(); + steps.push([shown, ...leaf]); + return steps; + } + + const expected = [ + ["off", 10], + ["off", 10], + ["on", 10, 20] + ]; + for (const form of ["inline", "memo inside", "memo outside"] as const) + it(`one hold, ${form}: the leaf's run waits for the landing`, async () => { + expect(await single(form)).toEqual(expected); + }); + + async function merged(form: "inline" | "memo" | "split") { + const [x, setX] = createSignal(0); + const [y, setY] = createSignal(0); + let a1!: () => void, a2!: () => void, b1!: () => void; + const actA = action(function* () { + setX(1); + yield new Promise(r => (a1 = r)); + setX(2); + yield new Promise(r => (a2 = r)); + }); + const actB = action(function* () { + setY(1); + yield new Promise(r => (b1 = r)); + }); + let shownM: unknown, shown: unknown, leaf: unknown; + createRoot(() => { + const m = createMemo(() => latest(x) * 10); + const k = createMemo(() => x() + y()); + createRenderEffect(m, v => { + shownM = v; + }); + createRenderEffect(k, () => {}); + const when = createMemo(() => y() > 0); + const content = createMemo(() => { + if (!when()) return "off"; + if (form === "inline") + createRenderEffect( + () => `${latest(y)}/${m()}`, + v => { + leaf = v; + } + ); + else if (form === "memo") { + const mm = createMemo(() => `${latest(y)}/${m()}`); + createRenderEffect(mm, v => { + leaf = v; + }); + } else { + let ly: unknown, lm: unknown; + createRenderEffect( + () => latest(y), + v => { + ly = v; + leaf = `${ly}/${lm}`; + } + ); + createRenderEffect(m, v => { + lm = v; + leaf = `${ly}/${lm}`; + }); + } + return "on"; + }); + createRenderEffect(content, v => { + shown = v; + }); + }); + flush(); + const steps: unknown[] = []; + actA(); + await tick(); + actB(); + await tick(); + steps.push([shownM, shown, leaf]); + a1(); + await tick(); + await tick(); + steps.push([shownM, shown, leaf]); + a2(); + b1(); + await tick(); + await tick(); + steps.push([shownM, shown, leaf]); + return steps; + } + + const expectedMerged = [ + [10, "off", "1/10"], + [20, "off", "1/10"], + [20, "on", "1/20"] + ]; + for (const form of ["inline", "memo", "split"] as const) + it(`merged holds, ${form}: the same screens`, async () => { + expect(await merged(form)).toEqual(expectedMerged); + }); +}); diff --git a/packages/signals/tests/verdict-seat-merged-lane-4427.test.ts b/packages/signals/tests/verdict-seat-merged-lane-4427.test.ts index b0e1b8469f..d2488767a4 100644 --- a/packages/signals/tests/verdict-seat-merged-lane-4427.test.ts +++ b/packages/signals/tests/verdict-seat-merged-lane-4427.test.ts @@ -31,69 +31,79 @@ const tick = async () => { flush(); }; -it("a verdict reader seated in a hold reads a merged transaction's verdict lane as that hold's", async () => { - const [x, setX] = createSignal(0); - const [y, setY] = createSignal(0); - let a1!: () => void, a2!: () => void, b1!: () => void, b2!: () => void; - const actA = action(function* () { - setX(1); - yield new Promise(r => (a1 = r)); - setX(2); - yield new Promise(r => (a2 = r)); - }); - const actB = action(function* () { - setY(1); - yield new Promise(r => (b1 = r)); - setY(2); - yield new Promise(r => (b2 = r)); - }); - let shownM: unknown, leaf: unknown; - createRoot(() => { - const m = createMemo(() => latest(x) * 10); - const k = createMemo(() => x() + y()); - createRenderEffect(x, () => {}); - createRenderEffect(y, () => {}); - createRenderEffect(m, v => { - shownM = v; +// `mount`: `held` — ` 0}>`, the branch B's held pass +// built: the leaf is the hold's own work, born in its world (D-25, D-27), +// so `latest(y)` does not seat it and its runs wait with the frame for B's +// landing, inline as through a memo (held-frame-verdict-parity.test.ts). +// `latest` — ` 0}>`, a verdict reader's mount +// (mainline, D-27): the leaf is the verdict lanes' work and shows `m` with +// `m`. (Neither shape reaches the D-28 seat comparison any more: a held +// frame's leaf is never seated, and the mainline leaf's lane read enters +// A's lane either way.) +for (const mount of ["latest", "held"] as const) + it(`a verdict reader seated in a hold reads a merged transaction's verdict lane as that hold's (${mount} mount)`, async () => { + const [x, setX] = createSignal(0); + const [y, setY] = createSignal(0); + let a1!: () => void, a2!: () => void, b1!: () => void, b2!: () => void; + const actA = action(function* () { + setX(1); + yield new Promise(r => (a1 = r)); + setX(2); + yield new Promise(r => (a2 = r)); }); - createRenderEffect(k, () => {}); - // 0}>{[latest(y), m()]} - const when = createMemo(() => y() > 0); - const content = createMemo(() => { - if (!when()) return "off"; - createRenderEffect( - () => `${latest(y)}/${m()}`, - v => { - leaf = v; - } - ); - return "on"; + const actB = action(function* () { + setY(1); + yield new Promise(r => (b1 = r)); + setY(2); + yield new Promise(r => (b2 = r)); }); - createRenderEffect(content, () => {}); - }); - flush(); - actA(); - await tick(); - expect(shownM).toBe(10); - actB(); - await tick(); - expect(leaf).toBe("1/10"); + let shownM: unknown, leaf: unknown; + createRoot(() => { + const m = createMemo(() => latest(x) * 10); + const k = createMemo(() => x() + y()); + createRenderEffect(x, () => {}); + createRenderEffect(y, () => {}); + createRenderEffect(m, v => { + shownM = v; + }); + createRenderEffect(k, () => {}); + // 0 | y() > 0}>{[latest(y), m()]} + const when = createMemo(() => (mount === "latest" ? latest(y) : y()) > 0); + const content = createMemo(() => { + if (!when()) return "off"; + createRenderEffect( + () => `${latest(y)}/${m()}`, + v => { + leaf = v; + } + ); + return "on"; + }); + createRenderEffect(content, () => {}); + }); + flush(); + actA(); + await tick(); + expect(shownM).toBe(10); + actB(); + await tick(); + expect(leaf).toBe("1/10"); - // A's next step re-derives `m` in its (merged) verdict lane: the leaf - // shows it with `m`, not at B's landing. - a1(); - await tick(); - await tick(); - expect(shownM).toBe(20); - expect(leaf).toBe("1/20"); + // A's next step re-derives `m` in its (merged) verdict lane: the seated + // leaf shows it with `m`, not at B's landing; the held frame's waits. + a1(); + await tick(); + await tick(); + expect(shownM).toBe(20); + expect(leaf).toBe(mount === "latest" ? "1/20" : "1/10"); - b1(); - await tick(); - await tick(); - expect(leaf).toBe("2/20"); - b2(); - a2(); - await tick(); - await tick(); - expect([shownM, leaf]).toEqual([20, "2/20"]); -}); + b1(); + await tick(); + await tick(); + expect(leaf).toBe(mount === "latest" ? "2/20" : "1/10"); + b2(); + a2(); + await tick(); + await tick(); + expect([shownM, leaf]).toEqual([20, "2/20"]); + });