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:
39
pkgs/clan-app/ui/package-lock.json
generated
39
pkgs/clan-app/ui/package-lock.json
generated
@@ -25,12 +25,14 @@
|
|||||||
"@babel/plugin-syntax-import-attributes": "^7.27.1",
|
"@babel/plugin-syntax-import-attributes": "^7.27.1",
|
||||||
"@eslint/js": "^9.3.0",
|
"@eslint/js": "^9.3.0",
|
||||||
"@kachurun/storybook-solid-vite": "^9.0.11",
|
"@kachurun/storybook-solid-vite": "^9.0.11",
|
||||||
|
"@sinonjs/fake-timers": "^14.0.0",
|
||||||
"@storybook/addon-a11y": "^9.0.8",
|
"@storybook/addon-a11y": "^9.0.8",
|
||||||
"@storybook/addon-docs": "^9.0.8",
|
"@storybook/addon-docs": "^9.0.8",
|
||||||
"@storybook/addon-links": "^9.0.8",
|
"@storybook/addon-links": "^9.0.8",
|
||||||
"@storybook/addon-viewport": "^9.0.8",
|
"@storybook/addon-viewport": "^9.0.8",
|
||||||
"@storybook/addon-vitest": "^9.0.8",
|
"@storybook/addon-vitest": "^9.0.8",
|
||||||
"@types/node": "^22.15.19",
|
"@types/node": "^22.15.19",
|
||||||
|
"@types/sinonjs__fake-timers": "^8.1.5",
|
||||||
"@types/three": "^0.176.0",
|
"@types/three": "^0.176.0",
|
||||||
"@typescript-eslint/parser": "^8.32.1",
|
"@typescript-eslint/parser": "^8.32.1",
|
||||||
"@vitest/browser": "^3.2.3",
|
"@vitest/browser": "^3.2.3",
|
||||||
@@ -1776,6 +1778,26 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause"
|
"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": {
|
"node_modules/@solid-devtools/debugger": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@solid-devtools/debugger/-/debugger-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@solid-devtools/debugger/-/debugger-0.28.0.tgz",
|
||||||
@@ -2503,6 +2525,13 @@
|
|||||||
"csstype": "^3.0.2"
|
"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": {
|
"node_modules/@types/stats.js": {
|
||||||
"version": "0.17.4",
|
"version": "0.17.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
||||||
@@ -7699,6 +7728,16 @@
|
|||||||
"node": ">= 0.8.0"
|
"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": {
|
"node_modules/typescript": {
|
||||||
"version": "5.8.3",
|
"version": "5.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
|
|||||||
@@ -25,12 +25,14 @@
|
|||||||
"@babel/plugin-syntax-import-attributes": "^7.27.1",
|
"@babel/plugin-syntax-import-attributes": "^7.27.1",
|
||||||
"@eslint/js": "^9.3.0",
|
"@eslint/js": "^9.3.0",
|
||||||
"@kachurun/storybook-solid-vite": "^9.0.11",
|
"@kachurun/storybook-solid-vite": "^9.0.11",
|
||||||
|
"@sinonjs/fake-timers": "^14.0.0",
|
||||||
"@storybook/addon-a11y": "^9.0.8",
|
"@storybook/addon-a11y": "^9.0.8",
|
||||||
"@storybook/addon-docs": "^9.0.8",
|
"@storybook/addon-docs": "^9.0.8",
|
||||||
"@storybook/addon-links": "^9.0.8",
|
"@storybook/addon-links": "^9.0.8",
|
||||||
"@storybook/addon-viewport": "^9.0.8",
|
"@storybook/addon-viewport": "^9.0.8",
|
||||||
"@storybook/addon-vitest": "^9.0.8",
|
"@storybook/addon-vitest": "^9.0.8",
|
||||||
"@types/node": "^22.15.19",
|
"@types/node": "^22.15.19",
|
||||||
|
"@types/sinonjs__fake-timers": "^8.1.5",
|
||||||
"@types/three": "^0.176.0",
|
"@types/three": "^0.176.0",
|
||||||
"@typescript-eslint/parser": "^8.32.1",
|
"@typescript-eslint/parser": "^8.32.1",
|
||||||
"@vitest/browser": "^3.2.3",
|
"@vitest/browser": "^3.2.3",
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { Button, ButtonProps } from "./Button";
|
|||||||
import { Component } from "solid-js";
|
import { Component } from "solid-js";
|
||||||
import { expect, fn, waitFor } from "storybook/test";
|
import { expect, fn, waitFor } from "storybook/test";
|
||||||
import { StoryContext } from "@kachurun/storybook-solid-vite";
|
import { StoryContext } from "@kachurun/storybook-solid-vite";
|
||||||
|
import { StorybookClock } from "@/tests/clock";
|
||||||
|
|
||||||
|
const clock = StorybookClock();
|
||||||
|
|
||||||
const getCursorStyle = (el: Element) => window.getComputedStyle(el).cursor;
|
const getCursorStyle = (el: Element) => window.getComputedStyle(el).cursor;
|
||||||
|
|
||||||
@@ -149,7 +152,7 @@ export const Primary: Story = {
|
|||||||
hierarchy: "primary",
|
hierarchy: "primary",
|
||||||
onAction: fn(async () => {
|
onAction: fn(async () => {
|
||||||
// wait 500 ms to simulate an action
|
// 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
|
// randomly fail to check that the loading state still returns to normal
|
||||||
if (Math.random() > 0.5) {
|
if (Math.random() > 0.5) {
|
||||||
throw new Error("Action failure");
|
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");
|
const buttons = await canvas.findAllByRole("button");
|
||||||
|
|
||||||
for (const button of buttons) {
|
for (const button of buttons) {
|
||||||
@@ -192,6 +201,9 @@ export const Primary: Story = {
|
|||||||
// click the button
|
// click the button
|
||||||
await userEvent.click(button);
|
await userEvent.click(button);
|
||||||
|
|
||||||
|
// advance the clock
|
||||||
|
clock.tick(1);
|
||||||
|
|
||||||
// check the button has changed
|
// check the button has changed
|
||||||
await waitFor(async () => {
|
await waitFor(async () => {
|
||||||
// the action handler should have been called
|
// the action handler should have been called
|
||||||
@@ -204,6 +216,9 @@ export const Primary: Story = {
|
|||||||
await expect(getCursorStyle(button)).toEqual("wait");
|
await expect(getCursorStyle(button)).toEqual("wait");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// advance the clock
|
||||||
|
clock.tick(2000);
|
||||||
|
|
||||||
// wait for the action handler to finish
|
// wait for the action handler to finish
|
||||||
await waitFor(
|
await waitFor(
|
||||||
async () => {
|
async () => {
|
||||||
@@ -214,7 +229,7 @@ export const Primary: Story = {
|
|||||||
// the pointer should be normal
|
// the pointer should be normal
|
||||||
await expect(getCursorStyle(button)).toEqual("pointer");
|
await expect(getCursorStyle(button)).toEqual("pointer");
|
||||||
},
|
},
|
||||||
{ timeout: 1500 },
|
{ timeout: 2500 },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
56
pkgs/clan-app/ui/tests/clock.ts
Normal file
56
pkgs/clan-app/ui/tests/clock.ts
Normal 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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user