From 71014c3a90773fa85c29ab981fc9ab1506d2398e Mon Sep 17 00:00:00 2001 From: Jixun Wu Date: Tue, 16 May 2023 22:47:47 +0100 Subject: [PATCH 1/3] test: get coverage work --- .gitignore | 1 + jest.config.cjs | 8 +++++++- src/decrypt-worker/client.ts | 12 +----------- src/util/DecryptionQueue.ts | 13 +++++++++++++ 4 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 src/util/DecryptionQueue.ts diff --git a/.gitignore b/.gitignore index a547bf3..6cf4bce 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ pnpm-debug.log* lerna-debug.log* node_modules +coverage/ dist dist-ssr *.local diff --git a/jest.config.cjs b/jest.config.cjs index 0b3269b..d353e67 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -1,6 +1,12 @@ module.exports = { roots: ['/src'], - collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts', '!src/mocks/**'], + collectCoverageFrom: [ + 'src/**/*.{js,jsx,ts,tsx}', + '!src/**/*.d.ts', + '!src/mocks/**', + // Worker client file uses "import.meta.url" and breaks coverage collector. + '!src/decrypt-worker/client.ts', + ], coveragePathIgnorePatterns: [], setupFilesAfterEnv: ['./src/test-utils/setup-jest.ts'], testEnvironment: 'jsdom', diff --git a/src/decrypt-worker/client.ts b/src/decrypt-worker/client.ts index b65cc05..2eb702b 100644 --- a/src/decrypt-worker/client.ts +++ b/src/decrypt-worker/client.ts @@ -1,6 +1,6 @@ -import { ConcurrentQueue } from '~/util/ConcurrentQueue'; import { WorkerClientBus } from '~/util/WorkerEventBus'; import { DECRYPTION_WORKER_ACTION_NAME } from './constants'; +import { DecryptionQueue } from '~/util/DecryptionQueue'; // TODO: Worker pool? export const workerClient = new Worker(new URL('./worker', import.meta.url), { type: 'module' }); @@ -8,15 +8,5 @@ export const workerClient = new Worker(new URL('./worker', import.meta.url), { t // FIXME: report the error so is obvious to the user. workerClient.onerror = (err) => console.error(err); -class DecryptionQueue extends ConcurrentQueue<{ id: string; blobURI: string }> { - constructor(private workerClientBus: WorkerClientBus, maxQueue?: number) { - super(maxQueue); - } - - async handler(item: { id: string; blobURI: string }): Promise { - return this.workerClientBus.request(DECRYPTION_WORKER_ACTION_NAME.DECRYPT, item.blobURI); - } -} - export const workerClientBus = new WorkerClientBus(workerClient); export const decryptionQueue = new DecryptionQueue(workerClientBus); diff --git a/src/util/DecryptionQueue.ts b/src/util/DecryptionQueue.ts new file mode 100644 index 0000000..e111d22 --- /dev/null +++ b/src/util/DecryptionQueue.ts @@ -0,0 +1,13 @@ +import { DECRYPTION_WORKER_ACTION_NAME } from '~/decrypt-worker/constants'; +import { ConcurrentQueue } from './ConcurrentQueue'; +import { WorkerClientBus } from './WorkerEventBus'; + +export class DecryptionQueue extends ConcurrentQueue<{ id: string; blobURI: string }> { + constructor(private workerClientBus: WorkerClientBus, maxQueue?: number) { + super(maxQueue); + } + + async handler(item: { id: string; blobURI: string }): Promise { + return this.workerClientBus.request(DECRYPTION_WORKER_ACTION_NAME.DECRYPT, item.blobURI); + } +} From 1069c4c6bbe21c03e603bad5ff8b501a16567e2b Mon Sep 17 00:00:00 2001 From: Jixun Wu Date: Tue, 16 May 2023 22:50:03 +0100 Subject: [PATCH 2/3] chore: apply indentation rule for cjs/mjs as well --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index fd4bad9..43a0ceb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,5 +11,5 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[*.{js{x,on,},ts{x,}}] +[*.{{c,m,}js{x,on,},ts{x,}}] indent_size = 2 From cd1f6cb186fbb39649e1652869a4c0dd2809697b Mon Sep 17 00:00:00 2001 From: Jixun Wu Date: Wed, 17 May 2023 01:46:20 +0100 Subject: [PATCH 3/3] test: working test with TypeScript + vite (using vitest) --- jest.config.cjs | 37 -- package.json | 15 +- pnpm-lock.yaml | 456 +++++++++++++++++- src/SDKVersion.tsx | 2 +- .../sanity-check.test.tsx} | 0 src/assets/no-cover.svg | 9 + src/decrypt-worker/client.ts | 2 +- src/features/file-listing/FileRow.tsx | 32 +- .../__tests__/FileListing.test.tsx | 18 + .../file-listing/__tests__/FileRow.test.tsx | 24 + .../__tests__/__fixture__/file-list.ts | 43 ++ src/features/file-listing/fileListingSlice.ts | 10 +- src/main.tsx | 5 +- src/store.ts | 21 +- src/test-utils/setup-jest.ts | 15 + src/test-utils/test-helper.tsx | 25 + tsconfig.json | 5 +- tsconfig.node.json | 4 +- vitest.config.ts | 39 ++ 19 files changed, 687 insertions(+), 75 deletions(-) delete mode 100644 jest.config.cjs rename src/{__test__/hello.test.tsx => __tests__/sanity-check.test.tsx} (100%) create mode 100644 src/assets/no-cover.svg create mode 100644 src/features/file-listing/__tests__/FileListing.test.tsx create mode 100644 src/features/file-listing/__tests__/FileRow.test.tsx create mode 100644 src/features/file-listing/__tests__/__fixture__/file-list.ts create mode 100644 src/test-utils/test-helper.tsx create mode 100644 vitest.config.ts diff --git a/jest.config.cjs b/jest.config.cjs deleted file mode 100644 index d353e67..0000000 --- a/jest.config.cjs +++ /dev/null @@ -1,37 +0,0 @@ -module.exports = { - roots: ['/src'], - collectCoverageFrom: [ - 'src/**/*.{js,jsx,ts,tsx}', - '!src/**/*.d.ts', - '!src/mocks/**', - // Worker client file uses "import.meta.url" and breaks coverage collector. - '!src/decrypt-worker/client.ts', - ], - coveragePathIgnorePatterns: [], - setupFilesAfterEnv: ['./src/test-utils/setup-jest.ts'], - testEnvironment: 'jsdom', - modulePaths: ['/src'], - transform: { - '^.+\\.(ts|js|tsx|jsx)$': '@swc/jest', - }, - transformIgnorePatterns: [ - '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$', - '^.+\\.module\\.(css|sass|scss)$', - ], - modulePaths: ['/src'], - moduleNameMapper: { - '^react-native$': 'react-native-web', - '^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy', - }, - moduleFileExtensions: [ - // Place tsx and ts to beginning as suggestion from Jest team - // https://jestjs.io/docs/configuration#modulefileextensions-arraystring - 'tsx', - 'ts', - 'js', - 'json', - 'jsx', - ], - // watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'], - resetMocks: true, -}; diff --git a/package.json b/package.json index 5bcfae3..a8536f2 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,11 @@ "build": "tsc -p tsconfig.prod.json && vite build", "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "format": "prettier -w .", - "test": "NODE_ENV=test jest", - "preview": "vite preview" + "test": "vitest run", + "test:ui": "vitest --ui", + "test:coverage": "vitest run --coverage", + "preview": "vite preview", + "preview:coverage": "vite preview --outDir coverage --port 5175" }, "dependencies": { "@chakra-ui/icons": "^2.0.19", @@ -40,20 +43,24 @@ "@typescript-eslint/eslint-plugin": "^5.57.1", "@typescript-eslint/parser": "^5.57.1", "@vitejs/plugin-react": "^4.0.0", + "@vitest/coverage-c8": "^0.31.0", + "@vitest/ui": "^0.31.0", "eslint": "^8.38.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.3.4", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", + "prettier": "^2.8.8", "typescript": "^5.0.2", "vite": "^4.3.2", "vite-plugin-top-level-await": "^1.3.0", - "vite-plugin-wasm": "^3.2.2" + "vite-plugin-wasm": "^3.2.2", + "vitest": "^0.31.0" }, "prettier": { "singleQuote": true, "printWidth": 120, "tabWidth": 2 } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8191ce4..c177017 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,6 +81,12 @@ devDependencies: '@vitejs/plugin-react': specifier: ^4.0.0 version: 4.0.0(vite@4.3.2) + '@vitest/coverage-c8': + specifier: ^0.31.0 + version: 0.31.0(vitest@0.31.0) + '@vitest/ui': + specifier: ^0.31.0 + version: 0.31.0(vitest@0.31.0) eslint: specifier: ^8.38.0 version: 8.38.0 @@ -99,6 +105,9 @@ devDependencies: jest-environment-jsdom: specifier: ^29.5.0 version: 29.5.0 + prettier: + specifier: ^2.8.8 + version: 2.8.8 typescript: specifier: ^5.0.2 version: 5.0.2 @@ -111,6 +120,9 @@ devDependencies: vite-plugin-wasm: specifier: ^3.2.2 version: 3.2.2(vite@4.3.2) + vitest: + specifier: ^0.31.0 + version: 0.31.0(@vitest/ui@0.31.0) packages: @@ -2255,6 +2267,10 @@ packages: fastq: 1.15.0 dev: true + /@polka/url@1.0.0-next.21: + resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} + dev: true + /@popperjs/core@2.11.7: resolution: {integrity: sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==} dev: false @@ -2544,6 +2560,16 @@ packages: '@babel/types': 7.21.5 dev: true + /@types/chai-subset@1.3.3: + resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} + dependencies: + '@types/chai': 4.3.5 + dev: true + + /@types/chai@4.3.5: + resolution: {integrity: sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==} + dev: true + /@types/estree@1.0.1: resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} dev: true @@ -2819,6 +2845,73 @@ packages: - supports-color dev: true + /@vitest/coverage-c8@0.31.0(vitest@0.31.0): + resolution: {integrity: sha512-h72qN1D962AO7UefQVulm9JFP5ACS7OfhCdBHioXU8f7ohH/+NTZCgAqmgcfRNHHO/8wLFxx+93YVxhodkEJVA==} + peerDependencies: + vitest: '>=0.30.0 <1' + dependencies: + '@ampproject/remapping': 2.2.1 + c8: 7.13.0 + magic-string: 0.30.0 + picocolors: 1.0.0 + std-env: 3.3.3 + vitest: 0.31.0(@vitest/ui@0.31.0) + dev: true + + /@vitest/expect@0.31.0: + resolution: {integrity: sha512-Jlm8ZTyp6vMY9iz9Ny9a0BHnCG4fqBa8neCF6Pk/c/6vkUk49Ls6UBlgGAU82QnzzoaUs9E/mUhq/eq9uMOv/g==} + dependencies: + '@vitest/spy': 0.31.0 + '@vitest/utils': 0.31.0 + chai: 4.3.7 + dev: true + + /@vitest/runner@0.31.0: + resolution: {integrity: sha512-H1OE+Ly7JFeBwnpHTrKyCNm/oZgr+16N4qIlzzqSG/YRQDATBYmJb/KUn3GrZaiQQyL7GwpNHVZxSQd6juLCgw==} + dependencies: + '@vitest/utils': 0.31.0 + concordance: 5.0.4 + p-limit: 4.0.0 + pathe: 1.1.0 + dev: true + + /@vitest/snapshot@0.31.0: + resolution: {integrity: sha512-5dTXhbHnyUMTMOujZPB0wjFjQ6q5x9c8TvAsSPUNKjp1tVU7i9pbqcKPqntyu2oXtmVxKbuHCqrOd+Ft60r4tg==} + dependencies: + magic-string: 0.30.0 + pathe: 1.1.0 + pretty-format: 27.5.1 + dev: true + + /@vitest/spy@0.31.0: + resolution: {integrity: sha512-IzCEQ85RN26GqjQNkYahgVLLkULOxOm5H/t364LG0JYb3Apg0PsYCHLBYGA006+SVRMWhQvHlBBCyuByAMFmkg==} + dependencies: + tinyspy: 2.1.0 + dev: true + + /@vitest/ui@0.31.0(vitest@0.31.0): + resolution: {integrity: sha512-Dy86l6r3/dbJposgm7w+oqb/15UWJ0lDBbEQaS1ived3+0CTaMbT8OMkUf9vNBkSL47kvBHEBnZLa5fw5i9gUQ==} + peerDependencies: + vitest: '>=0.30.1 <1' + dependencies: + '@vitest/utils': 0.31.0 + fast-glob: 3.2.12 + fflate: 0.7.4 + flatted: 3.2.7 + pathe: 1.1.0 + picocolors: 1.0.0 + sirv: 2.0.3 + vitest: 0.31.0(@vitest/ui@0.31.0) + dev: true + + /@vitest/utils@0.31.0: + resolution: {integrity: sha512-kahaRyLX7GS1urekRXN2752X4gIgOGVX4Wo8eDUGUkTWlGpXzf5ZS6N9RUUS+Re3XEE8nVGqNyxkSxF5HXlGhQ==} + dependencies: + concordance: 5.0.4 + loupe: 2.3.6 + pretty-format: 27.5.1 + dev: true + /@zag-js/element-size@0.3.2: resolution: {integrity: sha512-bVvvigUGvAuj7PCkE5AbzvTJDTw5f3bg9nQdv+ErhVN8SfPPppLJEmmWdxqsRzrHXgx8ypJt/+Ty0kjtISVDsQ==} dev: false @@ -2948,6 +3041,10 @@ packages: engines: {node: '>=8'} dev: true + /assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true + /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: true @@ -3042,6 +3139,10 @@ packages: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true + /blueimp-md5@2.19.0: + resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==} + dev: true + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -3061,7 +3162,7 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001486 + caniuse-lite: 1.0.30001469 electron-to-chromium: 1.4.385 node-releases: 2.0.10 update-browserslist-db: 1.0.11(browserslist@4.21.5) @@ -3077,6 +3178,30 @@ packages: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: true + /c8@7.13.0: + resolution: {integrity: sha512-/NL4hQTv1gBL6J6ei80zu3IiTrmePDKXKXOTLpHvcIWZTVYQlDhVWjjWvkhICylE8EwwnMVzDZugCvdx0/DIIA==} + engines: {node: '>=10.12.0'} + hasBin: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@istanbuljs/schema': 0.1.3 + find-up: 5.0.0 + foreground-child: 2.0.0 + istanbul-lib-coverage: 3.2.0 + istanbul-lib-report: 3.0.0 + istanbul-reports: 3.1.5 + rimraf: 3.0.2 + test-exclude: 6.0.0 + v8-to-istanbul: 9.1.0 + yargs: 16.2.0 + yargs-parser: 20.2.9 + dev: true + + /cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + dev: true + /call-bind@1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: @@ -3098,8 +3223,21 @@ packages: engines: {node: '>=10'} dev: true - /caniuse-lite@1.0.30001486: - resolution: {integrity: sha512-uv7/gXuHi10Whlj0pp5q/tsK/32J2QSqVRKQhs2j8VsDCjgyruAh/eEXHF822VqO9yT6iZKw3nRwZRSPBE9OQg==} + /caniuse-lite@1.0.30001469: + resolution: {integrity: sha512-Rcp7221ScNqQPP3W+lVOYDyjdR6dC+neEQCttoNr5bAyz54AboB4iwpnWgyi8P4YUsPybVzT4LgWiBbI3drL4g==} + dev: true + + /chai@4.3.7: + resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.2 + deep-eql: 4.1.3 + get-func-name: 2.0.0 + loupe: 2.3.6 + pathval: 1.1.1 + type-detect: 4.0.8 dev: true /chalk@2.4.2: @@ -3131,6 +3269,10 @@ packages: engines: {node: '>=10'} dev: true + /check-error@1.0.2: + resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} + dev: true + /ci-info@3.8.0: resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} engines: {node: '>=8'} @@ -3140,6 +3282,14 @@ packages: resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} dev: true + /cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + /cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -3196,6 +3346,20 @@ packages: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true + /concordance@5.0.4: + resolution: {integrity: sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==} + engines: {node: '>=10.18.0 <11 || >=12.14.0 <13 || >=14'} + dependencies: + date-time: 3.1.0 + esutils: 2.0.3 + fast-diff: 1.2.0 + js-string-escape: 1.0.1 + lodash: 4.17.21 + md5-hex: 3.0.1 + semver: 7.5.0 + well-known-symbols: 2.0.0 + dev: true + /convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} @@ -3266,6 +3430,13 @@ packages: whatwg-url: 11.0.0 dev: true + /date-time@3.1.0: + resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==} + engines: {node: '>=6'} + dependencies: + time-zone: 1.0.0 + dev: true + /debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -3286,6 +3457,13 @@ packages: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} dev: true + /deep-eql@4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} + dependencies: + type-detect: 4.0.8 + dev: true + /deep-equal@2.2.1: resolution: {integrity: sha512-lKdkdV6EOGoVn65XaOsPdH4rMxTZOnmFyuIkMjM1i5HHCbfjC97dawgTAy0deYNfuqUqW+Q5VrVaQYtUpSd6yQ==} dependencies: @@ -3651,6 +3829,10 @@ packages: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true + /fast-diff@1.2.0: + resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==} + dev: true + /fast-glob@3.2.12: resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} engines: {node: '>=8.6.0'} @@ -3682,6 +3864,10 @@ packages: bser: 2.1.1 dev: true + /fflate@0.7.4: + resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} + dev: true + /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -3741,6 +3927,14 @@ packages: is-callable: 1.2.7 dev: true + /foreground-child@2.0.0: + resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} + engines: {node: '>=8.0.0'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 3.0.7 + dev: true + /form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -3803,6 +3997,10 @@ packages: engines: {node: 6.* || 8.* || >= 10.*} dev: true + /get-func-name@2.0.0: + resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} + dev: true + /get-intrinsic@1.2.1: resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} dependencies: @@ -4689,6 +4887,11 @@ packages: resolution: {integrity: sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==} dev: true + /js-string-escape@1.0.1: + resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} + engines: {node: '>= 0.8'} + dev: true + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -4804,6 +5007,11 @@ packages: /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + /local-pkg@0.4.3: + resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} + engines: {node: '>=14'} + dev: true + /locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -4836,6 +5044,12 @@ packages: dependencies: js-tokens: 4.0.0 + /loupe@2.3.6: + resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} + dependencies: + get-func-name: 2.0.0 + dev: true + /lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} dependencies: @@ -4861,6 +5075,13 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /magic-string@0.30.0: + resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -4874,6 +5095,13 @@ packages: tmpl: 1.0.5 dev: true + /md5-hex@3.0.1: + resolution: {integrity: sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==} + engines: {node: '>=8'} + dependencies: + blueimp-md5: 2.19.0 + dev: true + /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} dev: true @@ -4919,6 +5147,20 @@ packages: brace-expansion: 1.1.11 dev: true + /mlly@1.2.1: + resolution: {integrity: sha512-1aMEByaWgBPEbWV2BOPEMySRrzl7rIHXmQxam4DM8jVjalTQDjpN2ZKOLUrwyhfZQO7IXHml2StcHMhooDeEEQ==} + dependencies: + acorn: 8.8.2 + pathe: 1.1.0 + pkg-types: 1.0.3 + ufo: 1.1.2 + dev: true + + /mrmime@1.0.1: + resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} + engines: {node: '>=10'} + dev: true + /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} dev: true @@ -5050,6 +5292,13 @@ packages: yocto-queue: 0.1.0 dev: true + /p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + yocto-queue: 1.0.0 + dev: true + /p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -5112,6 +5361,14 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + /pathe@1.1.0: + resolution: {integrity: sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==} + dev: true + + /pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true + /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} dev: true @@ -5133,6 +5390,14 @@ packages: find-up: 4.1.0 dev: true + /pkg-types@1.0.3: + resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} + dependencies: + jsonc-parser: 3.2.0 + mlly: 1.2.1 + pathe: 1.1.0 + dev: true + /postcss@8.4.23: resolution: {integrity: sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==} engines: {node: ^10 || ^12 || >=14} @@ -5152,6 +5417,12 @@ packages: engines: {node: '>= 0.8.0'} dev: true + /prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + dev: true + /pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -5513,10 +5784,23 @@ packages: object-inspect: 1.12.3 dev: true + /siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: true + /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true + /sirv@2.0.3: + resolution: {integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==} + engines: {node: '>= 10'} + dependencies: + '@polka/url': 1.0.0-next.21 + mrmime: 1.0.1 + totalist: 3.0.1 + dev: true + /sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} dev: true @@ -5559,6 +5843,14 @@ packages: escape-string-regexp: 2.0.0 dev: true + /stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: true + + /std-env@3.3.3: + resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==} + dev: true + /stop-iteration-iterator@1.0.0: resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} engines: {node: '>= 0.4'} @@ -5612,6 +5904,12 @@ packages: engines: {node: '>=8'} dev: true + /strip-literal@1.0.1: + resolution: {integrity: sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==} + dependencies: + acorn: 8.8.2 + dev: true + /stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} dev: false @@ -5657,10 +5955,29 @@ packages: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true + /time-zone@1.0.0: + resolution: {integrity: sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==} + engines: {node: '>=4'} + dev: true + /tiny-invariant@1.3.1: resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} dev: false + /tinybench@2.5.0: + resolution: {integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==} + dev: true + + /tinypool@0.5.0: + resolution: {integrity: sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==} + engines: {node: '>=14.0.0'} + dev: true + + /tinyspy@2.1.0: + resolution: {integrity: sha512-7eORpyqImoOvkQJCSkL0d0mB4NHHIFAy4b1u8PHdDa7SjGS2njzl6/lyGoZLm+eyYEtlUmFGE0rFj66SWxZgQQ==} + engines: {node: '>=14.0.0'} + dev: true + /tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} dev: true @@ -5680,6 +5997,11 @@ packages: resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} dev: false + /totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + dev: true + /tough-cookie@4.1.2: resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==} engines: {node: '>=6'} @@ -5754,6 +6076,10 @@ packages: hasBin: true dev: true + /ufo@1.1.2: + resolution: {integrity: sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==} + dev: true + /universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} @@ -5836,6 +6162,27 @@ packages: convert-source-map: 1.9.0 dev: true + /vite-node@0.31.0(@types/node@20.1.1): + resolution: {integrity: sha512-8x1x1LNuPvE2vIvkSB7c1mApX5oqlgsxzHQesYF7l5n1gKrEmrClIiZuOFbFDQcjLsmcWSwwmrWrcGWm9Fxc/g==} + engines: {node: '>=v14.18.0'} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + mlly: 1.2.1 + pathe: 1.1.0 + picocolors: 1.0.0 + vite: 4.3.2(@types/node@20.1.1) + transitivePeerDependencies: + - '@types/node' + - less + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vite-plugin-top-level-await@1.3.0(vite@4.3.2): resolution: {integrity: sha512-owIfsgWudMlQODWJSwp0sQB3AZZu3qsMygeBjZy8CyjEk6OB9AGd8lHqmgwrcEqgvy9N58lYxSBLVk3/4ejEiA==} peerDependencies: @@ -5891,6 +6238,72 @@ packages: fsevents: 2.3.2 dev: true + /vitest@0.31.0(@vitest/ui@0.31.0): + resolution: {integrity: sha512-JwWJS9p3GU9GxkG7eBSmr4Q4x4bvVBSswaCFf1PBNHiPx00obfhHRJfgHcnI0ffn+NMlIh9QGvG75FlaIBdKGA==} + engines: {node: '>=v14.18.0'} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@vitest/browser': '*' + '@vitest/ui': '*' + happy-dom: '*' + jsdom: '*' + playwright: '*' + safaridriver: '*' + webdriverio: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + playwright: + optional: true + safaridriver: + optional: true + webdriverio: + optional: true + dependencies: + '@types/chai': 4.3.5 + '@types/chai-subset': 1.3.3 + '@types/node': 20.1.1 + '@vitest/expect': 0.31.0 + '@vitest/runner': 0.31.0 + '@vitest/snapshot': 0.31.0 + '@vitest/spy': 0.31.0 + '@vitest/ui': 0.31.0(vitest@0.31.0) + '@vitest/utils': 0.31.0 + acorn: 8.8.2 + acorn-walk: 8.2.0 + cac: 6.7.14 + chai: 4.3.7 + concordance: 5.0.4 + debug: 4.3.4 + local-pkg: 0.4.3 + magic-string: 0.30.0 + pathe: 1.1.0 + picocolors: 1.0.0 + std-env: 3.3.3 + strip-literal: 1.0.1 + tinybench: 2.5.0 + tinypool: 0.5.0 + vite: 4.3.2(@types/node@20.1.1) + vite-node: 0.31.0(@types/node@20.1.1) + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /w3c-xmlserializer@4.0.0: resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} engines: {node: '>=14'} @@ -5909,6 +6322,11 @@ packages: engines: {node: '>=12'} dev: true + /well-known-symbols@2.0.0: + resolution: {integrity: sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==} + engines: {node: '>=6'} + dev: true + /whatwg-encoding@2.0.0: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} @@ -5968,6 +6386,15 @@ packages: isexe: 2.0.0 dev: true + /why-is-node-running@2.2.2: + resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true + /word-wrap@1.2.3: resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} engines: {node: '>=0.10.0'} @@ -6034,11 +6461,29 @@ packages: engines: {node: '>= 6'} dev: false + /yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + dev: true + /yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} dev: true + /yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + dev: true + /yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -6056,3 +6501,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true + + /yocto-queue@1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + dev: true diff --git a/src/SDKVersion.tsx b/src/SDKVersion.tsx index 603deff..b213ddf 100644 --- a/src/SDKVersion.tsx +++ b/src/SDKVersion.tsx @@ -1,6 +1,6 @@ import { InfoOutlineIcon } from '@chakra-ui/icons'; import { Tooltip, VStack, Text, Flex } from '@chakra-ui/react'; -import { workerClientBus } from './decrypt-worker/client'; +import { workerClientBus } from './decrypt-worker/client.ts'; import { DECRYPTION_WORKER_ACTION_NAME } from './decrypt-worker/constants'; import usePromise from 'react-promise-suspense'; diff --git a/src/__test__/hello.test.tsx b/src/__tests__/sanity-check.test.tsx similarity index 100% rename from src/__test__/hello.test.tsx rename to src/__tests__/sanity-check.test.tsx diff --git a/src/assets/no-cover.svg b/src/assets/no-cover.svg new file mode 100644 index 0000000..60be7c4 --- /dev/null +++ b/src/assets/no-cover.svg @@ -0,0 +1,9 @@ + + + + 暂无封面 + + diff --git a/src/decrypt-worker/client.ts b/src/decrypt-worker/client.ts index 2eb702b..bdb4e25 100644 --- a/src/decrypt-worker/client.ts +++ b/src/decrypt-worker/client.ts @@ -6,7 +6,7 @@ import { DecryptionQueue } from '~/util/DecryptionQueue'; export const workerClient = new Worker(new URL('./worker', import.meta.url), { type: 'module' }); // FIXME: report the error so is obvious to the user. -workerClient.onerror = (err) => console.error(err); +workerClient.addEventListener('error', console.error); export const workerClientBus = new WorkerClientBus(workerClient); export const decryptionQueue = new DecryptionQueue(workerClientBus); diff --git a/src/features/file-listing/FileRow.tsx b/src/features/file-listing/FileRow.tsx index 07c04c3..0bd7e59 100644 --- a/src/features/file-listing/FileRow.tsx +++ b/src/features/file-listing/FileRow.tsx @@ -1,5 +1,4 @@ import { - Avatar, Box, Button, Card, @@ -7,6 +6,7 @@ import { Center, Grid, GridItem, + Image, Link, Text, VStack, @@ -16,6 +16,7 @@ import { import { DecryptedAudioFile, ProcessState, deleteFile } from './fileListingSlice'; import { useRef } from 'react'; import { useAppDispatch } from '~/hooks'; +import coverFallback from '~/assets/no-cover.svg'; interface FileRowProps { id: string; @@ -25,6 +26,7 @@ interface FileRowProps { export function FileRow({ id, file }: FileRowProps) { const dispatch = useAppDispatch(); const isDecrypted = file.state === ProcessState.COMPLETE; + const metadata = file.metadata; const nameWithoutExt = file.fileName.replace(/\.[a-z\d]{3,6}$/, ''); const decryptedName = nameWithoutExt + '.' + file.ext; @@ -48,7 +50,7 @@ export function FileRow({ id, file }: FileRowProps) { }; return ( - +
- {file.metadata.cover && } - {!file.metadata.cover && 暂无封面} + {metadata && ( + {`"${metadata.album}" + )}
- {file.metadata.name || nameWithoutExt} + {metadata?.name ?? nameWithoutExt} - {isDecrypted && ( + {isDecrypted && metadata && ( - 专辑: {file.metadata.album} - 艺术家: {file.metadata.artist} - 专辑艺术家: {file.metadata.albumArtist} + + 专辑: {metadata.album} + + + 艺术家: {metadata.artist} + + + 专辑艺术家: {metadata.albumArtist} + )} diff --git a/src/features/file-listing/__tests__/FileListing.test.tsx b/src/features/file-listing/__tests__/FileListing.test.tsx new file mode 100644 index 0000000..a694595 --- /dev/null +++ b/src/features/file-listing/__tests__/FileListing.test.tsx @@ -0,0 +1,18 @@ +import { FileListing } from '../FileListing'; +import { renderWithProviders, screen } from '~/test-utils/test-helper'; +import { ListingMode } from '../fileListingSlice'; +import { dummyFiles } from './__fixture__/file-list'; + +test('should be able to render a list of 3 items', () => { + renderWithProviders(, { + preloadedState: { + fileListing: { + displayMode: ListingMode.LIST, + files: dummyFiles, + }, + }, + }); + + expect(screen.getAllByTestId('file-row')).toHaveLength(3); + expect(screen.getByText('Für Alice')).toBeInTheDocument(); +}); diff --git a/src/features/file-listing/__tests__/FileRow.test.tsx b/src/features/file-listing/__tests__/FileRow.test.tsx new file mode 100644 index 0000000..4cc497e --- /dev/null +++ b/src/features/file-listing/__tests__/FileRow.test.tsx @@ -0,0 +1,24 @@ +import { renderWithProviders, screen } from '~/test-utils/test-helper'; +import { untouchedFile } from './__fixture__/file-list'; +import { FileRow } from '../FileRow'; +import { completedFile } from './__fixture__/file-list'; + +test('should render no metadata when unavailable', () => { + renderWithProviders(); + + expect(screen.getAllByTestId('file-row')).toHaveLength(1); + expect(screen.getByTestId('audio-meta-song-name')).toHaveTextContent('ready'); + expect(screen.queryByTestId('audio-meta-album-name')).toBeFalsy(); + expect(screen.queryByTestId('audio-meta-song-artist')).toBeFalsy(); + expect(screen.queryByTestId('audio-meta-album-artist')).toBeFalsy(); +}); + +test('should render metadata when file has been processed', () => { + renderWithProviders(); + + expect(screen.getAllByTestId('file-row')).toHaveLength(1); + expect(screen.getByTestId('audio-meta-song-name')).toHaveTextContent('Für Alice'); + expect(screen.getByTestId('audio-meta-album-name')).toHaveTextContent("NOW That's What I Call Cryptography 2023"); + expect(screen.getByTestId('audio-meta-song-artist')).toHaveTextContent('Jixun'); + expect(screen.getByTestId('audio-meta-album-artist')).toHaveTextContent('Cipher Lovers'); +}); diff --git a/src/features/file-listing/__tests__/__fixture__/file-list.ts b/src/features/file-listing/__tests__/__fixture__/file-list.ts new file mode 100644 index 0000000..89669b7 --- /dev/null +++ b/src/features/file-listing/__tests__/__fixture__/file-list.ts @@ -0,0 +1,43 @@ +import { DecryptedAudioFile, ProcessState } from '../../fileListingSlice'; + +export const untouchedFile: DecryptedAudioFile = { + fileName: 'ready.bin', + raw: 'blob://localhost/file-a', + decrypted: '', + ext: '', + state: ProcessState.UNTOUCHED, + errorMessage: null, + metadata: null, +}; + +export const completedFile: DecryptedAudioFile = { + fileName: 'hello-b.bin', + raw: 'blob://localhost/file-b', + decrypted: 'blob://localhost/file-b-decrypted', + ext: 'flac', + state: ProcessState.COMPLETE, + errorMessage: null, + metadata: { + name: 'Für Alice', + artist: 'Jixun', + albumArtist: 'Cipher Lovers', + album: "NOW That's What I Call Cryptography 2023", + cover: '', + }, +}; + +export const fileWithError: DecryptedAudioFile = { + fileName: 'hello-c.bin', + raw: 'blob://localhost/file-c', + decrypted: 'blob://localhost/file-c-decrypted', + ext: 'flac', + state: ProcessState.ERROR, + errorMessage: 'Could not decrypt blah blah', + metadata: null, +}; + +export const dummyFiles: Record = { + 'file://untouched': untouchedFile, + 'file://completed': completedFile, + 'file://error': fileWithError, +}; diff --git a/src/features/file-listing/fileListingSlice.ts b/src/features/file-listing/fileListingSlice.ts index 9d3a1c4..73faf5f 100644 --- a/src/features/file-listing/fileListingSlice.ts +++ b/src/features/file-listing/fileListingSlice.ts @@ -31,7 +31,7 @@ export interface DecryptedAudioFile { decrypted: string; // blob uri state: ProcessState; errorMessage: null | string; - metadata: AudioMetadata; + metadata: null | AudioMetadata; } export interface FileListingState { @@ -69,13 +69,7 @@ export const fileListingSlice = createSlice({ ext: '', state: ProcessState.UNTOUCHED, errorMessage: null, - metadata: { - name: '', - artist: '', - album: '', - albumArtist: '', - cover: '', - }, + metadata: null, }; }, setDecryptedContent: (state, { payload }: PayloadAction<{ id: string; decryptedBlobURI: string }>) => { diff --git a/src/main.tsx b/src/main.tsx index edd90f0..dad48ae 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,9 +4,12 @@ import App from './App'; import { ChakraProvider } from '@chakra-ui/react'; import { Provider } from 'react-redux'; -import { store } from './store'; +import { setupStore } from './store'; import { theme } from './theme'; +// Private to this file only. +const store = setupStore(); + ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( diff --git a/src/store.ts b/src/store.ts index 62584b0..542db54 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,11 +1,16 @@ -import { configureStore } from '@reduxjs/toolkit'; -import fileListing from './features/file-listing/fileListingSlice'; +import { PreloadedState, combineReducers, configureStore } from '@reduxjs/toolkit'; +import fileListingReducer from './features/file-listing/fileListingSlice'; -export const store = configureStore({ - reducer: { - fileListing: fileListing, - }, +const rootReducer = combineReducers({ + fileListing: fileListingReducer, }); -export type RootState = ReturnType; -export type AppDispatch = typeof store.dispatch; +export const setupStore = (preloadedState?: PreloadedState) => + configureStore({ + reducer: rootReducer, + preloadedState, + }); + +export type RootState = ReturnType; +export type AppStore = ReturnType; +export type AppDispatch = AppStore['dispatch']; diff --git a/src/test-utils/setup-jest.ts b/src/test-utils/setup-jest.ts index 7b0828b..6872c76 100644 --- a/src/test-utils/setup-jest.ts +++ b/src/test-utils/setup-jest.ts @@ -1 +1,16 @@ import '@testing-library/jest-dom'; + +// FIXME: Use something like jsdom-worker? +// see: https://github.com/developit/jsdom-worker +if (!global.Worker) { + (global as any).Worker = class MockWorker { + events: Record void> = Object.create(null); + + onmessage?: () => {}; + addEventListener(name: string, e: unknown) { + if (Object.hasOwn(this.events, name)) { + this.events[name](e); + } + } + }; +} diff --git a/src/test-utils/test-helper.tsx b/src/test-utils/test-helper.tsx new file mode 100644 index 0000000..9968a97 --- /dev/null +++ b/src/test-utils/test-helper.tsx @@ -0,0 +1,25 @@ +import { PreloadedState } from '@reduxjs/toolkit'; +import { RenderOptions, render } from '@testing-library/react'; +import { PropsWithChildren } from 'react'; +import { Provider } from 'react-redux'; +import { AppStore, RootState, setupStore } from '~/store'; + +// Adapted from: https://redux.js.org/usage/writing-tests + +export * from '@testing-library/react'; + +export interface ExtendedRenderOptions extends RenderOptions { + preloadedState?: PreloadedState; + store?: AppStore; +} + +export function renderWithProviders( + ui: React.ReactElement, + { preloadedState = {}, store = setupStore(preloadedState), ...renderOptions }: ExtendedRenderOptions = {} +) { + function Wrapper({ children }: PropsWithChildren<{}>): JSX.Element { + return {children}; + } + + return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) }; +} diff --git a/tsconfig.json b/tsconfig.json index c79a75c..f27c612 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,9 +21,10 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "baseUrl": ".", + "esModuleInterop": true, "paths": { "~/*": [ - "src/*" + "./src/*" ] }, }, @@ -35,4 +36,4 @@ "path": "./tsconfig.node.json" } ] -} \ No newline at end of file +} diff --git a/tsconfig.node.json b/tsconfig.node.json index 42872c5..b5a3431 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -6,5 +6,7 @@ "moduleResolution": "bundler", "allowSyntheticDefaultImports": true }, - "include": ["vite.config.ts"] + "include": [ + "vite.config.ts" + ] } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..043a3cf --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,39 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + mockReset: true, + environment: 'jsdom', + setupFiles: ['src/test-utils/setup-jest.ts'], + alias: [ + { + find: /^~\/(.*)/, + replacement: 'src/$1', + }, + ], + api: { + port: 5174, // vite port + 1 + }, + coverage: { + exclude: [ + // default rules + 'coverage/**', + 'dist/**', + 'packages/*/test{,s}/**', + '**/*.d.ts', + 'cypress/**', + 'test{,s}/**', + 'test{,-*}.{js,cjs,mjs,ts,tsx,jsx}', + '**/*{.,-}test.{js,cjs,mjs,ts,tsx,jsx}', + '**/*{.,-}spec.{js,cjs,mjs,ts,tsx,jsx}', + '**/__tests__/**', + '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*', + '**/.{eslint,mocha,prettier}rc.{js,cjs,yml}', + + // custom ones + 'src/test-utils/**', + ], + }, + }, +});