From 17f7bf9c5b308ac04e9a438a7129d004c994dce9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 02:43:02 +0000 Subject: [PATCH 1/5] docs(rest): Click the Optimistic Transforms race 20ms apart Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MwXaUUnHx6HuRPR1t1D7US --- docs/rest/shared/_optimisticTransform.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/rest/shared/_optimisticTransform.mdx b/docs/rest/shared/_optimisticTransform.mdx index 0d0dfa930a50..79ce7dee758b 100644 --- a/docs/rest/shared/_optimisticTransform.mdx +++ b/docs/rest/shared/_optimisticTransform.mdx @@ -4,9 +4,9 @@ import {RestEndpoint} from '@data-client/rest'; export const raceDemo = [ { delay: 800, store: 'open' }, { delay: 1000, click: 'button.upvote' }, - { delay: 75, click: 'button.upvote' }, - { delay: 75, click: 'button.upvote' }, - { delay: 75, click: 'button.upvote' }, + { delay: 20, click: 'button.upvote' }, + { delay: 20, click: 'button.upvote' }, + { delay: 20, click: 'button.upvote' }, // past the slowest increment response (the fixture's 5s), so the // timeline holds every response { wait: 5500 }, From 48bb8fac80931b2bb99fdd6c645b71aeed90f198 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 02:47:01 +0000 Subject: [PATCH 2/5] docs(website): Skip the autoplay pointer's glide when it hasn't moved A repeated click on the same button waited out the pointer's ~400ms glide each time, so the race demo's 20ms clicks landed ~420ms apart. The stage now says whether the pointer moved, and the player waits only when it did. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MwXaUUnHx6HuRPR1t1D7US --- .../preview/__tests__/autorun.test.ts | 31 ++++++++++++++++++- .../Playground/preview/autorun/player.ts | 13 +++----- .../Playground/preview/autorun/stage.ts | 7 +++-- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/website/src/components/Playground/preview/__tests__/autorun.test.ts b/website/src/components/Playground/preview/__tests__/autorun.test.ts index 54a51512f806..560e8ba79b24 100644 --- a/website/src/components/Playground/preview/__tests__/autorun.test.ts +++ b/website/src/components/Playground/preview/__tests__/autorun.test.ts @@ -22,7 +22,10 @@ function fakeStage( const calls: string[] = []; const stage: Stage = { find: target => (target === STORE_TOGGLE ? toggle : found), - point: el => void calls.push(el === toggle ? 'point toggle' : 'point'), + point: el => { + calls.push(el === toggle ? 'point toggle' : 'point'); + return true; + }, click: () => void calls.push('click'), append: (_, text) => void calls.push(`append ${text}`), store: (open, { pressed, view } = {}) => @@ -59,6 +62,19 @@ it('plays steps in order, at their times', async () => { expect(player.getState()).toBe('done'); }); +it("clicks again without a glide when the pointer hasn't moved", async () => { + const player = createPlayer(); + const { stage, calls } = fakeStage(); + stage.point = () => { + calls.push('point'); + return false; + }; + const done = player.play([{ delay: 20, click: 'button' }], stage); + await jest.advanceTimersByTimeAsync(20); + await done; + expect(calls).toEqual(['point', 'click', 'end']); +}); + it('pausing keeps the time left', async () => { const player = createPlayer(); const { stage, calls } = fakeStage(); @@ -211,6 +227,19 @@ describe('stage', () => { } }); + it('says whether the pointer moved', () => { + const root = playground(); + const stage = createStage(root, () => {}); + const [one, two] = root.querySelectorAll('button'); + one.getBoundingClientRect = () => + ({ left: 0, top: 0, width: 20, height: 20 }) as DOMRect; + two.getBoundingClientRect = () => + ({ left: 40, top: 0, width: 20, height: 20 }) as DOMRect; + expect(stage.point(one)).toBe(true); + expect(stage.point(one)).toBe(false); + expect(stage.point(two)).toBe(true); + }); + it('the pointer follows its target when the playground resizes', () => { let resized: () => void = () => {}; (globalThis as any).ResizeObserver = class { diff --git a/website/src/components/Playground/preview/autorun/player.ts b/website/src/components/Playground/preview/autorun/player.ts index 6e43fde27479..3e9e2c00247a 100644 --- a/website/src/components/Playground/preview/autorun/player.ts +++ b/website/src/components/Playground/preview/autorun/player.ts @@ -5,8 +5,9 @@ import type { StoreView } from '../store/storeView'; /** What a script acts on: the playground's DOM (./stage.ts) */ export interface Stage { find(target: Target): HTMLElement | null; - /** Brings `el` into view and moves the pointer onto it */ - point(el: HTMLElement): void; + /** Brings `el` into view and moves the pointer onto it; false when it + * was there already (a repeated click), so there's no glide to wait out */ + point(el: HTMLElement): boolean; click(el: HTMLElement): void; append(el: HTMLElement, text: string): void; /** Opens or closes the Store, in `view` if given; `pressed`: the pointer @@ -95,10 +96,7 @@ async function perform( // onto the Store's toggle first, as a reader would; where the layout // shows one const toggle = stage.find(STORE_TOGGLE); - if (toggle) { - stage.point(toggle); - await sleep(MOVE_MS); - } + if (toggle && stage.point(toggle)) await sleep(MOVE_MS); return stage.store(step.store === 'open', { pressed: !!toggle, view: step.view, @@ -115,8 +113,7 @@ async function perform( throw new Error(`nothing matches ${JSON.stringify(target)}`); }; const target = 'click' in step ? step.click : step.type; - stage.point(await find(target)); - await sleep(MOVE_MS); + if (stage.point(await find(target))) await sleep(MOVE_MS); if ('click' in step) return stage.click(await find(target)); for (const char of step.text) { stage.append(await find(target), char); diff --git a/website/src/components/Playground/preview/autorun/stage.ts b/website/src/components/Playground/preview/autorun/stage.ts index c831cfc53b61..c2469d0c26a0 100644 --- a/website/src/components/Playground/preview/autorun/stage.ts +++ b/website/src/components/Playground/preview/autorun/stage.ts @@ -19,10 +19,12 @@ export function createStage(root: HTMLElement, showStore: ShowStore): Stage { let at = { x: 0, y: 0 }; let pointedAt: HTMLElement | undefined; - /** Moves the pointer onto `el`, the tip at its center */ + /** Moves the pointer onto `el`, the tip at its center; false when it was + * there already */ const place = (el: HTMLElement) => { const box = el.getBoundingClientRect(); const origin = layer.getBoundingClientRect(); + const was = at; at = { x: box.left + box.width / 2, y: box.top + box.height / 2, @@ -40,6 +42,7 @@ export function createStage(root: HTMLElement, showStore: ShowStore): Stage { void pointer.offsetWidth; pointer.dataset.shown = ''; } + return first || at.x !== was.x || at.y !== was.y; }; /** A press: a ring spreads from the tip (none without Web Animations, * e.g. jsdom) */ @@ -87,7 +90,7 @@ export function createStage(root: HTMLElement, showStore: ShowStore): Stage { revealIn(root, el); revealOnPage(el); pointedAt = el; - place(el); + return place(el); }, click(el) { From f09dca6939637f1828aac6947c33b442ced167a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 02:50:49 +0000 Subject: [PATCH 3/5] docs(website): Judge the autoplay pointer's move where it's drawn Compare its position in the layer, which scrolls with the page, so a scroll under a sticky target still glides it back before the click. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MwXaUUnHx6HuRPR1t1D7US --- .../Playground/preview/__tests__/autorun.test.ts | 6 ++++++ website/src/components/Playground/preview/autorun/stage.ts | 7 ++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/website/src/components/Playground/preview/__tests__/autorun.test.ts b/website/src/components/Playground/preview/__tests__/autorun.test.ts index 560e8ba79b24..aa73b6d65ac0 100644 --- a/website/src/components/Playground/preview/__tests__/autorun.test.ts +++ b/website/src/components/Playground/preview/__tests__/autorun.test.ts @@ -238,6 +238,12 @@ describe('stage', () => { expect(stage.point(one)).toBe(true); expect(stage.point(one)).toBe(false); expect(stage.point(two)).toBe(true); + // the page scrolled under a target that stays put (a sticky Store): + // the layer moved, so the pointer must glide back onto it + const layer = root.lastElementChild as HTMLElement; + layer.getBoundingClientRect = () => + ({ left: 0, top: -50, width: 0, height: 0 }) as DOMRect; + expect(stage.point(two)).toBe(true); }); it('the pointer follows its target when the playground resizes', () => { diff --git a/website/src/components/Playground/preview/autorun/stage.ts b/website/src/components/Playground/preview/autorun/stage.ts index c2469d0c26a0..fcc409b3d3a8 100644 --- a/website/src/components/Playground/preview/autorun/stage.ts +++ b/website/src/components/Playground/preview/autorun/stage.ts @@ -20,11 +20,11 @@ export function createStage(root: HTMLElement, showStore: ShowStore): Stage { let pointedAt: HTMLElement | undefined; /** Moves the pointer onto `el`, the tip at its center; false when it was - * there already */ + * drawn there already (in the layer's coordinates, which scroll with the + * page) */ const place = (el: HTMLElement) => { const box = el.getBoundingClientRect(); const origin = layer.getBoundingClientRect(); - const was = at; at = { x: box.left + box.width / 2, y: box.top + box.height / 2, @@ -36,13 +36,14 @@ export function createStage(root: HTMLElement, showStore: ShowStore): Stage { pointer.innerHTML = POINTER_SVG; layer.append(pointer); } + const previous = pointer.style.transform; pointer.style.transform = `translate(${at.x - origin.left}px, ${at.y - origin.top}px)`; // the first point fades in where it is, then glides from there if (first) { void pointer.offsetWidth; pointer.dataset.shown = ''; } - return first || at.x !== was.x || at.y !== was.y; + return first || pointer.style.transform !== previous; }; /** A press: a ring spreads from the tip (none without Web Animations, * e.g. jsdom) */ From 2ca7ee03a0064ad90757b08680f55fb5d7c97838 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 02:53:03 +0000 Subject: [PATCH 4/5] docs(rest): Race 5 clicks 100ms apart Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MwXaUUnHx6HuRPR1t1D7US --- docs/rest/shared/_optimisticTransform.mdx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/rest/shared/_optimisticTransform.mdx b/docs/rest/shared/_optimisticTransform.mdx index 79ce7dee758b..823cbed06aea 100644 --- a/docs/rest/shared/_optimisticTransform.mdx +++ b/docs/rest/shared/_optimisticTransform.mdx @@ -4,15 +4,16 @@ import {RestEndpoint} from '@data-client/rest'; export const raceDemo = [ { delay: 800, store: 'open' }, { delay: 1000, click: 'button.upvote' }, - { delay: 20, click: 'button.upvote' }, - { delay: 20, click: 'button.upvote' }, - { delay: 20, click: 'button.upvote' }, + ...Array.from({ length: 4 }, () => ({ + delay: 100, + click: 'button.upvote', + })), // past the slowest increment response (the fixture's 5s), so the // timeline holds every response { wait: 5500 }, { click: { in: 'store', selector: 'button[aria-label="Diff"]' } }, // the first action (the count's load), then on through each change, time - // to read each diff: 5 = the changes the 4 clicks make, as Diff's "Next + // to read each diff: 6 = the changes the 5 clicks make, as Diff's "Next // change" counts them (more would idle at the end before the close) { click: { @@ -20,7 +21,7 @@ export const raceDemo = [ selector: '[role="group"][aria-label^="Scrubber"] button[data-id]', }, }, - ...Array.from({ length: 5 }, () => ({ + ...Array.from({ length: 6 }, () => ({ delay: 1500, click: { in: 'store', selector: 'button[aria-label="Next change"]' }, })), From 08ac0e61b3ecc46d8caefb0731ab73970ad62430 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 02:55:55 +0000 Subject: [PATCH 5/5] docs(rest): Race clicks 120ms apart Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MwXaUUnHx6HuRPR1t1D7US --- docs/rest/shared/_optimisticTransform.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rest/shared/_optimisticTransform.mdx b/docs/rest/shared/_optimisticTransform.mdx index 823cbed06aea..3b6f5e44002e 100644 --- a/docs/rest/shared/_optimisticTransform.mdx +++ b/docs/rest/shared/_optimisticTransform.mdx @@ -5,7 +5,7 @@ export const raceDemo = [ { delay: 800, store: 'open' }, { delay: 1000, click: 'button.upvote' }, ...Array.from({ length: 4 }, () => ({ - delay: 100, + delay: 120, click: 'button.upvote', })), // past the slowest increment response (the fixture's 5s), so the