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",
|
||||
"@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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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