feat(ui): use fake timer in tests and real timer in browser for storybook interaction tests

I believe the time-based tests are falsely failing when the CI machine is under high load. This also speeds up the tests in CI.

I'm not 100% happy with the approach, but this should resolve CI issues in the short term until I can improve things.
This commit is contained in:
Brian McGee
2025-06-30 15:08:01 +01:00
parent 847f8363f3
commit 96b5ca9de0
4 changed files with 115 additions and 3 deletions

View File

@@ -25,12 +25,14 @@
"@babel/plugin-syntax-import-attributes": "^7.27.1",
"@eslint/js": "^9.3.0",
"@kachurun/storybook-solid-vite": "^9.0.11",
"@sinonjs/fake-timers": "^14.0.0",
"@storybook/addon-a11y": "^9.0.8",
"@storybook/addon-docs": "^9.0.8",
"@storybook/addon-links": "^9.0.8",
"@storybook/addon-viewport": "^9.0.8",
"@storybook/addon-vitest": "^9.0.8",
"@types/node": "^22.15.19",
"@types/sinonjs__fake-timers": "^8.1.5",
"@types/three": "^0.176.0",
"@typescript-eslint/parser": "^8.32.1",
"@vitest/browser": "^3.2.3",
@@ -1776,6 +1778,26 @@
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@sinonjs/commons": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
"integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"type-detect": "4.0.8"
}
},
"node_modules/@sinonjs/fake-timers": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-14.0.0.tgz",
"integrity": "sha512-QfoXRaUTjMVVn/ZbnD4LS3TPtqOkOdKIYCKldIVPnuClcwRKat6LI2mRZ2s5qiBfO6Fy03An35dSls/2/FEc0Q==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^3.0.1"
}
},
"node_modules/@solid-devtools/debugger": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@solid-devtools/debugger/-/debugger-0.28.0.tgz",
@@ -2503,6 +2525,13 @@
"csstype": "^3.0.2"
}
},
"node_modules/@types/sinonjs__fake-timers": {
"version": "8.1.5",
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz",
"integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/stats.js": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
@@ -7699,6 +7728,16 @@
"node": ">= 0.8.0"
}
},
"node_modules/type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",

View File

@@ -25,12 +25,14 @@
"@babel/plugin-syntax-import-attributes": "^7.27.1",
"@eslint/js": "^9.3.0",
"@kachurun/storybook-solid-vite": "^9.0.11",
"@sinonjs/fake-timers": "^14.0.0",
"@storybook/addon-a11y": "^9.0.8",
"@storybook/addon-docs": "^9.0.8",
"@storybook/addon-links": "^9.0.8",
"@storybook/addon-viewport": "^9.0.8",
"@storybook/addon-vitest": "^9.0.8",
"@types/node": "^22.15.19",
"@types/sinonjs__fake-timers": "^8.1.5",
"@types/three": "^0.176.0",
"@typescript-eslint/parser": "^8.32.1",
"@vitest/browser": "^3.2.3",

View File

@@ -3,6 +3,9 @@ import { Button, ButtonProps } from "./Button";
import { Component } from "solid-js";
import { expect, fn, waitFor } from "storybook/test";
import { StoryContext } from "@kachurun/storybook-solid-vite";
import { StorybookClock } from "@/tests/clock";
const clock = StorybookClock();
const getCursorStyle = (el: Element) => window.getComputedStyle(el).cursor;
@@ -149,7 +152,7 @@ export const Primary: Story = {
hierarchy: "primary",
onAction: fn(async () => {
// wait 500 ms to simulate an action
await new Promise((resolve) => setTimeout(resolve, 1000));
await new Promise((resolve) => clock.setTimeout(resolve, 2000));
// randomly fail to check that the loading state still returns to normal
if (Math.random() > 0.5) {
throw new Error("Action failure");
@@ -163,7 +166,13 @@ export const Primary: Story = {
},
},
play: async ({ canvas, step, userEvent, args }: StoryContext) => {
play: async ({
canvas,
canvasElement,
step,
userEvent,
args,
}: StoryContext) => {
const buttons = await canvas.findAllByRole("button");
for (const button of buttons) {
@@ -192,6 +201,9 @@ export const Primary: Story = {
// click the button
await userEvent.click(button);
// advance the clock
clock.tick(1);
// check the button has changed
await waitFor(async () => {
// the action handler should have been called
@@ -204,6 +216,9 @@ export const Primary: Story = {
await expect(getCursorStyle(button)).toEqual("wait");
});
// advance the clock
clock.tick(2000);
// wait for the action handler to finish
await waitFor(
async () => {
@@ -214,7 +229,7 @@ export const Primary: Story = {
// the pointer should be normal
await expect(getCursorStyle(button)).toEqual("pointer");
},
{ timeout: 1500 },
{ timeout: 2500 },
);
});
}

View File

@@ -0,0 +1,56 @@
import { query } from "@solidjs/router";
import set = query.set;
import FakeTimers, { Clock } from "@sinonjs/fake-timers";
export interface StorybookClock {
tick: (ms: number) => void;
setTimeout: (
callback: (...args: any[]) => void,
delay: number,
...args: any[]
) => void;
}
class BrowserClock implements StorybookClock {
setTimeout(
callback: (...args: any[]) => void,
delay: number,
args: any,
): void {
// set a normal timeout
setTimeout(callback, delay, args);
}
tick(_: number): void {
// do nothing
}
}
class FakeClock implements StorybookClock {
private clock: Clock;
constructor() {
this.clock = FakeTimers.createClock();
}
setTimeout(
callback: (...args: any[]) => void,
delay: number,
args: any,
): void {
this.clock.setTimeout(callback, delay, args);
}
tick(ms: number): void {
this.clock.tick(ms);
}
}
export function StorybookClock(): StorybookClock {
// Check if we're in a browser environment
const isBrowser = process.env.NODE_ENV !== "test";
console.log("is browser", isBrowser);
return isBrowser ? new BrowserClock() : new FakeClock();
}