From 1ddedec359b6458bce30382ce2d4b14487da09aa Mon Sep 17 00:00:00 2001 From: Cristian Necula Date: Fri, 6 Feb 2026 13:24:06 +0200 Subject: [PATCH 1/8] test: migrate from Web Test Runner to Vitest - Replace WTR with Vitest for test infrastructure - Add dual project setup: unit tests (jsdom) and storybook tests (browser) - Migrate unit tests (fetch, path) to Vitest syntax with vi.* mocks - Convert UI tests (render, item-click, use-pref) to Storybook interaction tests - Add @storybook/addon-vitest integration for browser-based testing - Update package.json scripts: test, test:unit, test:storybook, test:watch NEO-927 --- .storybook/main.js | 2 +- .storybook/vitest.setup.ts | 7 + package-lock.json | 1355 ++++++++++++++--- package.json | 21 +- .../test/__snapshots__/render.test.snap.js | 68 - src/queue/test/item-click.test.ts | 34 - src/queue/test/render.test.ts | 32 - src/queue/test/use-pref.test.ts | 20 - src/util/{test => }/path.test.ts | 55 +- stories/item-click.stories.ts | 103 ++ stories/render.stories.ts | 119 ++ stories/use-pref.stories.ts | 95 ++ test/fetch.test.ts | 53 +- tsconfig.json | 30 +- vitest.config.ts | 29 + web-test-runner.config.mjs | 15 - 16 files changed, 1578 insertions(+), 460 deletions(-) create mode 100644 .storybook/vitest.setup.ts delete mode 100644 src/queue/test/__snapshots__/render.test.snap.js delete mode 100644 src/queue/test/item-click.test.ts delete mode 100644 src/queue/test/render.test.ts delete mode 100644 src/queue/test/use-pref.test.ts rename src/util/{test => }/path.test.ts (60%) create mode 100644 stories/item-click.stories.ts create mode 100644 stories/render.stories.ts create mode 100644 stories/use-pref.stories.ts create mode 100644 vitest.config.ts delete mode 100644 web-test-runner.config.mjs diff --git a/.storybook/main.js b/.storybook/main.js index c2e1d6a..424ac08 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -1,5 +1,5 @@ export default { stories: ['../stories/**/*.stories.@(js|jsx|ts|tsx|mdx)'], - addons: ['@storybook/addon-links'], + addons: ['@storybook/addon-links', '@storybook/addon-vitest'], framework: '@storybook/web-components-vite', }; diff --git a/.storybook/vitest.setup.ts b/.storybook/vitest.setup.ts new file mode 100644 index 0000000..a58536e --- /dev/null +++ b/.storybook/vitest.setup.ts @@ -0,0 +1,7 @@ +import { setProjectAnnotations } from '@storybook/web-components'; +import { beforeAll } from 'vitest'; +import * as previewAnnotations from './preview'; + +const annotations = setProjectAnnotations([previewAnnotations]); + +beforeAll(annotations.beforeAll); diff --git a/package-lock.json b/package-lock.json index 47c5d18..11a418c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,25 +29,26 @@ "@eslint/eslintrc": "^2.0.0", "@neovici/cfg": "^2.8.0", "@neovici/testing": "^2.0.0", - "@open-wc/testing": "^4.0.0", - "@open-wc/testing-helpers": "^3.0.1", "@semantic-release/changelog": "^6.0.0", "@semantic-release/git": "^10.0.0", "@storybook/addon-links": "^10.0.0", + "@storybook/addon-vitest": "^10.0.0", "@storybook/web-components": "^10.0.0", "@storybook/web-components-vite": "^10.0.0", - "@types/mocha": "^10.0.6", "@types/node": "^22.10.2", "@types/split.js": "^1.6.0", - "@web/dev-server-esbuild": "^1.0.4", - "@web/test-runner-playwright": "^0.11.1", + "@vitest/browser": "^4.0.0", + "@vitest/browser-playwright": "^4.0.18", "esbuild": "^0.27.0", "http-server": "^14.1.1", "husky": "^9.0.11", + "jsdom": "^26.0.0", + "playwright": "^1.52.0", "semantic-release": "^25.0.0", - "sinon": "^19.0.0", + "shadow-dom-testing-library": "^1.11.0", "storybook": "^10.0.0", - "typescript": "^5.4.3" + "typescript": "^5.4.3", + "vitest": "^4.0.0" } }, "node_modules/@actions/core": { @@ -106,6 +107,27 @@ "dev": true, "license": "MIT" }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", @@ -408,6 +430,123 @@ "node": ">=v18" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@emnapi/core": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", @@ -1940,6 +2079,13 @@ "node": ">=12" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, "node_modules/@polymer/iron-flex-layout": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@polymer/iron-flex-layout/-/iron-flex-layout-3.0.1.tgz", @@ -3135,53 +3281,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, - "node_modules/@sinonjs/samsam": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", - "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "type-detect": "^4.1.0" - } - }, - "node_modules/@sinonjs/samsam/node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@sinonjs/text-encoding": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", - "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, - "license": "(Unlicense OR Apache-2.0)" + "license": "MIT" }, "node_modules/@storybook/addon-links": { "version": "10.2.1", @@ -3206,6 +3311,42 @@ } } }, + "node_modules/@storybook/addon-vitest": { + "version": "10.2.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-vitest/-/addon-vitest-10.2.7.tgz", + "integrity": "sha512-pVFEZRSi7APkVcAdwuTL1crXa65fJ1ME/uVXajRR832UssIk8YwOGle3P7ciqPEL/8KSsn+GbDGQXLsKN2/oFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@storybook/icons": "^2.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "@vitest/browser": "^3.0.0 || ^4.0.0", + "@vitest/browser-playwright": "^4.0.0", + "@vitest/runner": "^3.0.0 || ^4.0.0", + "storybook": "^10.2.7", + "vitest": "^3.0.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/runner": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, "node_modules/@storybook/builder-vite": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.2.1.tgz", @@ -3648,13 +3789,6 @@ "@types/koa": "*" } }, - "node_modules/@types/mocha": { - "version": "10.0.10", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", - "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { "version": "22.19.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", @@ -4314,120 +4448,375 @@ "win32" ] }, - "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "node_modules/@vitest/browser": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.18.tgz", + "integrity": "sha512-gVQqh7paBz3gC+ZdcCmNSWJMk70IUjDeVqi+5m5vYpEHsIwRgw3Y545jljtajhkekIpIp5Gg8oK7bctgY0E2Ng==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "@vitest/mocker": "4.0.18", + "@vitest/utils": "4.0.18", + "magic-string": "^0.30.21", + "pixelmatch": "7.1.0", + "pngjs": "^7.0.0", + "sirv": "^3.0.2", + "tinyrainbow": "^3.0.3", + "ws": "^8.18.3" }, "funding": { "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.18" } }, - "node_modules/@vitest/expect/node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "node_modules/@vitest/browser-playwright": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.18.tgz", + "integrity": "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" + "@vitest/browser": "4.0.18", + "@vitest/mocker": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": false + } } }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "node_modules/@vitest/browser-playwright/node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", "dev": true, "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "node_modules/@vitest/browser/node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^4.0.3" + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "node_modules/@vitest/browser/node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@web/browser-logs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-0.4.1.tgz", - "integrity": "sha512-ypmMG+72ERm+LvP+loj9A64MTXvWMXHUOu773cPO4L1SV/VWg6xA9Pv7vkvkXQX+ItJtCJt+KQ+U6ui2HhSFUw==", + "node_modules/@vitest/browser/node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", "dev": true, "license": "MIT", - "dependencies": { - "errorstacks": "^2.4.1" - }, "engines": { - "node": ">=18.0.0" + "node": ">=14.0.0" } }, - "node_modules/@web/config-loader": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@web/config-loader/-/config-loader-0.3.3.tgz", - "integrity": "sha512-ilzeQzrPpPLWZhzFCV+4doxKDGm7oKVfdKpW9wiUNVgive34NSzCw+WzXTvjE4Jgr5CkyTDIObEmMrqQEjhT0g==", + "node_modules/@vitest/browser/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "dev": true, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, - "node_modules/@web/dev-server": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/@web/dev-server/-/dev-server-0.4.6.tgz", - "integrity": "sha512-jj/1bcElAy5EZet8m2CcUdzxT+CRvUjIXGh8Lt7vxtthkN9PzY9wlhWx/9WOs5iwlnG1oj0VGo6f/zvbPO0s9w==", + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.12.11", - "@types/command-line-args": "^5.0.0", - "@web/config-loader": "^0.3.0", - "@web/dev-server-core": "^0.7.2", - "@web/dev-server-rollup": "^0.6.1", - "camelcase": "^6.2.0", - "command-line-args": "^5.1.1", - "command-line-usage": "^7.0.1", - "debounce": "^1.2.0", - "deepmerge": "^4.2.2", - "internal-ip": "^6.2.0", - "nanocolors": "^0.2.1", - "open": "^8.0.2", - "portfinder": "^1.0.32" - }, + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/expect/node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@web/browser-logs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-0.4.1.tgz", + "integrity": "sha512-ypmMG+72ERm+LvP+loj9A64MTXvWMXHUOu773cPO4L1SV/VWg6xA9Pv7vkvkXQX+ItJtCJt+KQ+U6ui2HhSFUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "errorstacks": "^2.4.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/config-loader": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@web/config-loader/-/config-loader-0.3.3.tgz", + "integrity": "sha512-ilzeQzrPpPLWZhzFCV+4doxKDGm7oKVfdKpW9wiUNVgive34NSzCw+WzXTvjE4Jgr5CkyTDIObEmMrqQEjhT0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/dev-server": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@web/dev-server/-/dev-server-0.4.6.tgz", + "integrity": "sha512-jj/1bcElAy5EZet8m2CcUdzxT+CRvUjIXGh8Lt7vxtthkN9PzY9wlhWx/9WOs5iwlnG1oj0VGo6f/zvbPO0s9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.11", + "@types/command-line-args": "^5.0.0", + "@web/config-loader": "^0.3.0", + "@web/dev-server-core": "^0.7.2", + "@web/dev-server-rollup": "^0.6.1", + "camelcase": "^6.2.0", + "command-line-args": "^5.1.1", + "command-line-usage": "^7.0.1", + "debounce": "^1.2.0", + "deepmerge": "^4.2.2", + "internal-ip": "^6.2.0", + "nanocolors": "^0.2.1", + "open": "^8.0.2", + "portfinder": "^1.0.32" + }, "bin": { "wds": "dist/bin.js", "web-dev-server": "dist/bin.js" @@ -6596,6 +6985,20 @@ "dev": true, "license": "MIT" }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/dargs": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz", @@ -6619,6 +7022,20 @@ "node": ">= 14" } }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -6708,6 +7125,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -7480,6 +7904,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -8236,6 +8661,16 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -9926,6 +10361,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -10246,24 +10688,152 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "node_modules/jsdom/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/jsdom/node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jsdom/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/jsdom/node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true, "license": "MIT" }, @@ -10334,13 +10904,6 @@ "node": "*" } }, - "node_modules/just-extend": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", - "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", - "dev": true, - "license": "MIT" - }, "node_modules/keygrip": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", @@ -10748,6 +11311,16 @@ "lz-string": "bin/bin.js" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/make-asynchronous": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/make-asynchronous/-/make-asynchronous-1.0.1.tgz", @@ -11046,6 +11619,16 @@ "node": ">=10" } }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -11148,20 +11731,6 @@ "node": ">= 0.4.0" } }, - "node_modules/nise": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", - "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^13.0.1", - "@sinonjs/text-encoding": "^0.7.3", - "just-extend": "^6.2.0", - "path-to-regexp": "^8.1.0" - } - }, "node_modules/node-emoji": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", @@ -13322,6 +13891,13 @@ "inBundle": true, "license": "ISC" }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -13429,6 +14005,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -13824,17 +14411,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -13845,6 +14421,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pathval": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", @@ -13892,6 +14475,19 @@ "node": ">=4" } }, + "node_modules/pixelmatch": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz", + "integrity": "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==", + "dev": true, + "license": "ISC", + "dependencies": { + "pngjs": "^7.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, "node_modules/pkg-conf": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-2.1.0.tgz", @@ -13975,6 +14571,7 @@ "integrity": "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright-core": "1.58.0" }, @@ -14001,6 +14598,16 @@ "node": ">=18" } }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/portfinder": { "version": "1.0.38", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz", @@ -14879,6 +15486,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -14985,6 +15599,19 @@ "dev": true, "license": "MIT" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -15476,6 +16103,20 @@ "dev": true, "license": "ISC" }, + "node_modules/shadow-dom-testing-library": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/shadow-dom-testing-library/-/shadow-dom-testing-library-1.13.1.tgz", + "integrity": "sha512-X73VDI3KumYeYuz/C2yjn4/1bsUN68JleYI3NwFJUDa+5fWhO//s+02gdxdetY+mVRYZosVokkkmIhdzjABnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14", + "npm": ">= 7" + }, + "peerDependencies": { + "@testing-library/dom": ">= 8" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -15575,6 +16216,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -15688,33 +16336,19 @@ "node": ">=4" } }, - "node_modules/sinon": { - "version": "19.0.5", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.5.tgz", - "integrity": "sha512-r15s9/s+ub/d4bxNXqIUmwp6imVSdTorIRaxoecYjqTVLZ8RuoXr/4EDGwIBo6Waxn7f2gnURX9zuhAfCwaF6Q==", + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^13.0.5", - "@sinonjs/samsam": "^8.0.1", - "diff": "^7.0.0", - "nise": "^6.1.1", - "supports-color": "^7.2.0" + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/sinon" - } - }, - "node_modules/sinon/node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", - "dev": true, - "license": "BSD-3-Clause", "engines": { - "node": ">=0.3.1" + "node": ">=18" } }, "node_modules/skin-tone": { @@ -15888,6 +16522,13 @@ "node": ">=12.0.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -15898,6 +16539,13 @@ "node": ">= 0.6" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -15913,9 +16561,9 @@ } }, "node_modules/storybook": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.2.1.tgz", - "integrity": "sha512-hgiiwT4ZWJ/yrRpoXnHpCzWOsUvLUwQqgM/ws6mCIDsKJ7Gc7irL6DjWpi8G7l1Uq5VXYsQjXQo5ydb8Pyajdg==", + "version": "10.2.7", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.2.7.tgz", + "integrity": "sha512-LFKSuZyF6EW2/Kkl5d7CvqgwhXXfuWv+aLBuoc616boLKJ3mxXuea+GxIgfk02NEyTKctJ0QsnSh5pAomf6Qkg==", "dev": true, "license": "MIT", "peer": true, @@ -16240,6 +16888,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/table-layout": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz", @@ -16446,6 +17101,13 @@ "dev": true, "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", @@ -16525,6 +17187,26 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -16548,6 +17230,29 @@ "node": ">=0.6" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", @@ -16649,16 +17354,6 @@ "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/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -17116,6 +17811,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -17232,6 +17928,197 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/vitest/node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/web-worker": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz", @@ -17290,6 +18177,16 @@ "node": ">=0.10.0" } }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "14.2.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", @@ -17409,6 +18306,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -17512,6 +18426,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 2568c87..77d13dd 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,10 @@ "lint": "tsc && eslint --cache .", "build": "tsc -p tsconfig.build.json", "start": "wds", - "test": "wtr --coverage", - "test:watch": "wtr --watch", + "test": "vitest --run", + "test:unit": "vitest --project=unit --run", + "test:storybook": "vitest --project=storybook --run", + "test:watch": "vitest", "check:duplicates": "check-duplicate-components", "dev": "npm run storybook:start", "storybook:start": "storybook dev -p 8000", @@ -91,26 +93,25 @@ "@commitlint/config-conventional": "^20.0.0", "@eslint/eslintrc": "^2.0.0", "@neovici/cfg": "^2.8.0", - "@neovici/testing": "^2.0.0", - "@open-wc/testing": "^4.0.0", - "@open-wc/testing-helpers": "^3.0.1", "@semantic-release/changelog": "^6.0.0", "@semantic-release/git": "^10.0.0", "@storybook/addon-links": "^10.0.0", + "@storybook/addon-vitest": "^10.0.0", "@storybook/web-components": "^10.0.0", "@storybook/web-components-vite": "^10.0.0", - "@types/mocha": "^10.0.6", "@types/node": "^22.10.2", "@types/split.js": "^1.6.0", - "@web/dev-server-esbuild": "^1.0.4", - "@web/test-runner-playwright": "^0.11.1", + "@vitest/browser": "^4.0.0", + "@vitest/browser-playwright": "^4.0.0", "esbuild": "^0.27.0", "http-server": "^14.1.1", "husky": "^9.0.11", + "jsdom": "^26.0.0", + "playwright": "^1.52.0", "semantic-release": "^25.0.0", - "sinon": "^19.0.0", "storybook": "^10.0.0", - "typescript": "^5.4.3" + "typescript": "^5.4.3", + "vitest": "^4.0.0" }, "overrides": { "conventional-changelog-conventionalcommits": ">= 8.0.0", diff --git a/src/queue/test/__snapshots__/render.test.snap.js b/src/queue/test/__snapshots__/render.test.snap.js deleted file mode 100644 index af225cf..0000000 --- a/src/queue/test/__snapshots__/render.test.snap.js +++ /dev/null @@ -1,68 +0,0 @@ -/* @web/test-runner snapshot v1 */ -export const snapshots = {}; - -snapshots['queue > render renderNav'] = ` -`; -/* end snapshot queue > render renderNav */ - -snapshots['queue > render renderPagination'] = `
- - -
-`; -/* end snapshot queue > render renderPagination */ -snapshots['queue > render renderNav'] = ` -`; -/* end snapshot queue > render renderNav */ - -snapshots['queue > render renderPagination'] = `
- - -
-`; -/* end snapshot queue > render renderPagination */ -snapshots['queue > render renderNav'] = ` -`; -/* end snapshot queue > render renderNav */ - -snapshots['queue > render renderPagination'] = `
- - -
-`; -/* end snapshot queue > render renderPagination */ diff --git a/src/queue/test/item-click.test.ts b/src/queue/test/item-click.test.ts deleted file mode 100644 index 72d205f..0000000 --- a/src/queue/test/item-click.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { assert, oneEvent } from '@open-wc/testing'; -import { oneDefaultPreventedEvent } from '@open-wc/testing-helpers'; -import { itemClick } from '../item-click'; - -describe('item-click', () => { - it('fires event', async () => { - const el = document.createElement('div'); - el.addEventListener('click', itemClick({ index: 2, activate: 'queue' })); - setTimeout(() => el.click()); - const { detail } = await oneEvent(el, 'omnitable-item-click'); - assert.equal(detail.index, 2); - assert.equal(detail.activate, 'queue'); - }); - - it('prevents default', async () => { - const el = document.createElement('div'); - const ev = new MouseEvent('click'); - el.addEventListener('click', itemClick({ index: 3, activate: 'list' })); - setTimeout(() => el.dispatchEvent(ev)); - const { detail } = await oneDefaultPreventedEvent( - el, - 'omnitable-item-click', - ); - assert.equal(detail.index, 3); - assert.equal(detail.activate, 'list'); - }); - - it('does not fire event', async () => { - const el = document.createElement('div'); - const ev = new MouseEvent('click', { ctrlKey: true }); - el.addEventListener('click', itemClick({ index: 3, activate: 'list' })); - setTimeout(() => el.dispatchEvent(ev)); - }); -}); diff --git a/src/queue/test/render.test.ts b/src/queue/test/render.test.ts deleted file mode 100644 index 211664e..0000000 --- a/src/queue/test/render.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { expect, fixture } from '@open-wc/testing'; -import { nothing, TemplateResult } from 'lit-html'; -import { spy } from 'sinon'; -import { renderNav, renderPagination } from '../render'; - -describe('queue > render', () => { - it('renderNav', async () => { - const el = await fixture(renderNav({})); - await expect(el).dom.to.equalSnapshot(); - }); - - it('renderPagination nothing', async () => { - expect(renderPagination()).to.equal(nothing); - }); - - it('renderPagination', async () => { - const onPage = spy(); - const el = await fixture( - renderPagination({ - totalPages: 10, - pageNumber: 3, - onPage, - }) as TemplateResult, - ); - await expect(el).dom.to.equalSnapshot(); - el.querySelector('.page-next')?.click(); - expect(onPage).to.have.been.calledWith(4); - onPage.resetHistory(); - el.querySelector('.page-prev')?.click(); - expect(onPage).to.have.been.calledWith(2); - }); -}); diff --git a/src/queue/test/use-pref.test.ts b/src/queue/test/use-pref.test.ts deleted file mode 100644 index 12403bc..0000000 --- a/src/queue/test/use-pref.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { renderHook } from '@neovici/testing'; -import { assert } from '@open-wc/testing'; -import { usePref } from '../use-pref'; - -describe('use-pref', () => { - it('default pref', async () => { - const { result } = await renderHook(() => usePref('some', 'asdad')); - assert.equal(result.current[0], 'asdad'); - }); - - it('update pref', async () => { - const { result, nextUpdate } = await renderHook(() => - usePref('somethingelse'), - ); - assert.equal(result.current[0], undefined); - setTimeout(() => result.current[1]('dads')); - await nextUpdate(); - assert.equal(result.current[0], 'dads'); - }); -}); diff --git a/src/util/test/path.test.ts b/src/util/path.test.ts similarity index 60% rename from src/util/test/path.test.ts rename to src/util/path.test.ts index ad380d2..f5ba20d 100644 --- a/src/util/test/path.test.ts +++ b/src/util/path.test.ts @@ -1,104 +1,99 @@ -import { assert } from '@open-wc/testing'; -import { get, normalize, split } from '../path'; +import { describe, expect, it } from 'vitest'; +import { get, normalize, split } from './path'; describe('path', () => { describe('normalize', () => { it('returns string path as-is', () => { - assert.equal(normalize('foo.bar.0.baz'), 'foo.bar.0.baz'); + expect(normalize('foo.bar.0.baz')).toBe('foo.bar.0.baz'); }); it('returns empty string as-is', () => { - assert.equal(normalize(''), ''); + expect(normalize('')).toBe(''); }); it('converts array path to flattened string', () => { - assert.equal(normalize(['foo.bar', 0, 'baz']), 'foo.bar.0.baz'); + expect(normalize(['foo.bar', 0, 'baz'])).toBe('foo.bar.0.baz'); }); it('handles array with single element', () => { - assert.equal(normalize(['foo']), 'foo'); + expect(normalize(['foo'])).toBe('foo'); }); it('handles array with dotted strings and numbers', () => { - assert.equal(normalize(['a.b', 1, 'c.d', 2]), 'a.b.1.c.d.2'); + expect(normalize(['a.b', 1, 'c.d', 2])).toBe('a.b.1.c.d.2'); }); it('handles empty array', () => { - assert.equal(normalize([]), ''); + expect(normalize([])).toBe(''); }); }); describe('split', () => { it('splits string path into array', () => { - assert.deepEqual(split('foo.bar.0.baz'), ['foo', 'bar', '0', 'baz']); + expect(split('foo.bar.0.baz')).toEqual(['foo', 'bar', '0', 'baz']); }); it('splits array path into flat array', () => { - assert.deepEqual(split(['foo.bar', 0, 'baz']), [ - 'foo', - 'bar', - '0', - 'baz', - ]); + expect(split(['foo.bar', 0, 'baz'])).toEqual(['foo', 'bar', '0', 'baz']); }); it('handles single segment string', () => { - assert.deepEqual(split('foo'), ['foo']); + expect(split('foo')).toEqual(['foo']); }); it('handles array with single element', () => { - assert.deepEqual(split(['foo']), ['foo']); + expect(split(['foo'])).toEqual(['foo']); }); it('handles empty string', () => { - assert.deepEqual(split(''), ['']); + expect(split('')).toEqual(['']); }); }); describe('get', () => { it('retrieves nested value with string path', () => { const obj = { foo: { bar: { baz: 'value' } } }; - assert.equal(get(obj, 'foo.bar.baz'), 'value'); + expect(get(obj, 'foo.bar.baz')).toBe('value'); }); it('retrieves nested value with array path', () => { const obj = { foo: { bar: { baz: 'value' } } }; - assert.equal(get(obj, ['foo', 'bar', 'baz']), 'value'); + expect(get(obj, ['foo', 'bar', 'baz'])).toBe('value'); }); it('retrieves nested value with dotted string in array path', () => { const obj = { foo: { bar: { baz: 'value' } } }; - assert.equal(get(obj, ['foo.bar', 'baz']), 'value'); + expect(get(obj, ['foo.bar', 'baz'])).toBe('value'); }); it('retrieves array element by index', () => { const obj = { items: ['a', 'b', 'c'] }; - assert.equal(get(obj, 'items.1'), 'b'); + expect(get(obj, 'items.1')).toBe('b'); }); it('retrieves array element with array path', () => { const obj = { items: ['a', 'b', 'c'] }; - assert.equal(get(obj, ['items', 0]), 'a'); + expect(get(obj, ['items', 0])).toBe('a'); }); it('returns undefined for non-existent path', () => { const obj = { foo: { bar: 'value' } }; - assert.equal(get(obj, 'foo.baz.qux'), undefined); + expect(get(obj, 'foo.baz.qux')).toBeUndefined(); }); it('returns undefined when intermediate property is undefined', () => { const obj = { foo: undefined }; - assert.equal(get(obj, 'foo.bar'), undefined); + expect(get(obj, 'foo.bar')).toBeUndefined(); }); it('returns undefined when intermediate property is null', () => { const obj = { foo: null }; - assert.equal(get(obj, 'foo.bar'), undefined); + expect(get(obj, 'foo.bar')).toBeUndefined(); }); it('returns undefined with empty string path', () => { const obj = { foo: 'bar' }; - assert.equal(get(obj, ''), undefined); + expect(get(obj, '')).toBeUndefined(); }); it('handles complex nested structure', () => { @@ -108,15 +103,15 @@ describe('path', () => { { name: 'Bob', address: { city: 'LA' } }, ], }; - assert.equal(get(obj, 'users.1.address.city'), 'LA'); + expect(get(obj, 'users.1.address.city')).toBe('LA'); }); it('returns undefined for null root', () => { - assert.equal(get(null, 'foo'), undefined); + expect(get(null, 'foo')).toBeUndefined(); }); it('returns undefined for undefined root', () => { - assert.equal(get(undefined, 'foo'), undefined); + expect(get(undefined, 'foo')).toBeUndefined(); }); }); }); diff --git a/stories/item-click.stories.ts b/stories/item-click.stories.ts new file mode 100644 index 0000000..5001ac2 --- /dev/null +++ b/stories/item-click.stories.ts @@ -0,0 +1,103 @@ +import type { Meta, StoryObj } from '@storybook/web-components'; +import { html } from 'lit-html'; +import { expect, userEvent } from 'storybook/test'; +import { itemClick } from '../src/queue/item-click'; + +const meta: Meta = { + title: 'Tests/ItemClick', +}; + +export default meta; + +export const FiresEvent: StoryObj = { + render: () => html` + + `, + async play({ canvasElement }) { + const button = canvasElement.querySelector( + '#test-button', + ) as HTMLButtonElement; + + let eventDetail: { index: number; activate: string } | null = null; + button.addEventListener('omnitable-item-click', ((e: CustomEvent) => { + eventDetail = e.detail; + }) as EventListener); + + await userEvent.click(button); + + expect(eventDetail).not.toBeNull(); + expect(eventDetail!.index).toBe(2); + expect(eventDetail!.activate).toBe('queue'); + }, +}; + +export const PreventsDefault: StoryObj = { + render: () => html` + + `, + async play({ canvasElement }) { + const button = canvasElement.querySelector( + '#test-button', + ) as HTMLButtonElement; + + let eventDetail: { index: number; activate: string } | null = null; + let wasDefaultPrevented = false; + + button.addEventListener('omnitable-item-click', ((e: CustomEvent) => { + eventDetail = e.detail; + e.preventDefault(); + }) as EventListener); + + button.addEventListener('click', (e: MouseEvent) => { + wasDefaultPrevented = e.defaultPrevented; + }); + + await userEvent.click(button); + + expect(eventDetail).not.toBeNull(); + expect(eventDetail!.index).toBe(3); + expect(eventDetail!.activate).toBe('list'); + expect(wasDefaultPrevented).toBe(true); + }, +}; + +export const DoesNotFireWithCtrlKey: StoryObj = { + render: () => html` + + `, + async play({ canvasElement }) { + const button = canvasElement.querySelector( + '#test-button', + ) as HTMLButtonElement; + + let eventFired = false; + button.addEventListener('omnitable-item-click', () => { + eventFired = true; + }); + + // Simulate ctrl+click using native MouseEvent + const ctrlClickEvent = new MouseEvent('click', { + bubbles: true, + cancelable: true, + ctrlKey: true, + }); + button.dispatchEvent(ctrlClickEvent); + + expect(eventFired).toBe(false); + }, +}; diff --git a/stories/render.stories.ts b/stories/render.stories.ts new file mode 100644 index 0000000..595942e --- /dev/null +++ b/stories/render.stories.ts @@ -0,0 +1,119 @@ +import type { Meta, StoryObj } from '@storybook/web-components'; +import { html, nothing } from 'lit-html'; +import { expect, fn, userEvent, within } from 'storybook/test'; +import { renderNav, renderPagination } from '../src/queue/render'; + +const meta: Meta = { + title: 'Tests/Render', +}; + +export default meta; + +export const RenderNavTest: StoryObj = { + render: () => html`
${renderNav({})}
`, + async play({ canvasElement }) { + const canvas = within(canvasElement); + const container = canvas.getByTestId + ? canvasElement.querySelector('#test-container') + : canvasElement.querySelector('#test-container'); + + // Both buttons should be disabled when no callbacks are provided + const buttons = container?.querySelectorAll('button.button-nav'); + expect(buttons).toHaveLength(2); + + const prevButton = container?.querySelector('.button-nav.prev'); + const nextButton = container?.querySelector('.button-nav.next'); + expect(prevButton).toHaveAttribute('disabled'); + expect(nextButton).toHaveAttribute('disabled'); + }, +}; + +export const RenderNavWithCallbacks: StoryObj = { + render: () => { + const prev = fn(); + const next = fn(); + (window as unknown as Record).__testPrev = prev; + (window as unknown as Record).__testNext = next; + return html`
${renderNav({ prev, next })}
`; + }, + async play({ canvasElement }) { + const container = canvasElement.querySelector('#test-container'); + + const prevButton = container?.querySelector( + '.button-nav.prev', + ) as HTMLButtonElement; + const nextButton = container?.querySelector( + '.button-nav.next', + ) as HTMLButtonElement; + + // Buttons should not be disabled when callbacks are provided + expect(prevButton).not.toHaveAttribute('disabled'); + expect(nextButton).not.toHaveAttribute('disabled'); + + // Test clicking + await userEvent.click(prevButton); + expect( + (window as unknown as Record).__testPrev, + ).toHaveBeenCalled(); + + await userEvent.click(nextButton); + expect( + (window as unknown as Record).__testNext, + ).toHaveBeenCalled(); + }, +}; + +export const RenderPaginationNothing: StoryObj = { + render: () => + html`
+ ${renderPagination() === nothing ? 'nothing' : renderPagination()} +
`, + async play({ canvasElement }) { + const container = canvasElement.querySelector('#test-container'); + expect(container?.textContent?.trim()).toBe('nothing'); + }, +}; + +export const RenderPaginationTest: StoryObj = { + render: () => { + const onPage = fn(); + (window as unknown as Record).__testOnPage = onPage; + return html`
+ ${renderPagination({ + totalPages: 10, + pageNumber: 3, + onPage, + })} +
`; + }, + async play({ canvasElement }) { + const container = canvasElement.querySelector('#test-container'); + + const prevButton = container?.querySelector( + '.page-prev', + ) as HTMLButtonElement; + const nextButton = container?.querySelector( + '.page-next', + ) as HTMLButtonElement; + + expect(prevButton).toBeTruthy(); + expect(nextButton).toBeTruthy(); + + // Click next page + await userEvent.click(nextButton); + expect( + (window as unknown as Record).__testOnPage, + ).toHaveBeenCalledWith(4); + + // Reset and click prev page + ( + (window as unknown as Record).__testOnPage as ReturnType< + typeof fn + > + ).mockClear(); + await userEvent.click(prevButton); + expect( + (window as unknown as Record).__testOnPage, + ).toHaveBeenCalledWith(2); + }, +}; diff --git a/stories/use-pref.stories.ts b/stories/use-pref.stories.ts new file mode 100644 index 0000000..6e87d34 --- /dev/null +++ b/stories/use-pref.stories.ts @@ -0,0 +1,95 @@ +import { component } from '@pionjs/pion'; +import type { Meta, StoryObj } from '@storybook/web-components'; +import { html } from 'lit-html'; +import { expect } from 'storybook/test'; +import { usePref } from '../src/queue/use-pref'; + +const meta: Meta = { + title: 'Tests/UsePref', +}; + +export default meta; + +// Counter for unique component names +let counter = 0; + +// Store for hook results - keyed by component instance +const hookResults = new Map< + string, + [string | undefined, (v: string) => void] +>(); + +// Create unique component for each test run +const createPrefComponent = (key: string, defaultValue?: string) => { + const id = counter++; + const tagName = `test-pref-${id}`; + const prefKey = `${key}-${id}`; + const localStorageKey = `pref-${prefKey}`; + + // Clear localStorage BEFORE defining component + localStorage.removeItem(localStorageKey); + + const TestComponent = component(() => { + const result = usePref(prefKey, defaultValue!); + hookResults.set(tagName, result); + return html`
+ ${result[0] ?? 'undefined'} +
`; + }); + customElements.define(tagName, TestComponent); + + return tagName; +}; + +export const DefaultPref: StoryObj = { + render: () => { + const tagName = createPrefComponent('default-test', 'asdad'); + (window as Record).__defaultPrefTagName = tagName; + const el = document.createElement(tagName); + return el; + }, + async play({ canvasElement }) { + // Wait for component to render + await new Promise((r) => setTimeout(r, 100)); + + const tagName = (window as Record) + .__defaultPrefTagName as string; + + // Query inside shadow DOM + const hostEl = canvasElement.querySelector(tagName); + const shadowRoot = hostEl?.shadowRoot; + const el = shadowRoot?.querySelector('[data-value]'); + + expect(el?.getAttribute('data-value')).toBe('asdad'); + }, +}; + +export const UpdatePref: StoryObj = { + render: () => { + const tagName = createPrefComponent('update-test'); + (window as Record).__updatePrefTagName = tagName; + const el = document.createElement(tagName); + return el; + }, + async play() { + // Wait for component to render + await new Promise((r) => setTimeout(r, 100)); + + const tagName = (window as Record) + .__updatePrefTagName as string; + const result = hookResults.get(tagName); + expect(result).toBeDefined(); + + // Initial value should be undefined + expect(result![0]).toBeUndefined(); + + // Update the pref + result![1]('dads'); + + // Wait for re-render + await new Promise((r) => setTimeout(r, 100)); + + const newResult = hookResults.get(tagName); + expect(newResult![0]).toBe('dads'); + }, +}; diff --git a/test/fetch.test.ts b/test/fetch.test.ts index 5112121..db1892c 100644 --- a/test/fetch.test.ts +++ b/test/fetch.test.ts @@ -1,27 +1,34 @@ -import { expect } from '@open-wc/testing'; -import { stub, type SinonStub } from 'sinon'; +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, + type MockInstance, +} from 'vitest'; import { fetch, setBaseInit } from '../src/util/fetch/fetch'; describe('fetch', () => { - let fetchStub: SinonStub; + let fetchSpy: MockInstance; beforeEach(() => { - fetchStub = stub(window, 'fetch').resolves(new Response()); + fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(new Response()); }); afterEach(() => { - fetchStub.restore(); + fetchSpy.mockRestore(); }); describe('getHeaders callback', () => { it('invokes getHeaders on each request', async () => { - const getHeaders = stub().returns({ 'X-Dynamic': 'value1' }); + const getHeaders = vi.fn().mockReturnValue({ 'X-Dynamic': 'value1' }); setBaseInit({ getHeaders }); await fetch('/api/test1'); await fetch('/api/test2'); - expect(getHeaders.callCount).to.equal(2); + expect(getHeaders).toHaveBeenCalledTimes(2); }); it('includes dynamic headers in request', async () => { @@ -31,8 +38,8 @@ describe('fetch', () => { await fetch('/api/test'); - const [, opts] = fetchStub.firstCall.args; - expect(opts.headers).to.have.property('X-Dynamic', 'dynamic-value'); + const [, opts] = fetchSpy.mock.calls[0]; + expect(opts.headers).toHaveProperty('X-Dynamic', 'dynamic-value'); }); it('dynamic headers override static headers', async () => { @@ -43,8 +50,8 @@ describe('fetch', () => { await fetch('/api/test'); - const [, opts] = fetchStub.firstCall.args; - expect(opts.headers).to.have.property('X-Header', 'dynamic'); + const [, opts] = fetchSpy.mock.calls[0]; + expect(opts.headers).toHaveProperty('X-Header', 'dynamic'); }); it('per-request headers override dynamic headers', async () => { @@ -56,8 +63,8 @@ describe('fetch', () => { headers: { 'X-Header': 'per-request' }, }); - const [, opts] = fetchStub.firstCall.args; - expect(opts.headers).to.have.property('X-Header', 'per-request'); + const [, opts] = fetchSpy.mock.calls[0]; + expect(opts.headers).toHaveProperty('X-Header', 'per-request'); }); it('per-request headers override both static and dynamic headers', async () => { @@ -70,8 +77,8 @@ describe('fetch', () => { headers: { 'X-Header': 'per-request' }, }); - const [, opts] = fetchStub.firstCall.args; - expect(opts.headers).to.have.property('X-Header', 'per-request'); + const [, opts] = fetchSpy.mock.calls[0]; + expect(opts.headers).toHaveProperty('X-Header', 'per-request'); }); it('returns different values on subsequent calls', async () => { @@ -83,10 +90,10 @@ describe('fetch', () => { await fetch('/api/test1'); await fetch('/api/test2'); - const [, opts1] = fetchStub.firstCall.args; - const [, opts2] = fetchStub.secondCall.args; - expect(opts1.headers).to.have.property('X-Request-Id', '1'); - expect(opts2.headers).to.have.property('X-Request-Id', '2'); + const [, opts1] = fetchSpy.mock.calls[0]; + const [, opts2] = fetchSpy.mock.calls[1]; + expect(opts1.headers).toHaveProperty('X-Request-Id', '1'); + expect(opts2.headers).toHaveProperty('X-Request-Id', '2'); }); it('merges static, dynamic, and per-request headers', async () => { @@ -99,10 +106,10 @@ describe('fetch', () => { headers: { 'X-Request': 'request-value' }, }); - const [, opts] = fetchStub.firstCall.args; - expect(opts.headers).to.have.property('X-Static', 'static-value'); - expect(opts.headers).to.have.property('X-Dynamic', 'dynamic-value'); - expect(opts.headers).to.have.property('X-Request', 'request-value'); + const [, opts] = fetchSpy.mock.calls[0]; + expect(opts.headers).toHaveProperty('X-Static', 'static-value'); + expect(opts.headers).toHaveProperty('X-Dynamic', 'dynamic-value'); + expect(opts.headers).toHaveProperty('X-Request', 'request-value'); }); }); }); diff --git a/tsconfig.json b/tsconfig.json index b8da42e..ff06c8b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,17 @@ { - "compilerOptions": { - "allowSyntheticDefaultImports": true, - "noEmit": true, - "module": "esnext", - "moduleResolution": "bundler", - "strict": true, - "target": "esnext", - "allowJs": true, - "types": ["node", "mocha"], - "baseUrl": ".", - "paths": { - "@neovici/cosmoz-queue/*": ["./src/*"] - } - }, - "include": ["src/**/*", "test/**/*", "stories/**/*"] + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "noEmit": true, + "module": "esnext", + "moduleResolution": "bundler", + "strict": true, + "target": "esnext", + "allowJs": true, + "types": ["node"], + "baseUrl": ".", + "paths": { + "@neovici/cosmoz-queue/*": ["./src/*"] + } + }, + "include": ["src/**/*", "test/**/*", "stories/**/*"] } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..e637f30 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,29 @@ +import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; +import { playwright } from '@vitest/browser-playwright'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + projects: [ + { + test: { + name: 'unit', + include: ['test/**/*.test.ts', 'src/**/*.test.ts'], + environment: 'jsdom', + }, + }, + { + plugins: [storybookTest({ configDir: '.storybook' })], + test: { + name: 'storybook', + browser: { + enabled: true, + provider: playwright(), + instances: [{ browser: 'chromium' }], + }, + setupFiles: ['.storybook/vitest.setup.ts'], + }, + }, + ], + }, +}); diff --git a/web-test-runner.config.mjs b/web-test-runner.config.mjs deleted file mode 100644 index 359824d..0000000 --- a/web-test-runner.config.mjs +++ /dev/null @@ -1,15 +0,0 @@ -import { esbuildPlugin } from '@web/dev-server-esbuild'; -import { playwrightLauncher } from '@web/test-runner-playwright'; - -export default { - nodeResolve: true, - files: ['test/**/*.test.ts', 'src/**/*.test.ts'], - plugins: [esbuildPlugin({ ts: true, target: 'auto' })], - browsers: [playwrightLauncher({ product: 'chromium' })], - testFramework: { config: { ui: 'bdd' } }, - coverage: true, - coverageConfig: { - include: ['src/**/*.ts'], - exclude: ['**/*.test.ts', '**/*.spec.ts'], - }, -}; From 575ff20d4b632184e7413debe82ceeaa16890e8a Mon Sep 17 00:00:00 2001 From: Cristian Necula Date: Fri, 6 Feb 2026 14:01:28 +0200 Subject: [PATCH 2/8] fix: resolve lint errors in test migration - Add @types/react for Storybook type definitions - Add skipLibCheck to tsconfig.json for Storybook type compatibility - Fix unused 'within' import in render.stories.ts - Fix window type casts in use-pref.stories.ts --- package-lock.json | 179 ++++-------------------------------- package.json | 1 + stories/render.stories.ts | 7 +- stories/use-pref.stories.ts | 10 +- tsconfig.json | 3 +- 5 files changed, 30 insertions(+), 170 deletions(-) diff --git a/package-lock.json b/package-lock.json index 11a418c..af586d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,6 @@ "@commitlint/config-conventional": "^20.0.0", "@eslint/eslintrc": "^2.0.0", "@neovici/cfg": "^2.8.0", - "@neovici/testing": "^2.0.0", "@semantic-release/changelog": "^6.0.0", "@semantic-release/git": "^10.0.0", "@storybook/addon-links": "^10.0.0", @@ -36,16 +35,16 @@ "@storybook/web-components": "^10.0.0", "@storybook/web-components-vite": "^10.0.0", "@types/node": "^22.10.2", + "@types/react": "^19.2.13", "@types/split.js": "^1.6.0", "@vitest/browser": "^4.0.0", - "@vitest/browser-playwright": "^4.0.18", + "@vitest/browser-playwright": "^4.0.0", "esbuild": "^0.27.0", "http-server": "^14.1.1", "husky": "^9.0.11", "jsdom": "^26.0.0", "playwright": "^1.52.0", "semantic-release": "^25.0.0", - "shadow-dom-testing-library": "^1.11.0", "storybook": "^10.0.0", "typescript": "^5.4.3", "vitest": "^4.0.0" @@ -1191,16 +1190,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esm-bundle/chai": { - "version": "4.3.4-fix.0", - "resolved": "https://registry.npmjs.org/@esm-bundle/chai/-/chai-4.3.4-fix.0.tgz", - "integrity": "sha512-26SKdM4uvDWlY8/OOOxSB1AqQWeBosCX3wRYUZO7enTAj03CtVxIiCimYVG2WpULcyV51qapK4qTovwkUr5Mlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^4.2.12" - } - }, "node_modules/@floating-ui/core": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", @@ -1746,18 +1735,6 @@ "integrity": "sha512-bAiD+cIxT8B/5+M0o9ptmq7n5rkfVnuKQrTVrtLA8BiCMDzCvMUcmASUqzcclrepikUutLcRbROJogHF2vuCpg==", "license": "MIT" }, - "node_modules/@neovici/testing": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@neovici/testing/-/testing-2.1.1.tgz", - "integrity": "sha512-mZLicbQeWbeWqrO+L8JbBlqq/szb6C5P6ed6YJmnZ5VKngOOi4Jot1VUMCqKtDdCM7z+oLTL3FUEtDDVqAFtFQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@open-wc/testing": "^4.0.0", - "@pionjs/pion": "^2.0.0", - "lit-html": "^3.0.0" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1953,62 +1930,6 @@ "@octokit/openapi-types": "^27.0.0" } }, - "node_modules/@open-wc/dedupe-mixin": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@open-wc/dedupe-mixin/-/dedupe-mixin-2.0.1.tgz", - "integrity": "sha512-+R4VxvceUxHAUJXJQipkkoV9fy10vNo+OnUnGKZnVmcwxMl460KLzytnUM4S35SI073R0yZQp9ra0MbPUwVcEA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@open-wc/scoped-elements": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@open-wc/scoped-elements/-/scoped-elements-3.0.6.tgz", - "integrity": "sha512-w1ayJaUUmBw8tALtqQ6cBueld+op+bufujzbrOdH0uCTXnSQkONYZzOH+9jyQ8auVgKLqcxZ8oU6SzfqQhQkPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@open-wc/dedupe-mixin": "^2.0.0", - "lit": "^3.0.0" - } - }, - "node_modules/@open-wc/semantic-dom-diff": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.20.1.tgz", - "integrity": "sha512-mPF/RPT2TU7Dw41LEDdaeP6eyTOWBD4z0+AHP4/d0SbgcfJZVRymlIB6DQmtz0fd2CImIS9kszaMmwMt92HBPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^4.3.1", - "@web/test-runner-commands": "^0.9.0" - } - }, - "node_modules/@open-wc/testing": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@open-wc/testing/-/testing-4.0.0.tgz", - "integrity": "sha512-KI70O0CJEpBWs3jrTju4BFCy7V/d4tFfYWkg8pMzncsDhD7TYNHLw5cy+s1FHXIgVFetnMDhPpwlKIPvtTQW7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@esm-bundle/chai": "^4.3.4-fix.0", - "@open-wc/semantic-dom-diff": "^0.20.0", - "@open-wc/testing-helpers": "^3.0.0", - "@types/chai-dom": "^1.11.0", - "@types/sinon-chai": "^3.2.3", - "chai-a11y-axe": "^1.5.0" - } - }, - "node_modules/@open-wc/testing-helpers": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@open-wc/testing-helpers/-/testing-helpers-3.0.1.tgz", - "integrity": "sha512-hyNysSatbgT2FNxHJsS3rGKcLEo6+HwDFu1UQL6jcSQUabp/tj3PyX7UnXL3H5YGv0lJArdYLSnvjLnjn3O2fw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@open-wc/scoped-elements": "^3.0.2", - "lit": "^2.0.0 || ^3.0.0", - "lit-html": "^2.0.0 || ^3.0.0" - } - }, "node_modules/@pionjs/pion": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/@pionjs/pion/-/pion-2.11.0.tgz", @@ -3572,23 +3493,6 @@ "@types/node": "*" } }, - "node_modules/@types/chai": { - "version": "4.3.20", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", - "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/chai-dom": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@types/chai-dom/-/chai-dom-1.11.3.tgz", - "integrity": "sha512-EUEZI7uID4ewzxnU7DJXtyvykhQuwe+etJ1wwOiJyQRTH/ifMWKX+ghiXkxCUvNJ6IQDodf0JXhuP6zZcy2qXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "*" - } - }, "node_modules/@types/co-body": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/@types/co-body/-/co-body-6.1.3.tgz", @@ -3828,6 +3732,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/react": { + "version": "19.2.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz", + "integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -3856,34 +3770,6 @@ "@types/node": "*" } }, - "node_modules/@types/sinon": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-21.0.0.tgz", - "integrity": "sha512-+oHKZ0lTI+WVLxx1IbJDNmReQaIsQJjN2e7UUrJHEeByG7bFeKJYsv1E75JxTQ9QKJDp21bAa/0W2Xo4srsDnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/sinonjs__fake-timers": "*" - } - }, - "node_modules/@types/sinon-chai": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.12.tgz", - "integrity": "sha512-9y0Gflk3b0+NhQZ/oxGtaAJDvRywCa5sIyaVnounqLvmf93yBF4EgIRspePtkMs3Tr844nCclYMlcCNmLCvjuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "*", - "@types/sinon": "*" - } - }, - "node_modules/@types/sinonjs__fake-timers": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-15.0.1.tgz", - "integrity": "sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/split.js": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@types/split.js/-/split.js-1.6.0.tgz", @@ -5922,16 +5808,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/axe-core": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", - "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", - "dev": true, - "license": "MPL-2.0", - "engines": { - "node": ">=4" - } - }, "node_modules/b4a": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", @@ -6252,16 +6128,6 @@ "node": ">=18" } }, - "node_modules/chai-a11y-axe": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/chai-a11y-axe/-/chai-a11y-axe-1.5.0.tgz", - "integrity": "sha512-V/Vg/zJDr9aIkaHJ2KQu7lGTQQm5ZOH4u1k5iTMvIXuSVlSuUo0jcSpSqf9wUn9zl6oQXa4e4E0cqH18KOgKlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "axe-core": "^4.3.3" - } - }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -6999,6 +6865,13 @@ "node": ">=18" } }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/dargs": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz", @@ -16103,20 +15976,6 @@ "dev": true, "license": "ISC" }, - "node_modules/shadow-dom-testing-library": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/shadow-dom-testing-library/-/shadow-dom-testing-library-1.13.1.tgz", - "integrity": "sha512-X73VDI3KumYeYuz/C2yjn4/1bsUN68JleYI3NwFJUDa+5fWhO//s+02gdxdetY+mVRYZosVokkkmIhdzjABnmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14", - "npm": ">= 7" - }, - "peerDependencies": { - "@testing-library/dom": ">= 8" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index 77d13dd..04956e8 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "@storybook/web-components": "^10.0.0", "@storybook/web-components-vite": "^10.0.0", "@types/node": "^22.10.2", + "@types/react": "^19.2.13", "@types/split.js": "^1.6.0", "@vitest/browser": "^4.0.0", "@vitest/browser-playwright": "^4.0.0", diff --git a/stories/render.stories.ts b/stories/render.stories.ts index 595942e..39f1dc8 100644 --- a/stories/render.stories.ts +++ b/stories/render.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components'; import { html, nothing } from 'lit-html'; -import { expect, fn, userEvent, within } from 'storybook/test'; +import { expect, fn, userEvent } from 'storybook/test'; import { renderNav, renderPagination } from '../src/queue/render'; const meta: Meta = { @@ -12,10 +12,7 @@ export default meta; export const RenderNavTest: StoryObj = { render: () => html`
${renderNav({})}
`, async play({ canvasElement }) { - const canvas = within(canvasElement); - const container = canvas.getByTestId - ? canvasElement.querySelector('#test-container') - : canvasElement.querySelector('#test-container'); + const container = canvasElement.querySelector('#test-container'); // Both buttons should be disabled when no callbacks are provided const buttons = container?.querySelectorAll('button.button-nav'); diff --git a/stories/use-pref.stories.ts b/stories/use-pref.stories.ts index 6e87d34..1bfbdb7 100644 --- a/stories/use-pref.stories.ts +++ b/stories/use-pref.stories.ts @@ -44,7 +44,8 @@ const createPrefComponent = (key: string, defaultValue?: string) => { export const DefaultPref: StoryObj = { render: () => { const tagName = createPrefComponent('default-test', 'asdad'); - (window as Record).__defaultPrefTagName = tagName; + (window as unknown as Record).__defaultPrefTagName = + tagName; const el = document.createElement(tagName); return el; }, @@ -52,7 +53,7 @@ export const DefaultPref: StoryObj = { // Wait for component to render await new Promise((r) => setTimeout(r, 100)); - const tagName = (window as Record) + const tagName = (window as unknown as Record) .__defaultPrefTagName as string; // Query inside shadow DOM @@ -67,7 +68,8 @@ export const DefaultPref: StoryObj = { export const UpdatePref: StoryObj = { render: () => { const tagName = createPrefComponent('update-test'); - (window as Record).__updatePrefTagName = tagName; + (window as unknown as Record).__updatePrefTagName = + tagName; const el = document.createElement(tagName); return el; }, @@ -75,7 +77,7 @@ export const UpdatePref: StoryObj = { // Wait for component to render await new Promise((r) => setTimeout(r, 100)); - const tagName = (window as Record) + const tagName = (window as unknown as Record) .__updatePrefTagName as string; const result = hookResults.get(tagName); expect(result).toBeDefined(); diff --git a/tsconfig.json b/tsconfig.json index ff06c8b..ed23677 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,8 @@ "baseUrl": ".", "paths": { "@neovici/cosmoz-queue/*": ["./src/*"] - } + }, + "skipLibCheck": true }, "include": ["src/**/*", "test/**/*", "stories/**/*"] } From 462bdb19ac601f6c229685b1991b39d6c865b56b Mon Sep 17 00:00:00 2001 From: Cristian Necula Date: Fri, 6 Feb 2026 15:18:00 +0200 Subject: [PATCH 3/8] refactor: address PR feedback - Create stories/helpers/render-hook.ts utility for testing Pion hooks - Use Storybook args pattern in render.stories.ts instead of window globals - Simplify use-pref.stories.ts using renderHook utility - Remove all window global usage from story tests --- stories/helpers/render-hook.ts | 43 ++++++++++++++++ stories/render.stories.ts | 53 ++++++++------------ stories/use-pref.stories.ts | 91 +++++++--------------------------- 3 files changed, 83 insertions(+), 104 deletions(-) create mode 100644 stories/helpers/render-hook.ts diff --git a/stories/helpers/render-hook.ts b/stories/helpers/render-hook.ts new file mode 100644 index 0000000..7a006b9 --- /dev/null +++ b/stories/helpers/render-hook.ts @@ -0,0 +1,43 @@ +import { component } from '@pionjs/pion'; + +interface RenderHookResult { + result: { current: T }; + nextUpdate: () => Promise; + unmount: () => void; +} + +export const renderHook = async ( + callback: () => T, +): Promise> => { + let current: T; + let resolver: (() => void) | null = null; + + const tagName = `test-hook-${Math.random().toString(36).slice(2)}`; + + const TestComponent = component(() => { + current = callback(); + resolver?.(); + resolver = null; + return null; + }); + + customElements.define(tagName, TestComponent); + const el = document.createElement(tagName); + document.body.appendChild(el); + + // Wait for initial render + await new Promise((r) => setTimeout(r, 0)); + + return { + result: { + get current() { + return current; + }, + }, + nextUpdate: () => + new Promise((r) => { + resolver = r as () => void; + }), + unmount: () => el.remove(), + }; +}; diff --git a/stories/render.stories.ts b/stories/render.stories.ts index 39f1dc8..c6579e7 100644 --- a/stories/render.stories.ts +++ b/stories/render.stories.ts @@ -26,14 +26,15 @@ export const RenderNavTest: StoryObj = { }; export const RenderNavWithCallbacks: StoryObj = { - render: () => { - const prev = fn(); - const next = fn(); - (window as unknown as Record).__testPrev = prev; - (window as unknown as Record).__testNext = next; - return html`
${renderNav({ prev, next })}
`; + args: { + prev: fn(), + next: fn(), }, - async play({ canvasElement }) { + render: (args) => + html`
+ ${renderNav(args as Parameters[0])} +
`, + async play({ args, canvasElement }) { const container = canvasElement.querySelector('#test-container'); const prevButton = container?.querySelector( @@ -49,14 +50,10 @@ export const RenderNavWithCallbacks: StoryObj = { // Test clicking await userEvent.click(prevButton); - expect( - (window as unknown as Record).__testPrev, - ).toHaveBeenCalled(); + expect(args.prev).toHaveBeenCalled(); await userEvent.click(nextButton); - expect( - (window as unknown as Record).__testNext, - ).toHaveBeenCalled(); + expect(args.next).toHaveBeenCalled(); }, }; @@ -72,18 +69,18 @@ export const RenderPaginationNothing: StoryObj = { }; export const RenderPaginationTest: StoryObj = { - render: () => { - const onPage = fn(); - (window as unknown as Record).__testOnPage = onPage; - return html`
+ args: { + onPage: fn(), + }, + render: (args) => + html`
${renderPagination({ totalPages: 10, pageNumber: 3, - onPage, + onPage: args.onPage as (page: number) => void, })} -
`; - }, - async play({ canvasElement }) { +
`, + async play({ args, canvasElement }) { const container = canvasElement.querySelector('#test-container'); const prevButton = container?.querySelector( @@ -98,19 +95,11 @@ export const RenderPaginationTest: StoryObj = { // Click next page await userEvent.click(nextButton); - expect( - (window as unknown as Record).__testOnPage, - ).toHaveBeenCalledWith(4); + expect(args.onPage).toHaveBeenCalledWith(4); // Reset and click prev page - ( - (window as unknown as Record).__testOnPage as ReturnType< - typeof fn - > - ).mockClear(); + (args.onPage as ReturnType).mockClear(); await userEvent.click(prevButton); - expect( - (window as unknown as Record).__testOnPage, - ).toHaveBeenCalledWith(2); + expect(args.onPage).toHaveBeenCalledWith(2); }, }; diff --git a/stories/use-pref.stories.ts b/stories/use-pref.stories.ts index 1bfbdb7..b3fec5b 100644 --- a/stories/use-pref.stories.ts +++ b/stories/use-pref.stories.ts @@ -1,8 +1,8 @@ -import { component } from '@pionjs/pion'; import type { Meta, StoryObj } from '@storybook/web-components'; import { html } from 'lit-html'; import { expect } from 'storybook/test'; import { usePref } from '../src/queue/use-pref'; +import { renderHook } from './helpers/render-hook'; const meta: Meta = { title: 'Tests/UsePref', @@ -10,88 +10,35 @@ const meta: Meta = { export default meta; -// Counter for unique component names -let counter = 0; - -// Store for hook results - keyed by component instance -const hookResults = new Map< - string, - [string | undefined, (v: string) => void] ->(); - -// Create unique component for each test run -const createPrefComponent = (key: string, defaultValue?: string) => { - const id = counter++; - const tagName = `test-pref-${id}`; - const prefKey = `${key}-${id}`; - const localStorageKey = `pref-${prefKey}`; - - // Clear localStorage BEFORE defining component - localStorage.removeItem(localStorageKey); - - const TestComponent = component(() => { - const result = usePref(prefKey, defaultValue!); - hookResults.set(tagName, result); - return html`
- ${result[0] ?? 'undefined'} -
`; - }); - customElements.define(tagName, TestComponent); - - return tagName; -}; - export const DefaultPref: StoryObj = { - render: () => { - const tagName = createPrefComponent('default-test', 'asdad'); - (window as unknown as Record).__defaultPrefTagName = - tagName; - const el = document.createElement(tagName); - return el; - }, - async play({ canvasElement }) { - // Wait for component to render - await new Promise((r) => setTimeout(r, 100)); - - const tagName = (window as unknown as Record) - .__defaultPrefTagName as string; - - // Query inside shadow DOM - const hostEl = canvasElement.querySelector(tagName); - const shadowRoot = hostEl?.shadowRoot; - const el = shadowRoot?.querySelector('[data-value]'); + render: () => html`
`, + async play() { + localStorage.removeItem('pref-some'); + const { result, unmount } = await renderHook(() => + usePref('some', 'asdad'), + ); - expect(el?.getAttribute('data-value')).toBe('asdad'); + expect(result.current[0]).toBe('asdad'); + unmount(); }, }; export const UpdatePref: StoryObj = { - render: () => { - const tagName = createPrefComponent('update-test'); - (window as unknown as Record).__updatePrefTagName = - tagName; - const el = document.createElement(tagName); - return el; - }, + render: () => html`
`, async play() { - // Wait for component to render - await new Promise((r) => setTimeout(r, 100)); - - const tagName = (window as unknown as Record) - .__updatePrefTagName as string; - const result = hookResults.get(tagName); - expect(result).toBeDefined(); + localStorage.removeItem('pref-update'); + const { result, nextUpdate, unmount } = await renderHook(() => + usePref('update'), + ); // Initial value should be undefined - expect(result![0]).toBeUndefined(); + expect(result.current[0]).toBeUndefined(); // Update the pref - result![1]('dads'); - - // Wait for re-render - await new Promise((r) => setTimeout(r, 100)); + result.current[1]('dads'); + await nextUpdate(); - const newResult = hookResults.get(tagName); - expect(newResult![0]).toBe('dads'); + expect(result.current[0]).toBe('dads'); + unmount(); }, }; From c062359b9f9dcc1ae6dfbd07493ee6302a164125 Mon Sep 17 00:00:00 2001 From: Cristian Necula Date: Sat, 7 Feb 2026 01:05:13 +0200 Subject: [PATCH 4/8] refactor: use @neovici/testing instead of local renderHook helper Replace local stories/helpers/render-hook.ts with @neovici/testing@2.2.0 which now provides the same renderHook functionality for Pion hooks. --- package-lock.json | 12 ++++++++++ package.json | 1 + stories/helpers/render-hook.ts | 43 ---------------------------------- stories/use-pref.stories.ts | 2 +- 4 files changed, 14 insertions(+), 44 deletions(-) delete mode 100644 stories/helpers/render-hook.ts diff --git a/package-lock.json b/package-lock.json index af586d2..7798db6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@commitlint/config-conventional": "^20.0.0", "@eslint/eslintrc": "^2.0.0", "@neovici/cfg": "^2.8.0", + "@neovici/testing": "^2.2.0", "@semantic-release/changelog": "^6.0.0", "@semantic-release/git": "^10.0.0", "@storybook/addon-links": "^10.0.0", @@ -1735,6 +1736,17 @@ "integrity": "sha512-bAiD+cIxT8B/5+M0o9ptmq7n5rkfVnuKQrTVrtLA8BiCMDzCvMUcmASUqzcclrepikUutLcRbROJogHF2vuCpg==", "license": "MIT" }, + "node_modules/@neovici/testing": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@neovici/testing/-/testing-2.2.0.tgz", + "integrity": "sha512-933xOi5sYOTFrTC4omR27OKdtD3KY1quIXY6Y4t+vORbtT/DESDSd44tmy30eeICSmX7aEErxmDuL8Qq6JeAFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@pionjs/pion": "^2.0.0", + "lit-html": "^3.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index 04956e8..63285df 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "@commitlint/config-conventional": "^20.0.0", "@eslint/eslintrc": "^2.0.0", "@neovici/cfg": "^2.8.0", + "@neovici/testing": "^2.2.0", "@semantic-release/changelog": "^6.0.0", "@semantic-release/git": "^10.0.0", "@storybook/addon-links": "^10.0.0", diff --git a/stories/helpers/render-hook.ts b/stories/helpers/render-hook.ts deleted file mode 100644 index 7a006b9..0000000 --- a/stories/helpers/render-hook.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { component } from '@pionjs/pion'; - -interface RenderHookResult { - result: { current: T }; - nextUpdate: () => Promise; - unmount: () => void; -} - -export const renderHook = async ( - callback: () => T, -): Promise> => { - let current: T; - let resolver: (() => void) | null = null; - - const tagName = `test-hook-${Math.random().toString(36).slice(2)}`; - - const TestComponent = component(() => { - current = callback(); - resolver?.(); - resolver = null; - return null; - }); - - customElements.define(tagName, TestComponent); - const el = document.createElement(tagName); - document.body.appendChild(el); - - // Wait for initial render - await new Promise((r) => setTimeout(r, 0)); - - return { - result: { - get current() { - return current; - }, - }, - nextUpdate: () => - new Promise((r) => { - resolver = r as () => void; - }), - unmount: () => el.remove(), - }; -}; diff --git a/stories/use-pref.stories.ts b/stories/use-pref.stories.ts index b3fec5b..ddadfba 100644 --- a/stories/use-pref.stories.ts +++ b/stories/use-pref.stories.ts @@ -1,8 +1,8 @@ +import { renderHook } from '@neovici/testing'; import type { Meta, StoryObj } from '@storybook/web-components'; import { html } from 'lit-html'; import { expect } from 'storybook/test'; import { usePref } from '../src/queue/use-pref'; -import { renderHook } from './helpers/render-hook'; const meta: Meta = { title: 'Tests/UsePref', From 5ef94aa642cd07d8dab3048920b164750e07616c Mon Sep 17 00:00:00 2001 From: Cristian Necula Date: Sat, 7 Feb 2026 01:20:57 +0200 Subject: [PATCH 5/8] test: add HTML snapshot testing for render components - Add HTML snapshots to RenderNavTest, RenderNavWithCallbacks, and RenderPaginationTest - Use vitest expect for snapshot assertions (storybook/test uses Chai which lacks snapshot support) - Snapshots capture DOM structure before user interactions --- .storybook/vitest.setup.ts | 17 +++++- stories/__snapshots__/render.stories.ts.snap | 58 ++++++++++++++++++++ stories/render.stories.ts | 10 ++++ 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 stories/__snapshots__/render.stories.ts.snap diff --git a/.storybook/vitest.setup.ts b/.storybook/vitest.setup.ts index a58536e..761e7ef 100644 --- a/.storybook/vitest.setup.ts +++ b/.storybook/vitest.setup.ts @@ -1,7 +1,22 @@ import { setProjectAnnotations } from '@storybook/web-components'; -import { beforeAll } from 'vitest'; +import { beforeAll, expect } from 'vitest'; import * as previewAnnotations from './preview'; const annotations = setProjectAnnotations([previewAnnotations]); beforeAll(annotations.beforeAll); + +// Custom snapshot serializer to strip lit-html dynamic markers +// These markers (e.g., ) change between runs +expect.addSnapshotSerializer({ + test: (val) => typeof val === 'string' && val.includes('/gu, ''), + config, + indentation, + depth, + refs, + ), +}); diff --git a/stories/__snapshots__/render.stories.ts.snap b/stories/__snapshots__/render.stories.ts.snap new file mode 100644 index 0000000..c5d4ffe --- /dev/null +++ b/stories/__snapshots__/render.stories.ts.snap @@ -0,0 +1,58 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Render Nav Test 1`] = ` +" + " +`; + +exports[`Render Nav With Callbacks 1`] = ` +" + + + " +`; + +exports[`Render Pagination Test 1`] = ` +" +
+ + +
+ " +`; diff --git a/stories/render.stories.ts b/stories/render.stories.ts index c6579e7..2fd3be1 100644 --- a/stories/render.stories.ts +++ b/stories/render.stories.ts @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from '@storybook/web-components'; import { html, nothing } from 'lit-html'; import { expect, fn, userEvent } from 'storybook/test'; +import { expect as vitestExpect } from 'vitest'; import { renderNav, renderPagination } from '../src/queue/render'; const meta: Meta = { @@ -14,6 +15,9 @@ export const RenderNavTest: StoryObj = { async play({ canvasElement }) { const container = canvasElement.querySelector('#test-container'); + // HTML snapshot + vitestExpect(container?.innerHTML).toMatchSnapshot(); + // Both buttons should be disabled when no callbacks are provided const buttons = container?.querySelectorAll('button.button-nav'); expect(buttons).toHaveLength(2); @@ -37,6 +41,9 @@ export const RenderNavWithCallbacks: StoryObj = { async play({ args, canvasElement }) { const container = canvasElement.querySelector('#test-container'); + // HTML snapshot (before interactions) + vitestExpect(container?.innerHTML).toMatchSnapshot(); + const prevButton = container?.querySelector( '.button-nav.prev', ) as HTMLButtonElement; @@ -83,6 +90,9 @@ export const RenderPaginationTest: StoryObj = { async play({ args, canvasElement }) { const container = canvasElement.querySelector('#test-container'); + // HTML snapshot (before interactions) + vitestExpect(container?.innerHTML).toMatchSnapshot(); + const prevButton = container?.querySelector( '.page-prev', ) as HTMLButtonElement; From 259a64d27b9fbebc252d05449b4dd512de88e4b3 Mon Sep 17 00:00:00 2001 From: Cristian Necula Date: Sat, 7 Feb 2026 02:46:30 +0200 Subject: [PATCH 6/8] refactor: remove snapshot tests from stories Move snapshot assertions out of story play functions to avoid vitest expect being bundled into the deployed Storybook UI, which caused 'Cannot read properties of undefined' errors. The interaction tests remain and provide sufficient coverage. --- .storybook/vitest.setup.ts | 17 +----- stories/__snapshots__/render.stories.ts.snap | 58 -------------------- stories/render.stories.ts | 10 ---- 3 files changed, 1 insertion(+), 84 deletions(-) delete mode 100644 stories/__snapshots__/render.stories.ts.snap diff --git a/.storybook/vitest.setup.ts b/.storybook/vitest.setup.ts index 761e7ef..a58536e 100644 --- a/.storybook/vitest.setup.ts +++ b/.storybook/vitest.setup.ts @@ -1,22 +1,7 @@ import { setProjectAnnotations } from '@storybook/web-components'; -import { beforeAll, expect } from 'vitest'; +import { beforeAll } from 'vitest'; import * as previewAnnotations from './preview'; const annotations = setProjectAnnotations([previewAnnotations]); beforeAll(annotations.beforeAll); - -// Custom snapshot serializer to strip lit-html dynamic markers -// These markers (e.g., ) change between runs -expect.addSnapshotSerializer({ - test: (val) => typeof val === 'string' && val.includes('/gu, ''), - config, - indentation, - depth, - refs, - ), -}); diff --git a/stories/__snapshots__/render.stories.ts.snap b/stories/__snapshots__/render.stories.ts.snap deleted file mode 100644 index c5d4ffe..0000000 --- a/stories/__snapshots__/render.stories.ts.snap +++ /dev/null @@ -1,58 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`Render Nav Test 1`] = ` -" - " -`; - -exports[`Render Nav With Callbacks 1`] = ` -" - - - " -`; - -exports[`Render Pagination Test 1`] = ` -" -
- - -
- " -`; diff --git a/stories/render.stories.ts b/stories/render.stories.ts index 2fd3be1..c6579e7 100644 --- a/stories/render.stories.ts +++ b/stories/render.stories.ts @@ -1,7 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components'; import { html, nothing } from 'lit-html'; import { expect, fn, userEvent } from 'storybook/test'; -import { expect as vitestExpect } from 'vitest'; import { renderNav, renderPagination } from '../src/queue/render'; const meta: Meta = { @@ -15,9 +14,6 @@ export const RenderNavTest: StoryObj = { async play({ canvasElement }) { const container = canvasElement.querySelector('#test-container'); - // HTML snapshot - vitestExpect(container?.innerHTML).toMatchSnapshot(); - // Both buttons should be disabled when no callbacks are provided const buttons = container?.querySelectorAll('button.button-nav'); expect(buttons).toHaveLength(2); @@ -41,9 +37,6 @@ export const RenderNavWithCallbacks: StoryObj = { async play({ args, canvasElement }) { const container = canvasElement.querySelector('#test-container'); - // HTML snapshot (before interactions) - vitestExpect(container?.innerHTML).toMatchSnapshot(); - const prevButton = container?.querySelector( '.button-nav.prev', ) as HTMLButtonElement; @@ -90,9 +83,6 @@ export const RenderPaginationTest: StoryObj = { async play({ args, canvasElement }) { const container = canvasElement.querySelector('#test-container'); - // HTML snapshot (before interactions) - vitestExpect(container?.innerHTML).toMatchSnapshot(); - const prevButton = container?.querySelector( '.page-prev', ) as HTMLButtonElement; From f0a3a15f44d8ba0ad81cfa3ace4a2c803b767422 Mon Sep 17 00:00:00 2001 From: Cristian Necula Date: Sat, 7 Feb 2026 02:19:58 +0200 Subject: [PATCH 7/8] feat: add Load All button to fetch all data at once - Add loadAll function to use-more.ts that fetches all available data - Add Load All button to render-more.ts with onAll prop - Deprecate loading prop in favor of data$ for loading state - Wire up loadAll through use-list-core.ts and render-list-core.ts - Setup i18next in Storybook environment for translations - Add stories demonstrating Load More and Load All functionality - Update snapshots with translated button titles --- .storybook/preview.js | 15 ++ src/list/more/render-more.ts | 27 +++- src/list/more/use-more.ts | 24 +++- src/list/render-list-core.ts | 3 +- src/list/use-list-core.ts | 4 +- stories/load-more.stories.ts | 257 +++++++++++++++++++++++++++++++++++ 6 files changed, 324 insertions(+), 6 deletions(-) create mode 100644 stories/load-more.stories.ts diff --git a/.storybook/preview.js b/.storybook/preview.js index 47e288c..656a21b 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,3 +1,18 @@ +import i18next from 'i18next'; + +// Initialize i18next for Storybook environment +i18next.init({ + lng: 'en', + resources: { + en: { + translation: { + 'Load more': 'Load more', + 'Load all': 'Load all', + }, + }, + }, +}); + const preview = { parameters: { controls: { diff --git a/src/list/more/render-more.ts b/src/list/more/render-more.ts index 5a5509c..3437d87 100644 --- a/src/list/more/render-more.ts +++ b/src/list/more/render-more.ts @@ -25,12 +25,15 @@ export const renderLoadMore = ({ loading, data$, onMore, + onAll, }: { + /** @deprecated Use data$ instead */ loading?: boolean; data$?: PromiseLike; onMore?: () => void; -}) => - html``; + + +`; diff --git a/src/list/more/use-more.ts b/src/list/more/use-more.ts index 1825541..053469e 100644 --- a/src/list/more/use-more.ts +++ b/src/list/more/use-more.ts @@ -47,6 +47,28 @@ export const useMore = ({ hasMore ? () => setData((s) => ({ ...s, page: s.page + 1 })) : undefined, [hasMore], ); + + const loadAll = useMemo( + () => + hasMore + ? () => + setData((s) => ({ + ...s, + page: 0, + data$: list$({ + params: s.params, + page: 0, + pageSize: s.totalAvailable, + }).then((data) => { + setTotalAvailable(data.total); + setData((d) => ({ ...d, totalAvailable: data.total })); + return data.items; + }), + })) + : undefined, + [hasMore, list$, setTotalAvailable], + ); + useEffect( () => setData((d) => ({ @@ -81,5 +103,5 @@ export const useMore = ({ })); }, [page, params, list$, pageSize]); - return { data$, loadMore }; + return { data$, loadMore, loadAll }; }; diff --git a/src/list/render-list-core.ts b/src/list/render-list-core.ts index c86fa53..e4b2ca8 100644 --- a/src/list/render-list-core.ts +++ b/src/list/render-list-core.ts @@ -59,6 +59,7 @@ export const renderListCore = ({ content, loadMore, + loadAll, }: RenderListCore) => [ html`({ renderActions({ open, items: selectedItems, slot: 'actions' }), ), content?.({ selectedItems }), - renderLoadMore({ data$, onMore: loadMore }), + renderLoadMore({ data$, onMore: loadMore, onAll: loadAll }), ]}`, formDialog(dialog), diff --git a/src/list/use-list-core.ts b/src/list/use-list-core.ts index eb13a30..0a5a4aa 100644 --- a/src/list/use-list-core.ts +++ b/src/list/use-list-core.ts @@ -34,6 +34,7 @@ export interface UseListCoreResult< data$: PromiseLike; columns: TColumns; loadMore: (() => void) | undefined; + loadAll: (() => void) | undefined; } export const useListCore = < @@ -67,7 +68,7 @@ export const useListCore = < [_params, filters, descending, sortOn, columns, rtkn], ); const list$ = useCallback(..._list$); - const { data$, loadMore } = useMore({ + const { data$, loadMore, loadAll } = useMore({ list$, setTotalAvailable, params, @@ -81,5 +82,6 @@ export const useListCore = < dialog, open, loadMore, + loadAll, }; }; diff --git a/stories/load-more.stories.ts b/stories/load-more.stories.ts new file mode 100644 index 0000000..d76af18 --- /dev/null +++ b/stories/load-more.stories.ts @@ -0,0 +1,257 @@ +import { html, render } from 'lit-html'; +import { expect, userEvent } from 'storybook/test'; +import { renderLoadMore, style } from '../src/list/more/render-more'; + +export default { + title: 'Components/LoadMore', +}; + +interface Item { + id: string; + name: string; +} + +// Simulates paginated data fetching +const createMockList = ( + totalItems: number, + delay = 300, +): ((props: { + page: number; + pageSize: number; +}) => Promise<{ items: Item[]; total: number }>) => { + const allItems = Array.from({ length: totalItems }, (_, i) => ({ + id: `item-${i + 1}`, + name: `Item ${i + 1}`, + })); + + return async ({ page, pageSize }) => { + await new Promise((resolve) => setTimeout(resolve, delay)); + const start = page * pageSize; + const items = allItems.slice(start, start + pageSize); + return { items, total: totalItems }; + }; +}; + +// Helper to create a stateful demo +const createLoadMoreDemo = ( + container: HTMLElement, + options: { totalItems: number; pageSize: number }, +) => { + const { totalItems, pageSize } = options; + const list$ = createMockList(totalItems); + + let state = { + items: [] as Item[], + page: 0, + data$: undefined as Promise | undefined, + totalAvailable: Infinity, + }; + + const renderDemo = () => { + const hasMore = + state.totalAvailable < Infinity && + state.totalAvailable > state.items.length; + + const loadMore = hasMore + ? () => { + state.data$ = list$({ page: state.page, pageSize }).then((result) => { + state = { + ...state, + items: [...state.items, ...result.items], + page: state.page + 1, + totalAvailable: result.total, + data$: undefined, + }; + renderDemo(); + return result.items; + }); + renderDemo(); + } + : undefined; + + const loadAll = hasMore + ? () => { + state.data$ = list$({ page: 0, pageSize: state.totalAvailable }).then( + (result) => { + state = { + ...state, + items: result.items, + totalAvailable: result.total, + data$: undefined, + }; + renderDemo(); + return result.items; + }, + ); + renderDemo(); + } + : undefined; + + render( + html` + +
+
+ Showing ${state.items.length} of ${state.totalAvailable} items +
+
+ ${state.items.map( + (item) => html`
${item.name}
`, + )} +
+
+ ${renderLoadMore({ + data$: state.data$, + onMore: loadMore, + onAll: loadAll, + })} +
+
+ `, + container, + ); + }; + + // Initial load + state.data$ = list$({ page: 0, pageSize }).then((result) => { + state = { + ...state, + items: result.items, + page: 1, + totalAvailable: result.total, + data$: undefined, + }; + renderDemo(); + return result.items; + }); + renderDemo(); +}; + +// Stories +export const Default = { + render: () => html`
`, + play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { + const container = canvasElement.querySelector( + '#demo-container', + ) as HTMLElement; + createLoadMoreDemo(container, { totalItems: 100, pageSize: 10 }); + }, +}; + +export const WithCallbacks = { + render: () => html`
`, + play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { + const container = canvasElement.querySelector( + '#demo-container', + ) as HTMLElement; + createLoadMoreDemo(container, { totalItems: 100, pageSize: 10 }); + + // Wait for initial load + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Find buttons + const loadMoreButton = container.querySelector( + 'button.more:not([hidden])', + ) as HTMLButtonElement; + expect(loadMoreButton).not.toBeNull(); + expect(loadMoreButton.textContent).toContain('Load more'); + + // Click "Load more" + await userEvent.click(loadMoreButton); + + // Wait for load to complete + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Verify more items loaded + const stats = container.querySelector( + '[data-testid="stats"]', + ) as HTMLElement; + expect(stats.textContent).toContain('Showing 20 of 100 items'); + }, +}; + +export const LoadAllTest = { + render: () => html`
`, + play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { + const container = canvasElement.querySelector( + '#demo-container', + ) as HTMLElement; + createLoadMoreDemo(container, { totalItems: 100, pageSize: 10 }); + + // Wait for initial load + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Find Load all button (second .more button) + const buttons = container.querySelectorAll('button.more:not([hidden])'); + const loadAllButton = Array.from(buttons).find((b) => + b.textContent?.includes('Load all'), + ) as HTMLButtonElement; + expect(loadAllButton).not.toBeNull(); + + // Click "Load all" + await userEvent.click(loadAllButton); + + // Wait for load to complete + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Verify all items loaded + const stats = container.querySelector( + '[data-testid="stats"]', + ) as HTMLElement; + expect(stats.textContent).toContain('Showing 100 of 100 items'); + + // Buttons should be hidden now + const visibleButtons = container.querySelectorAll( + 'button.more:not([hidden])', + ); + expect(visibleButtons.length).toBe(0); + }, +}; + +export const AllDataLoaded = { + render: () => html`
`, + play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { + const container = canvasElement.querySelector( + '#demo-container', + ) as HTMLElement; + createLoadMoreDemo(container, { totalItems: 5, pageSize: 10 }); + + // Wait for initial load + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Buttons should be hidden when all data is already loaded + const visibleButtons = container.querySelectorAll( + 'button.more:not([hidden])', + ); + expect(visibleButtons.length).toBe(0); + }, +}; From 9241c1dd994255f05864986e8da59b1739ea7d6a Mon Sep 17 00:00:00 2001 From: Cristian Necula Date: Sat, 7 Feb 2026 03:13:35 +0200 Subject: [PATCH 8/8] refactor: use single spinner for load more buttons --- src/list/more/render-more.ts | 59 ++++++++++++++---------------------- 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/src/list/more/render-more.ts b/src/list/more/render-more.ts index 3437d87..fca4745 100644 --- a/src/list/more/render-more.ts +++ b/src/list/more/render-more.ts @@ -3,7 +3,6 @@ import { css } from '@pionjs/pion'; import { t } from 'i18next'; import { html, nothing } from 'lit-html'; import { until } from 'lit-html/directives/until.js'; -import { when } from 'lit-html/directives/when.js'; export const style = css` .more { @@ -21,6 +20,20 @@ export const style = css` } `; +const renderSpinner = (loading?: boolean, data$?: PromiseLike) => { + if (loading) return html``; + if (data$) { + return until( + data$.then( + () => nothing, + () => nothing, + ), + html``, + ); + } + return nothing; +}; + export const renderLoadMore = ({ loading, data$, @@ -33,39 +46,13 @@ export const renderLoadMore = ({ onMore?: () => void; onAll?: () => void; }) => html` - - + + ${renderSpinner(loading, data$)} + + + `;