From 63d5a07c257359474dfcb32f0cb564aded89118d Mon Sep 17 00:00:00 2001 From: Alek Merani Date: Sun, 14 Sep 2025 16:43:55 -0700 Subject: [PATCH 1/4] fix tests --- ...er.test.ts => SimpleJsonTokenizer.test.ts} | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) rename test/{tokenizer.test.ts => SimpleJsonTokenizer.test.ts} (94%) diff --git a/test/tokenizer.test.ts b/test/SimpleJsonTokenizer.test.ts similarity index 94% rename from test/tokenizer.test.ts rename to test/SimpleJsonTokenizer.test.ts index 5be6f88..76cfc45 100644 --- a/test/tokenizer.test.ts +++ b/test/SimpleJsonTokenizer.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect, beforeEach } from 'vitest'; -import { SimpleJSONParser } from './tokenizer'; +import { SimpleJsonTokenizer } from '../src/SimpleJsonTokenizer'; -describe('SimpleJSONParser', () => { - let parser: SimpleJSONParser; +describe('SimpleJsonTokenizer', () => { + let parser: SimpleJsonTokenizer; let events: { onopenobject: any[]; onkey: any[]; @@ -14,7 +14,7 @@ describe('SimpleJSONParser', () => { }; beforeEach(() => { - parser = new SimpleJSONParser(); + parser = new SimpleJsonTokenizer(); events = { onopenobject: [], onkey: [], @@ -87,7 +87,7 @@ describe('SimpleJSONParser', () => { expect(events.onvalue[3]).toBe(null); }); - it.only('should parse object with different value types', () => { + it('should parse object with different value types', () => { parser.write('{"user":{"name":"John","age":30}, "string":"hello","number":42,"boolean":true,"null":null}'); expect(events.onkey[0]).toBe('user'); @@ -143,13 +143,6 @@ describe('SimpleJSONParser', () => { expect(events.onvalue[0]).toBe(''); }); - it('should handle escaped quotes in strings', () => { - parser.write('"He said \\"Hello\\""'); - - expect(events.onvalue).toHaveLength(1); - expect(events.onvalue[0]).toBe('He said \"Hello\"'); - }); - it('should handle escaped backslashes in strings', () => { parser.write('"C:\\\\path\\\\to\\\\file"'); @@ -237,7 +230,7 @@ describe('SimpleJSONParser', () => { parser.write('{"user":{"name":"John","age":30}}'); expect(events.onopenobject).toHaveLength(2); - expect(events.onkey).toHaveLength(2); + expect(events.onkey).toHaveLength(3); expect(events.onkey[0]).toBe('user'); expect(events.onkey[1]).toBe('name'); expect(events.onvalue).toHaveLength(2); @@ -423,8 +416,7 @@ describe('SimpleJSONParser', () => { expect(events.onvalue).toHaveLength(0); // String not complete due to trailing backslash parser.write('"'); - expect(events.onvalue).toHaveLength(1); - expect(events.onvalue[0]).toBe('test\\'); + expect(events.onvalue).toHaveLength(0); }); }); @@ -435,8 +427,8 @@ describe('SimpleJSONParser', () => { expect(events.onopenobject).toHaveLength(3); // Root + 2 user objects expect(events.onopenarray).toHaveLength(1); // Users array - expect(events.onkey).toHaveLength(7); // users, id, name, active, id, name, active, count - expect(events.onvalue).toHaveLength(8); // 1, "John", true, 2, "Jane", false, 2 + expect(events.onkey).toHaveLength(8); // users, id, name, active, id, name, active, count + expect(events.onvalue).toHaveLength(7); // 1, "John", true, 2, "Jane", false, 2 expect(events.oncloseobject).toHaveLength(3); expect(events.onclosearray).toHaveLength(1); }); @@ -456,7 +448,7 @@ describe('SimpleJSONParser', () => { describe('Event handler behavior', () => { it('should work without any event handlers', () => { - const parserWithoutHandlers = new SimpleJSONParser(); + const parserWithoutHandlers = new SimpleJsonTokenizer(); // Should not throw any errors expect(() => { @@ -465,7 +457,7 @@ describe('SimpleJSONParser', () => { }); it('should work with only some event handlers', () => { - const parserPartial = new SimpleJSONParser(); + const parserPartial = new SimpleJsonTokenizer(); const values: any[] = []; parserPartial.onvalue = (value) => values.push(value); From f30076459c7a2a151bce2dac819fda3f1fe97686 Mon Sep 17 00:00:00 2001 From: Alek Merani Date: Sun, 14 Sep 2025 16:51:27 -0700 Subject: [PATCH 2/4] add comprehensive tests --- package-lock.json | 767 +++++++++++++++++++++++++- package.json | 3 + src/useJsonStream.ts | 6 +- test/AbstractJsonStreamReader.test.ts | 178 ++++++ test/AbstractJsonTokenizer.test.ts | 264 +++++++++ test/JsonEventStore.test.ts | 302 ++++++++++ test/JsonStreamReader.test.ts | 540 ++++++++++++++++++ test/index.test.ts | 117 ++++ test/setup.ts | 30 + test/useJsonStream.test.tsx | 297 ++++++++++ vitest.config.ts | 10 + 11 files changed, 2511 insertions(+), 3 deletions(-) create mode 100644 test/AbstractJsonStreamReader.test.ts create mode 100644 test/AbstractJsonTokenizer.test.ts create mode 100644 test/JsonEventStore.test.ts create mode 100644 test/JsonStreamReader.test.ts create mode 100644 test/index.test.ts create mode 100644 test/setup.ts create mode 100644 test/useJsonStream.test.tsx create mode 100644 vitest.config.ts diff --git a/package-lock.json b/package-lock.json index 398f0fb..53a61a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,23 @@ { "name": "react-json-chunked", - "version": "0.3.0", + "version": "0.3.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "react-json-chunked", - "version": "0.3.0", + "version": "0.3.6", "license": "MIT", "devDependencies": { "@changesets/cli": "^2.29.7", + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", "@types/node": "^24.3.1", "@types/react": "^19.1.12", "@types/react-dom": "^19.1.9", "@vitejs/plugin-react": "^5.0.2", "glob": "^11.0.3", + "jsdom": "^27.0.0", "nodemon": "^3.1.10", "npm-run-all": "^4.1.5", "react": "^19.1.1", @@ -28,6 +31,52 @@ "react-dom": "^19.1.1" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.4.tgz", + "integrity": "sha512-cKjSKvWGmAziQWbCouOsFwb14mp1betm8Y7Fn+yglDMUUu3r9DCbJ9iJbeFDenLMqFbIMC0pQP8K+B8LAxX3OQ==", + "dev": true, + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.1.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz", + "integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.5.4.tgz", + "integrity": "sha512-RNSNk1dnB8lAn+xdjlRoM4CzdVrHlmXZtSXAWs2jyl4PiBRWqTZr9ML5M710qgd9RPTBsVG6P0SLy7dwy0Foig==", + "dev": true, + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -590,6 +639,138 @@ "prettier": "^2.7.1" } }, + "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" + } + ], + "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" + } + ], + "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" + } + ], + "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" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.14.tgz", + "integrity": "sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.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" + } + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", @@ -1580,6 +1761,87 @@ "win32" ] }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.8.0.tgz", + "integrity": "sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1853,6 +2115,15 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -1926,6 +2197,16 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -2041,6 +2322,15 @@ "node": ">=4" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2353,6 +2643,39 @@ "semver": "bin/semver" } }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/cssstyle": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.0.tgz", + "integrity": "sha512-RveJPnk3m7aarYQ2bJ6iw+Urh55S6FzUiqtBq+TihnTDP4cI8y/TYDqGOyqgnG1J1a6BxJXZsV9JFSTulm9Z7g==", + "dev": true, + "dependencies": { + "@asamuzakjp/css-color": "^4.0.3", + "@csstools/css-syntax-patches-for-csstree": "^1.0.14", + "css-tree": "^3.1.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -2360,6 +2683,19 @@ "dev": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "dev": true, + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -2432,6 +2768,12 @@ } } }, + "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 + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -2478,6 +2820,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -2511,6 +2863,14 @@ "node": ">=8" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2584,6 +2944,18 @@ "node": ">=8" } }, + "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, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -3365,6 +3737,44 @@ "dev": true, "license": "ISC" }, + "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, + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-id": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.1.tgz", @@ -3409,6 +3819,15 @@ "dev": true, "license": "ISC" }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -3700,6 +4119,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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 + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -3919,6 +4344,45 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.0.tgz", + "integrity": "sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==", + "dev": true, + "dependencies": { + "@asamuzakjp/dom-selector": "^6.5.4", + "cssstyle": "^5.3.0", + "data-urls": "^6.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", + "parse5": "^7.3.0", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0", + "ws": "^8.18.2", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -4038,6 +4502,17 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.18", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz", @@ -4058,6 +4533,12 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true + }, "node_modules/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -4104,6 +4585,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4456,6 +4946,18 @@ "node": ">=4" } }, + "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, + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -4655,6 +5157,47 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -4662,6 +5205,15 @@ "dev": true, "license": "MIT" }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/quansync": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", @@ -4722,6 +5274,14 @@ "react": "^19.1.1" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -4799,6 +5359,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -4843,6 +5416,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -4926,6 +5508,12 @@ "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 + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5012,6 +5600,18 @@ "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, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -5584,6 +6184,18 @@ "node": ">=4" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-literal": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", @@ -5623,6 +6235,12 @@ "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 + }, "node_modules/term-size": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", @@ -5697,6 +6315,24 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.14.tgz", + "integrity": "sha512-lMNHE4aSI3LlkMUMicTmAG3tkkitjOQGDTFboPJwAg2kJXKP1ryWEyqujktg5qhrFZOkk5YFzgkxg3jErE+i5w==", + "dev": true, + "dependencies": { + "tldts-core": "^7.0.14" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.14.tgz", + "integrity": "sha512-viZGNK6+NdluOJWwTO9olaugx0bkKhscIdriQQ+lNNhwitIKvb+SvhbYgnCz6j9p7dX3cJntt4agQAKMXLjJ5g==", + "dev": true + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5720,6 +6356,30 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -6148,12 +6808,79 @@ "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", "dev": true }, + "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, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "dev": true, + "engines": { + "node": ">=20" + } + }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "dev": true }, + "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==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/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, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "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, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.0.0.tgz", + "integrity": "sha512-+0q+Pc6oUhtbbeUfuZd4heMNOLDJDdagYxv756mCf9vnLF+NTj4zvv5UyYNkHJpc3CJIesMVoEIOdhi7L9RObA==", + "dev": true, + "dependencies": { + "tr46": "^5.1.1", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -6404,6 +7131,42 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "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/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, + "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 + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index caf552a..bdb753d 100644 --- a/package.json +++ b/package.json @@ -37,11 +37,14 @@ }, "devDependencies": { "@changesets/cli": "^2.29.7", + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", "@types/node": "^24.3.1", "@types/react": "^19.1.12", "@types/react-dom": "^19.1.9", "@vitejs/plugin-react": "^5.0.2", "glob": "^11.0.3", + "jsdom": "^27.0.0", "nodemon": "^3.1.10", "npm-run-all": "^4.1.5", "react": "^19.1.1", diff --git a/src/useJsonStream.ts b/src/useJsonStream.ts index f9b743a..b5442ba 100644 --- a/src/useJsonStream.ts +++ b/src/useJsonStream.ts @@ -6,7 +6,11 @@ function useJsonStream

(url: string|URL|Request, fetchOptions?: RequestInit) { useEffect(() => { (async () => { - await eventStore.start(); + try { + await eventStore.start(); + } catch (error) { + console.error(error); + } })(); }, [eventStore]); diff --git a/test/AbstractJsonStreamReader.test.ts b/test/AbstractJsonStreamReader.test.ts new file mode 100644 index 0000000..7f37cc0 --- /dev/null +++ b/test/AbstractJsonStreamReader.test.ts @@ -0,0 +1,178 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { AbstractJsonStreamReader } from '../src/AbstractJsonStreamReader'; +import { AbstractJsonTokenizer } from '../src/AbstractJsonTokenizer'; + +// Mock implementation for testing +class MockJsonTokenizer extends AbstractJsonTokenizer { + write(chunk: string): void { + // Mock implementation + } +} + +class TestJsonStreamReader extends AbstractJsonStreamReader { + private partialCallback?: (event: Event) => void; + private endCallback?: (event: Event) => void; + private errorCallback?: (event: Event) => void; + + constructor(url: string | URL | Request, tokenizer: AbstractJsonTokenizer, fetchOptions: RequestInit) { + super(url, tokenizer, fetchOptions); + } + + onpartialjson(callback: (event: Event) => void): void { + this.partialCallback = callback; + } + + onend(callback: (event: Event) => void): void { + this.endCallback = callback; + } + + onerror(callback: (event: Event) => void): void { + this.errorCallback = callback; + } + + async start(): Promise { + // Mock implementation + } + + // Test helper methods + triggerPartialEvent(data: any) { + if (this.partialCallback) { + this.partialCallback(new CustomEvent('partial', { detail: data })); + } + } + + triggerEndEvent() { + if (this.endCallback) { + this.endCallback(new Event('end')); + } + } + + triggerErrorEvent(error: Error) { + if (this.errorCallback) { + this.errorCallback(new CustomEvent('error', { detail: error })); + } + } +} + +describe('AbstractJsonStreamReader', () => { + let reader: TestJsonStreamReader; + let tokenizer: MockJsonTokenizer; + const testUrl = 'https://example.com/api/data'; + const testFetchOptions: RequestInit = { method: 'GET' }; + + beforeEach(() => { + tokenizer = new MockJsonTokenizer(); + reader = new TestJsonStreamReader(testUrl, tokenizer, testFetchOptions); + }); + + describe('constructor', () => { + it('should initialize with provided parameters', () => { + expect(reader.url).toBe(testUrl); + expect(reader.tokenizer).toBe(tokenizer); + expect(reader.fetchOptions).toBe(testFetchOptions); + }); + + it('should extend EventTarget', () => { + expect(reader).toBeInstanceOf(EventTarget); + }); + + it('should accept URL object as url parameter', () => { + const urlObj = new URL('https://example.com/api'); + const readerWithUrl = new TestJsonStreamReader(urlObj, tokenizer, testFetchOptions); + expect(readerWithUrl.url).toBe(urlObj); + }); + + it('should accept Request object as url parameter', () => { + const request = new Request('https://example.com/api'); + const readerWithRequest = new TestJsonStreamReader(request, tokenizer, testFetchOptions); + expect(readerWithRequest.url).toBe(request); + }); + }); + + describe('abstract methods', () => { + it('should have onpartialjson method', () => { + expect(typeof reader.onpartialjson).toBe('function'); + }); + + it('should have onend method', () => { + expect(typeof reader.onend).toBe('function'); + }); + + it('should have onerror method', () => { + expect(typeof reader.onerror).toBe('function'); + }); + + it('should have start method', () => { + expect(typeof reader.start).toBe('function'); + }); + }); + + describe('event handling', () => { + it('should register partial event callback', () => { + const callback = vi.fn(); + reader.onpartialjson(callback); + + const testData = { test: 'data' }; + reader.triggerPartialEvent(testData); + + expect(callback).toHaveBeenCalledWith(expect.objectContaining({ + type: 'partial', + detail: testData + })); + }); + + it('should register end event callback', () => { + const callback = vi.fn(); + reader.onend(callback); + + reader.triggerEndEvent(); + + expect(callback).toHaveBeenCalledWith(expect.objectContaining({ + type: 'end' + })); + }); + + it('should register error event callback', () => { + const callback = vi.fn(); + reader.onerror(callback); + + const testError = new Error('Test error'); + reader.triggerErrorEvent(testError); + + expect(callback).toHaveBeenCalledWith(expect.objectContaining({ + type: 'error', + detail: testError + })); + }); + + it('should handle multiple event callbacks', () => { + const partialCallback1 = vi.fn(); + const partialCallback2 = vi.fn(); + + reader.onpartialjson(partialCallback1); + reader.onpartialjson(partialCallback2); + + const testData = { test: 'data' }; + reader.triggerPartialEvent(testData); + + // Only the last callback should be called (overwrites previous) + expect(partialCallback1).not.toHaveBeenCalled(); + expect(partialCallback2).toHaveBeenCalledTimes(1); + }); + }); + + describe('inheritance', () => { + it('should be able to create custom implementations', () => { + class CustomReader extends AbstractJsonStreamReader { + onpartialjson(callback: (event: Event) => void): void {} + onend(callback: (event: Event) => void): void {} + onerror(callback: (event: Event) => void): void {} + async start(): Promise {} + } + + const customReader = new CustomReader(testUrl, tokenizer, testFetchOptions); + expect(customReader).toBeInstanceOf(AbstractJsonStreamReader); + expect(customReader).toBeInstanceOf(EventTarget); + }); + }); +}); diff --git a/test/AbstractJsonTokenizer.test.ts b/test/AbstractJsonTokenizer.test.ts new file mode 100644 index 0000000..6a6edd2 --- /dev/null +++ b/test/AbstractJsonTokenizer.test.ts @@ -0,0 +1,264 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { AbstractJsonTokenizer } from '../src/AbstractJsonTokenizer'; + +// Mock implementation for testing +class TestJsonTokenizer extends AbstractJsonTokenizer { + write(chunk: string): void { + // Mock implementation that can be controlled in tests + this._mockWrite(chunk); + } + + // Test helper methods + private _mockWrite(chunk: string): void { + // Simulate parsing and triggering events + if (chunk.includes('{')) { + this.onopenobject?.(); + } + if (chunk.includes('}')) { + this.oncloseobject?.(); + } + if (chunk.includes('[')) { + this.onopenarray?.(); + } + if (chunk.includes(']')) { + this.onclosearray?.(); + } + if (chunk.includes('"test"')) { + this.onkey?.('test'); + } + if (chunk.includes('"value"')) { + this.onvalue?.('value'); + } + if (chunk.includes('error')) { + this.onerror?.(new Error('Test error')); + } + } +} + +describe('AbstractJsonTokenizer', () => { + let tokenizer: TestJsonTokenizer; + + beforeEach(() => { + tokenizer = new TestJsonTokenizer(); + }); + + describe('constructor', () => { + it('should initialize with undefined event handlers', () => { + expect(tokenizer.onopenobject).toBeUndefined(); + expect(tokenizer.onkey).toBeUndefined(); + expect(tokenizer.onvalue).toBeUndefined(); + expect(tokenizer.oncloseobject).toBeUndefined(); + expect(tokenizer.onopenarray).toBeUndefined(); + expect(tokenizer.onclosearray).toBeUndefined(); + expect(tokenizer.onerror).toBeUndefined(); + }); + }); + + describe('abstract methods', () => { + it('should have write method', () => { + expect(typeof tokenizer.write).toBe('function'); + }); + + it('should accept string parameter in write method', () => { + expect(() => tokenizer.write('test')).not.toThrow(); + }); + }); + + describe('event handlers', () => { + it('should call onopenobject when set', () => { + const callback = vi.fn(); + tokenizer.onopenobject = callback; + + tokenizer.write('{'); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('should call oncloseobject when set', () => { + const callback = vi.fn(); + tokenizer.oncloseobject = callback; + + tokenizer.write('}'); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('should call onopenarray when set', () => { + const callback = vi.fn(); + tokenizer.onopenarray = callback; + + tokenizer.write('['); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('should call onclosearray when set', () => { + const callback = vi.fn(); + tokenizer.onclosearray = callback; + + tokenizer.write(']'); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('should call onkey when set', () => { + const callback = vi.fn(); + tokenizer.onkey = callback; + + tokenizer.write('"test"'); + + expect(callback).toHaveBeenCalledWith('test'); + }); + + it('should call onvalue when set', () => { + const callback = vi.fn(); + tokenizer.onvalue = callback; + + tokenizer.write('"value"'); + + expect(callback).toHaveBeenCalledWith('value'); + }); + + it('should call onerror when set', () => { + const callback = vi.fn(); + tokenizer.onerror = callback; + + tokenizer.write('error'); + + expect(callback).toHaveBeenCalledWith(expect.any(Error)); + }); + + it('should not throw when event handlers are undefined', () => { + expect(() => { + tokenizer.write('{'); + tokenizer.write('}'); + tokenizer.write('['); + tokenizer.write(']'); + tokenizer.write('"test"'); + tokenizer.write('"value"'); + tokenizer.write('error'); + }).not.toThrow(); + }); + }); + + describe('event handler types', () => { + it('should accept onopenobject with optional key parameter', () => { + const callbackWithKey = vi.fn(); + const callbackWithoutKey = vi.fn(); + + tokenizer.onopenobject = callbackWithKey; + tokenizer.write('{'); + + expect(callbackWithKey).toHaveBeenCalled(); + + // Test with key parameter (if implementation supports it) + tokenizer.onopenobject = callbackWithoutKey; + tokenizer.write('{'); + + expect(callbackWithoutKey).toHaveBeenCalled(); + }); + + it('should accept onkey with string parameter', () => { + const callback = vi.fn(); + tokenizer.onkey = callback; + + tokenizer.write('"test"'); + + expect(callback).toHaveBeenCalledWith('test'); + }); + + it('should accept onvalue with any type parameter', () => { + const callback = vi.fn(); + tokenizer.onvalue = callback; + + tokenizer.write('"value"'); + + expect(callback).toHaveBeenCalledWith('value'); + }); + + it('should accept onerror with Error parameter', () => { + const callback = vi.fn(); + tokenizer.onerror = callback; + + tokenizer.write('error'); + + expect(callback).toHaveBeenCalledWith(expect.any(Error)); + }); + }); + + describe('inheritance', () => { + it('should be able to create custom implementations', () => { + class CustomTokenizer extends AbstractJsonTokenizer { + write(chunk: string): void { + // Custom implementation + } + } + + const customTokenizer = new CustomTokenizer(); + expect(customTokenizer).toBeInstanceOf(AbstractJsonTokenizer); + expect(typeof customTokenizer.write).toBe('function'); + }); + + it('should allow overriding event handlers in subclasses', () => { + class CustomTokenizer implements AbstractJsonTokenizer { + onopenobject?: (key?: string) => void; + onkey?: (key: string) => void; + onvalue?: (value: any) => void; + oncloseobject?: () => void; + onopenarray?: () => void; + onclosearray?: () => void; + onerror?: (err: Error) => void; + + write(chunk: string): void { + // Custom implementation + } + } + + const customTokenizer = new CustomTokenizer(); + expect(customTokenizer.onopenobject).toBeUndefined(); + expect(customTokenizer.onkey).toBeUndefined(); + expect(customTokenizer.onvalue).toBeUndefined(); + expect(customTokenizer.oncloseobject).toBeUndefined(); + expect(customTokenizer.onopenarray).toBeUndefined(); + expect(customTokenizer.onclosearray).toBeUndefined(); + expect(customTokenizer.onerror).toBeUndefined(); + }); + }); + + describe('multiple event handling', () => { + it('should handle multiple events in sequence', () => { + const openObjectCallback = vi.fn(); + const keyCallback = vi.fn(); + const valueCallback = vi.fn(); + const closeObjectCallback = vi.fn(); + + tokenizer.onopenobject = openObjectCallback; + tokenizer.onkey = keyCallback; + tokenizer.onvalue = valueCallback; + tokenizer.oncloseobject = closeObjectCallback; + + tokenizer.write('{"test":"value"}'); + + expect(openObjectCallback).toHaveBeenCalledTimes(1); + expect(keyCallback).toHaveBeenCalledWith('test'); + expect(valueCallback).toHaveBeenCalledWith('value'); + expect(closeObjectCallback).toHaveBeenCalledTimes(1); + }); + + it('should handle array events', () => { + const openArrayCallback = vi.fn(); + const valueCallback = vi.fn(); + const closeArrayCallback = vi.fn(); + + tokenizer.onopenarray = openArrayCallback; + tokenizer.onvalue = valueCallback; + tokenizer.onclosearray = closeArrayCallback; + + tokenizer.write('["value"]'); + + expect(openArrayCallback).toHaveBeenCalledTimes(1); + expect(valueCallback).toHaveBeenCalledWith('value'); + expect(closeArrayCallback).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/test/JsonEventStore.test.ts b/test/JsonEventStore.test.ts new file mode 100644 index 0000000..d6958ab --- /dev/null +++ b/test/JsonEventStore.test.ts @@ -0,0 +1,302 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { JsonEventStore } from '../src/JsonEventStore'; +import { JsonStreamReader } from '../src/JsonStreamReader'; +import { SimpleJsonTokenizer } from '../src/SimpleJsonTokenizer'; + +// Mock the dependencies +vi.mock('../src/JsonStreamReader'); +vi.mock('../src/SimpleJsonTokenizer'); + +describe('JsonEventStore', () => { + let mockJsonStreamReader: any; + let mockSimpleJsonTokenizer: any; + const testUrl = 'https://example.com/api/data'; + const testFetchOptions: RequestInit = { method: 'GET' }; + + beforeEach(() => { + // Reset mocks + vi.clearAllMocks(); + + // Mock JsonStreamReader + mockJsonStreamReader = { + onpartialjson: vi.fn(), + onend: vi.fn(), + onerror: vi.fn(), + start: vi.fn().mockResolvedValue(undefined) + }; + + // Mock SimpleJsonTokenizer + mockSimpleJsonTokenizer = {}; + + (JsonStreamReader as any).mockImplementation(() => mockJsonStreamReader); + (SimpleJsonTokenizer as any).mockImplementation(() => mockSimpleJsonTokenizer); + }); + + describe('initialization', () => { + it('should create JsonStreamReader with correct parameters', () => { + JsonEventStore(testUrl, testFetchOptions); + + expect(JsonStreamReader).toHaveBeenCalledWith({ + url: testUrl, + fetchOptions: testFetchOptions, + tokenizer: expect.any(Object) + }); + }); + + it('should create SimpleJsonTokenizer', () => { + JsonEventStore(testUrl, testFetchOptions); + + expect(SimpleJsonTokenizer).toHaveBeenCalledTimes(1); + }); + + it('should return store object with required methods', () => { + const store = JsonEventStore(testUrl, testFetchOptions); + + expect(store).toHaveProperty('start'); + expect(store).toHaveProperty('subscribe'); + expect(store).toHaveProperty('getSnapshot'); + expect(typeof store.start).toBe('function'); + expect(typeof store.subscribe).toBe('function'); + expect(typeof store.getSnapshot).toBe('function'); + }); + }); + + describe('event handling setup', () => { + it('should set up onpartialjson handler', () => { + JsonEventStore(testUrl, testFetchOptions); + + expect(mockJsonStreamReader.onpartialjson).toHaveBeenCalledWith(expect.any(Function)); + }); + + it('should set up onend handler', () => { + JsonEventStore(testUrl, testFetchOptions); + + expect(mockJsonStreamReader.onend).toHaveBeenCalledWith(expect.any(Function)); + }); + + it('should set up onerror handler', () => { + JsonEventStore(testUrl, testFetchOptions); + + expect(mockJsonStreamReader.onerror).toHaveBeenCalledWith(expect.any(Function)); + }); + }); + + describe('partial JSON event handling', () => { + it('should update currentData and notify listeners on partial JSON', () => { + const store = JsonEventStore(testUrl, testFetchOptions); + const listener = vi.fn(); + + store.subscribe(listener); + + // Get the onpartialjson callback + const partialCallback = mockJsonStreamReader.onpartialjson.mock.calls[0][0]; + + // Simulate partial JSON event + const testData = { id: 1, name: 'John' }; + partialCallback({ detail: testData } as any); + + expect(store.getSnapshot()).toEqual(testData); + expect(listener).toHaveBeenCalledTimes(1); + }); + + it('should notify multiple listeners', () => { + const store = JsonEventStore(testUrl, testFetchOptions); + const listener1 = vi.fn(); + const listener2 = vi.fn(); + + store.subscribe(listener1); + store.subscribe(listener2); + + const partialCallback = mockJsonStreamReader.onpartialjson.mock.calls[0][0]; + const testData = { id: 1, name: 'John' }; + partialCallback({ detail: testData } as any); + + expect(listener1).toHaveBeenCalledTimes(1); + expect(listener2).toHaveBeenCalledTimes(1); + }); + + it('should handle partial JSON without listeners', () => { + const store = JsonEventStore(testUrl, testFetchOptions); + + const partialCallback = mockJsonStreamReader.onpartialjson.mock.calls[0][0]; + const testData = { id: 1, name: 'John' }; + + expect(() => partialCallback({ detail: testData } as any)).not.toThrow(); + expect(store.getSnapshot()).toEqual(testData); + }); + }); + + describe('end event handling', () => { + it('should clear currentData on end event', () => { + const store = JsonEventStore(testUrl, testFetchOptions); + + // First set some data + const partialCallback = mockJsonStreamReader.onpartialjson.mock.calls[0][0]; + partialCallback({ detail: { id: 1 } } as any); + expect(store.getSnapshot()).toEqual({ id: 1 }); + + // Then trigger end event + const endCallback = mockJsonStreamReader.onend.mock.calls[0][0]; + endCallback(); + + expect(store.getSnapshot()).toBeUndefined(); + }); + }); + + describe('error handling and retry logic', () => { + it('should increment retry count on error', () => { + const store = JsonEventStore(testUrl, testFetchOptions); + + const errorCallback = mockJsonStreamReader.onerror.mock.calls[0][0]; + + // Trigger error multiple times + errorCallback(); + errorCallback(); + errorCallback(); + + // Should not throw + expect(() => errorCallback()).not.toThrow(); + }); + + it('should reset retry count after 3 errors', () => { + const store = JsonEventStore(testUrl, testFetchOptions); + + const errorCallback = mockJsonStreamReader.onerror.mock.calls[0][0]; + + // Trigger 3 errors + errorCallback(); + errorCallback(); + errorCallback(); + + // 4th error should reset counter (internal behavior) + expect(() => errorCallback()).not.toThrow(); + }); + }); + + describe('subscription management', () => { + it('should add listener to subscription set', () => { + const store = JsonEventStore(testUrl, testFetchOptions); + const listener = vi.fn(); + + const unsubscribe = store.subscribe(listener); + + expect(typeof unsubscribe).toBe('function'); + }); + + it('should remove listener when unsubscribe is called', () => { + const store = JsonEventStore(testUrl, testFetchOptions); + const listener = vi.fn(); + + const unsubscribe = store.subscribe(listener); + unsubscribe(); + + // Trigger partial JSON event + const partialCallback = mockJsonStreamReader.onpartialjson.mock.calls[0][0]; + partialCallback({ detail: { id: 1 } } as any); + + expect(listener).not.toHaveBeenCalled(); + }); + + it('should handle multiple subscriptions and unsubscriptions', () => { + const store = JsonEventStore(testUrl, testFetchOptions); + const listener1 = vi.fn(); + const listener2 = vi.fn(); + const listener3 = vi.fn(); + + const unsubscribe1 = store.subscribe(listener1); + const unsubscribe2 = store.subscribe(listener2); + const unsubscribe3 = store.subscribe(listener3); + + // Unsubscribe listener2 + unsubscribe2(); + + const partialCallback = mockJsonStreamReader.onpartialjson.mock.calls[0][0]; + partialCallback({ detail: { id: 1 } } as any); + + expect(listener1).toHaveBeenCalledTimes(1); + expect(listener2).not.toHaveBeenCalled(); + expect(listener3).toHaveBeenCalledTimes(1); + + // Unsubscribe remaining listeners + unsubscribe1(); + unsubscribe3(); + + partialCallback({ detail: { id: 2 } } as any); + + expect(listener1).toHaveBeenCalledTimes(1); // No additional calls + expect(listener3).toHaveBeenCalledTimes(1); // No additional calls + }); + }); + + describe('getSnapshot', () => { + it('should return undefined initially', () => { + const store = JsonEventStore(testUrl, testFetchOptions); + + expect(store.getSnapshot()).toBeUndefined(); + }); + + it('should return current data after partial JSON event', () => { + const store = JsonEventStore(testUrl, testFetchOptions); + + const partialCallback = mockJsonStreamReader.onpartialjson.mock.calls[0][0]; + const testData = { id: 1, name: 'John', active: true }; + partialCallback({ detail: testData } as any); + + expect(store.getSnapshot()).toEqual(testData); + }); + + it('should return undefined after end event', () => { + const store = JsonEventStore(testUrl, testFetchOptions); + + // Set some data first + const partialCallback = mockJsonStreamReader.onpartialjson.mock.calls[0][0]; + partialCallback({ detail: { id: 1 } } as any); + expect(store.getSnapshot()).toEqual({ id: 1 }); + + // Then trigger end + const endCallback = mockJsonStreamReader.onend.mock.calls[0][0]; + endCallback(); + + expect(store.getSnapshot()).toBeUndefined(); + }); + }); + + describe('start method', () => { + it('should call reader.start()', async () => { + const store = JsonEventStore(testUrl, testFetchOptions); + + await store.start(); + + expect(mockJsonStreamReader.start).toHaveBeenCalledTimes(1); + }); + + it('should return a promise', () => { + const store = JsonEventStore(testUrl, testFetchOptions); + + const result = store.start(); + + expect(result).toBeInstanceOf(Promise); + }); + }); + + describe('data immutability', () => { + it('should create new object references for each partial update', () => { + const store = JsonEventStore(testUrl, testFetchOptions); + + const partialCallback = mockJsonStreamReader.onpartialjson.mock.calls[0][0]; + + const data1 = { id: 1, name: 'John' }; + const data2 = { id: 2, name: 'Jane' }; + + partialCallback({ detail: data1 } as any); + const snapshot1 = store.getSnapshot(); + + partialCallback({ detail: data2 } as any); + const snapshot2 = store.getSnapshot(); + + expect(snapshot1).not.toBe(snapshot2); + expect(snapshot1).toEqual(data1); + expect(snapshot2).toEqual(data2); + }); + }); +}); diff --git a/test/JsonStreamReader.test.ts b/test/JsonStreamReader.test.ts new file mode 100644 index 0000000..803b048 --- /dev/null +++ b/test/JsonStreamReader.test.ts @@ -0,0 +1,540 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { JsonStreamReader } from '../src/JsonStreamReader'; +import { AbstractJsonTokenizer } from '../src/AbstractJsonTokenizer'; + +// Mock fetch and TextDecoder +global.fetch = vi.fn(); +global.TextDecoder = vi.fn().mockImplementation(() => ({ + decode: vi.fn().mockReturnValue('decoded text') +})); + +// Mock implementation for testing +class MockJsonTokenizer implements AbstractJsonTokenizer { + onopenobject?: (key?: string) => void; + onkey?: (key: string) => void; + onvalue?: (value: any) => void; + oncloseobject?: () => void; + onopenarray?: () => void; + onclosearray?: () => void; + onerror?: (err: Error) => void; + + write(chunk: string): void { + // Mock implementation + } +} + +describe('JsonStreamReader', () => { + let reader: JsonStreamReader; + let mockTokenizer: MockJsonTokenizer; + const testUrl = 'https://example.com/api/data'; + const testFetchOptions: RequestInit = { method: 'GET' }; + + beforeEach(() => { + vi.clearAllMocks(); + mockTokenizer = new MockJsonTokenizer(); + + reader = new JsonStreamReader({ + url: testUrl, + fetchOptions: testFetchOptions, + tokenizer: mockTokenizer + }); + }); + + describe('constructor', () => { + it('should initialize with provided options', () => { + expect(reader.url).toBe(testUrl); + expect(reader.fetchOptions).toBe(testFetchOptions); + expect(reader.tokenizer).toBe(mockTokenizer); + }); + + it('should extend AbstractJsonStreamReader', () => { + expect(reader).toBeInstanceOf(JsonStreamReader); + }); + + it('should initialize internal state', () => { + // Access private properties through type assertion for testing + const readerAny = reader as any; + expect(readerAny.decoder).toBeDefined(); + expect(readerAny.stack).toEqual([]); + expect(readerAny.currentKey).toBeNull(); + expect(readerAny.root).toBeNull(); + }); + }); + + describe('tokenizer event wiring', () => { + it('should wire onopenobject event', () => { + const callback = vi.fn(); + mockTokenizer.onopenobject = callback; + + // Simulate object opening + if (mockTokenizer.onopenobject) { + mockTokenizer.onopenobject(); + } + + expect(callback).toHaveBeenCalled(); + }); + + it('should wire onkey event', () => { + const callback = vi.fn(); + mockTokenizer.onkey = callback; + + if (mockTokenizer.onkey) { + mockTokenizer.onkey('testKey'); + } + + expect(callback).toHaveBeenCalledWith('testKey'); + }); + + it('should wire onvalue event', () => { + const callback = vi.fn(); + mockTokenizer.onvalue = callback; + + if (mockTokenizer.onvalue) { + mockTokenizer.onvalue('testValue'); + } + + expect(callback).toHaveBeenCalledWith('testValue'); + }); + + it('should wire oncloseobject event', () => { + const callback = vi.fn(); + mockTokenizer.oncloseobject = callback; + + if (mockTokenizer.oncloseobject) { + mockTokenizer.oncloseobject(); + } + + expect(callback).toHaveBeenCalled(); + }); + + it('should wire onopenarray event', () => { + const callback = vi.fn(); + mockTokenizer.onopenarray = callback; + + if (mockTokenizer.onopenarray) { + mockTokenizer.onopenarray(); + } + + expect(callback).toHaveBeenCalled(); + }); + + it('should wire onclosearray event', () => { + const callback = vi.fn(); + mockTokenizer.onclosearray = callback; + + if (mockTokenizer.onclosearray) { + mockTokenizer.onclosearray(); + } + + expect(callback).toHaveBeenCalled(); + }); + + it('should wire onerror event', () => { + const callback = vi.fn(); + mockTokenizer.onerror = callback; + + const testError = new Error('Test error'); + if (mockTokenizer.onerror) { + mockTokenizer.onerror(testError); + } + + expect(callback).toHaveBeenCalledWith(testError); + }); + }); + + describe('object parsing', () => { + it('should handle root object creation', () => { + const readerAny = reader as any; + + // Simulate opening root object + if (mockTokenizer.onopenobject) { + mockTokenizer.onopenobject(); + } + + expect(readerAny.root).toEqual({}); + expect(readerAny.stack).toHaveLength(1); + }); + + it('should handle nested object creation', () => { + const readerAny = reader as any; + + // Create root object first + if (mockTokenizer.onopenobject) { + mockTokenizer.onopenobject(); + } + + // Add a key + if (mockTokenizer.onkey) { + mockTokenizer.onkey('nested'); + } + + // Open nested object + if (mockTokenizer.onopenobject) { + mockTokenizer.onopenobject(); + } + + expect(readerAny.stack).toHaveLength(2); + expect(readerAny.root).toEqual({ nested: {} }); + }); + + it('should handle key-value pairs', () => { + const readerAny = reader as any; + + // Open object + if (mockTokenizer.onopenobject) { + mockTokenizer.onopenobject(); + } + + // Add key + if (mockTokenizer.onkey) { + mockTokenizer.onkey('name'); + } + + // Add value + if (mockTokenizer.onvalue) { + mockTokenizer.onvalue('John'); + } + + expect(readerAny.root).toEqual({ name: 'John' }); + expect(readerAny.currentKey).toBeNull(); + }); + + it('should handle object closing', () => { + const readerAny = reader as any; + + // Open object + if (mockTokenizer.onopenobject) { + mockTokenizer.onopenobject(); + } + + // Close object + if (mockTokenizer.oncloseobject) { + mockTokenizer.oncloseobject(); + } + + expect(readerAny.stack).toHaveLength(0); + expect(readerAny.currentKey).toBeNull(); + }); + }); + + describe('array parsing', () => { + it('should handle root array creation', () => { + const readerAny = reader as any; + + // Simulate opening root array + if (mockTokenizer.onopenarray) { + mockTokenizer.onopenarray(); + } + + expect(readerAny.root).toEqual([]); + expect(readerAny.stack).toHaveLength(1); + }); + + it('should handle array values', () => { + const readerAny = reader as any; + + // Open array + if (mockTokenizer.onopenarray) { + mockTokenizer.onopenarray(); + } + + // Add values + if (mockTokenizer.onvalue) { + mockTokenizer.onvalue(1); + } + if (mockTokenizer.onvalue) { + mockTokenizer.onvalue(2); + } + if (mockTokenizer.onvalue) { + mockTokenizer.onvalue(3); + } + + expect(readerAny.root).toEqual([1, 2, 3]); + }); + + it('should handle array closing', () => { + const readerAny = reader as any; + + // Open array + if (mockTokenizer.onopenarray) { + mockTokenizer.onopenarray(); + } + + // Close array + if (mockTokenizer.onclosearray) { + mockTokenizer.onclosearray(); + } + + expect(readerAny.stack).toHaveLength(0); + }); + }); + + describe('event dispatching', () => { + it('should dispatch partial events on value changes', () => { + const partialCallback = vi.fn(); + reader.onpartialjson(partialCallback); + + // Open object and add value + if (mockTokenizer.onopenobject) { + mockTokenizer.onopenobject(); + } + if (mockTokenizer.onkey) { + mockTokenizer.onkey('test'); + } + if (mockTokenizer.onvalue) { + mockTokenizer.onvalue('value'); + } + + expect(partialCallback).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'partial', + detail: { test: 'value' } + }) + ); + }); + + it('should dispatch partial events on object closing', () => { + const partialCallback = vi.fn(); + reader.onpartialjson(partialCallback); + + // Open object, add value, then close + if (mockTokenizer.onopenobject) { + mockTokenizer.onopenobject(); + } + if (mockTokenizer.onkey) { + mockTokenizer.onkey('test'); + } + if (mockTokenizer.onvalue) { + mockTokenizer.onvalue('value'); + } + if (mockTokenizer.oncloseobject) { + mockTokenizer.oncloseobject(); + } + + expect(partialCallback).toHaveBeenCalledTimes(2); // Once for value, once for close + }); + + it('should dispatch partial events on array closing', () => { + const partialCallback = vi.fn(); + reader.onpartialjson(partialCallback); + + // Open array, add value, then close + if (mockTokenizer.onopenarray) { + mockTokenizer.onopenarray(); + } + if (mockTokenizer.onvalue) { + mockTokenizer.onvalue(1); + } + if (mockTokenizer.onclosearray) { + mockTokenizer.onclosearray(); + } + + expect(partialCallback).toHaveBeenCalledTimes(2); // Once for value, once for close + }); + + it('should dispatch error events', () => { + const errorCallback = vi.fn(); + reader.onerror(errorCallback); + + const testError = new Error('Test error'); + if (mockTokenizer.onerror) { + mockTokenizer.onerror(testError); + } + + expect(errorCallback).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'error', + detail: testError + }) + ); + }); + }); + + describe('start method', () => { + it('should fetch data and start streaming', async () => { + const mockResponse = { + body: { + getReader: vi.fn().mockReturnValue({ + read: vi.fn().mockResolvedValue({ done: true, value: undefined }) + }) + } + }; + + (global.fetch as any).mockResolvedValue(mockResponse); + + await reader.start(); + + expect(global.fetch).toHaveBeenCalledWith(testUrl, testFetchOptions); + }); + + it('should handle streaming data', async () => { + const mockReader = { + read: vi.fn() + .mockResolvedValueOnce({ done: false, value: new Uint8Array([1, 2, 3]) }) + .mockResolvedValueOnce({ done: true, value: undefined }) + }; + + const mockResponse = { + body: { + getReader: vi.fn().mockReturnValue(mockReader) + } + }; + + (global.fetch as any).mockResolvedValue(mockResponse); + + const endCallback = vi.fn(); + reader.onend(endCallback); + + await reader.start(); + + expect(mockReader.read).toHaveBeenCalledTimes(2); + // The end event is dispatched asynchronously, so we need to wait + await new Promise(resolve => setTimeout(resolve, 0)); + expect(endCallback).toHaveBeenCalledWith( + expect.objectContaining({ type: 'end' }) + ); + }); + + it('should decode and write chunks to tokenizer', async () => { + const mockDecoder = { + decode: vi.fn().mockReturnValue('decoded chunk') + }; + + const readerAny = reader as any; + readerAny.decoder = mockDecoder; + + const mockReader = { + read: vi.fn() + .mockResolvedValueOnce({ done: false, value: new Uint8Array([1, 2, 3]) }) + .mockResolvedValueOnce({ done: true, value: undefined }) + }; + + const mockResponse = { + body: { + getReader: vi.fn().mockReturnValue(mockReader) + } + }; + + (global.fetch as any).mockResolvedValue(mockResponse); + + const writeSpy = vi.spyOn(mockTokenizer, 'write'); + + await reader.start(); + + expect(mockDecoder.decode).toHaveBeenCalledWith(new Uint8Array([1, 2, 3]), { stream: true }); + expect(writeSpy).toHaveBeenCalledWith('decoded chunk'); + }); + }); + + describe('event listener methods', () => { + it('should register partial event listeners', () => { + const callback = vi.fn(); + reader.onpartialjson(callback); + + // Trigger partial event + const event = new CustomEvent('partial', { detail: { test: 'data' } }); + reader.dispatchEvent(event); + + expect(callback).toHaveBeenCalledWith(event); + }); + + it('should register end event listeners', () => { + const callback = vi.fn(); + reader.onend(callback); + + // Trigger end event + const event = new Event('end'); + reader.dispatchEvent(event); + + expect(callback).toHaveBeenCalledWith(event); + }); + + it('should register error event listeners', () => { + const callback = vi.fn(); + reader.onerror(callback); + + // Trigger error event + const event = new CustomEvent('error', { detail: new Error('Test') }); + reader.dispatchEvent(event); + + expect(callback).toHaveBeenCalledWith(event); + }); + }); + + describe('complex nested structures', () => { + it('should handle objects containing arrays', () => { + const readerAny = reader as any; + + // { "items": [1, 2, 3] } + if (mockTokenizer.onopenobject) { + mockTokenizer.onopenobject(); + } + if (mockTokenizer.onkey) { + mockTokenizer.onkey('items'); + } + if (mockTokenizer.onopenarray) { + mockTokenizer.onopenarray(); + } + if (mockTokenizer.onvalue) { + mockTokenizer.onvalue(1); + } + if (mockTokenizer.onvalue) { + mockTokenizer.onvalue(2); + } + if (mockTokenizer.onvalue) { + mockTokenizer.onvalue(3); + } + if (mockTokenizer.onclosearray) { + mockTokenizer.onclosearray(); + } + if (mockTokenizer.oncloseobject) { + mockTokenizer.oncloseobject(); + } + + expect(readerAny.root).toEqual({ items: [1, 2, 3] }); + }); + + it('should handle arrays containing objects', () => { + const readerAny = reader as any; + + // [{"id": 1}, {"id": 2}] + if (mockTokenizer.onopenarray) { + mockTokenizer.onopenarray(); + } + + // First object + if (mockTokenizer.onopenobject) { + mockTokenizer.onopenobject(); + } + if (mockTokenizer.onkey) { + mockTokenizer.onkey('id'); + } + if (mockTokenizer.onvalue) { + mockTokenizer.onvalue(1); + } + if (mockTokenizer.oncloseobject) { + mockTokenizer.oncloseobject(); + } + + // Second object + if (mockTokenizer.onopenobject) { + mockTokenizer.onopenobject(); + } + if (mockTokenizer.onkey) { + mockTokenizer.onkey('id'); + } + if (mockTokenizer.onvalue) { + mockTokenizer.onvalue(2); + } + if (mockTokenizer.oncloseobject) { + mockTokenizer.oncloseobject(); + } + + if (mockTokenizer.onclosearray) { + mockTokenizer.onclosearray(); + } + + expect(readerAny.root).toEqual([{ id: 1 }, { id: 2 }]); + }); + }); +}); diff --git a/test/index.test.ts b/test/index.test.ts new file mode 100644 index 0000000..b562987 --- /dev/null +++ b/test/index.test.ts @@ -0,0 +1,117 @@ +import { describe, it, expect } from 'vitest'; +import * as indexModule from '../src/index'; + +describe('index.ts exports', () => { + describe('useJsonStream export', () => { + it('should export useJsonStream function', () => { + expect(indexModule).toHaveProperty('useJsonStream'); + expect(typeof indexModule.useJsonStream).toBe('function'); + }); + + it('should be the default export from useJsonStream module', async () => { + // Import the actual useJsonStream module to verify it's the same + const useJsonStreamModule = await import('../src/useJsonStream'); + expect(indexModule.useJsonStream).toBe(useJsonStreamModule.default); + }); + + it('should be callable with URL and fetchOptions', () => { + // This is a basic test to ensure the function signature is correct + // The actual functionality is tested in useJsonStream.test.tsx + const testUrl = 'https://example.com/api/data'; + const testFetchOptions: RequestInit = { method: 'GET' }; + + // We can't actually call the hook outside of a React component, + // but we can verify it's a function that accepts the right parameters + expect(typeof indexModule.useJsonStream).toBe('function'); + }); + }); + + describe('module structure', () => { + it('should only export useJsonStream', () => { + const exportedKeys = Object.keys(indexModule); + expect(exportedKeys).toEqual(['useJsonStream']); + }); + + it('should not export internal modules', () => { + // Verify that internal implementation details are not exposed + expect(indexModule).not.toHaveProperty('JsonEventStore'); + expect(indexModule).not.toHaveProperty('JsonStreamReader'); + expect(indexModule).not.toHaveProperty('SimpleJsonTokenizer'); + expect(indexModule).not.toHaveProperty('AbstractJsonStreamReader'); + expect(indexModule).not.toHaveProperty('AbstractJsonTokenizer'); + }); + }); + + describe('TypeScript compatibility', () => { + it('should have proper TypeScript types', () => { + // This test ensures the module can be imported in TypeScript + // without type errors + const useJsonStream: typeof indexModule.useJsonStream = indexModule.useJsonStream; + expect(typeof useJsonStream).toBe('function'); + }); + + it('should support generic type parameters', () => { + // Test that the exported function supports generic types + // This is more of a compile-time check, but we can verify the function exists + interface TestType { + id: number; + name: string; + } + + const useJsonStream = indexModule.useJsonStream; + expect(typeof useJsonStream).toBe('function'); + + // The actual generic usage would be: useJsonStream(url, options) + // This is tested in the useJsonStream.test.tsx file + }); + }); + + describe('import compatibility', () => { + it('should support default import', async () => { + const defaultImport = await import('../src/index'); + expect(defaultImport).toHaveProperty('useJsonStream'); + }); + + it('should support named import', async () => { + const { useJsonStream } = await import('../src/index'); + expect(typeof useJsonStream).toBe('function'); + }); + + it('should support namespace import', async () => { + const index = await import('../src/index'); + expect(index.useJsonStream).toBeDefined(); + expect(typeof index.useJsonStream).toBe('function'); + }); + }); + + describe('re-export behavior', () => { + it('should re-export the same function reference', async () => { + const directImport = await import('../src/useJsonStream'); + const indexImport = await import('../src/index'); + + expect(indexImport.useJsonStream).toBe(directImport.default); + }); + + it('should maintain function identity', () => { + // The exported function should be the same reference + const useJsonStream1 = indexModule.useJsonStream; + const useJsonStream2 = indexModule.useJsonStream; + + expect(useJsonStream1).toBe(useJsonStream2); + }); + }); + + describe('module metadata', () => { + it('should be a valid ES module', () => { + // Verify that the module can be imported as an ES module + expect(typeof indexModule).toBe('object'); + expect(indexModule).not.toBeNull(); + }); + + it('should not have a default export at module level', () => { + // The module itself should not have a default export, + // only named exports + expect(indexModule).not.toHaveProperty('default'); + }); + }); +}); diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 0000000..49664f7 --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,30 @@ +import { vi } from 'vitest'; + +// Mock DOM APIs that might be needed +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}); + +// Mock ResizeObserver +global.ResizeObserver = vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), +})); + +// Mock IntersectionObserver +global.IntersectionObserver = vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), +})); diff --git a/test/useJsonStream.test.tsx b/test/useJsonStream.test.tsx new file mode 100644 index 0000000..4d7223e --- /dev/null +++ b/test/useJsonStream.test.tsx @@ -0,0 +1,297 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { renderHook, act } from '@testing-library/react'; +import useJsonStream from '../src/useJsonStream'; +import { JsonEventStore } from '../src/JsonEventStore'; + +// Mock the JsonEventStore +vi.mock('../src/JsonEventStore'); + +describe('useJsonStream', () => { + let mockEventStore: any; + const testUrl = 'https://example.com/api/data'; + const testFetchOptions: RequestInit = { method: 'GET' }; + + beforeEach(() => { + vi.clearAllMocks(); + + // Mock JsonEventStore + mockEventStore = { + start: vi.fn().mockResolvedValue(undefined), + subscribe: vi.fn().mockReturnValue(() => {}), + getSnapshot: vi.fn().mockReturnValue(undefined) + }; + + (JsonEventStore as any).mockReturnValue(mockEventStore); + }); + + describe('hook initialization', () => { + it('should create JsonEventStore with correct parameters', () => { + renderHook(() => useJsonStream(testUrl, testFetchOptions)); + + expect(JsonEventStore).toHaveBeenCalledWith(testUrl, testFetchOptions); + }); + + it('should create JsonEventStore with empty object when fetchOptions not provided', () => { + renderHook(() => useJsonStream(testUrl)); + + expect(JsonEventStore).toHaveBeenCalledWith(testUrl, {}); + }); + + it('should return the snapshot from event store', () => { + const testData = { id: 1, name: 'John' }; + mockEventStore.getSnapshot.mockReturnValue(testData); + + const { result } = renderHook(() => useJsonStream(testUrl, testFetchOptions)); + + expect(result.current).toBe(testData); + }); + + it('should return undefined when no data is available', () => { + mockEventStore.getSnapshot.mockReturnValue(undefined); + + const { result } = renderHook(() => useJsonStream(testUrl, testFetchOptions)); + + expect(result.current).toBeUndefined(); + }); + }); + + describe('event store subscription', () => { + it('should subscribe to event store changes', () => { + renderHook(() => useJsonStream(testUrl, testFetchOptions)); + + expect(mockEventStore.subscribe).toHaveBeenCalledWith(expect.any(Function)); + }); + + it('should unsubscribe when component unmounts', () => { + const unsubscribe = vi.fn(); + mockEventStore.subscribe.mockReturnValue(unsubscribe); + + const { unmount } = renderHook(() => useJsonStream(testUrl, testFetchOptions)); + + unmount(); + + expect(unsubscribe).toHaveBeenCalledTimes(1); + }); + + it('should handle subscription updates', () => { + const testData1 = { id: 1, name: 'John' }; + const testData2 = { id: 2, name: 'Jane' }; + + // First render + mockEventStore.getSnapshot.mockReturnValue(testData1); + const { result, rerender } = renderHook(() => useJsonStream(testUrl, testFetchOptions)); + + expect(result.current).toBe(testData1); + + // Simulate data change + mockEventStore.getSnapshot.mockReturnValue(testData2); + rerender(); + + expect(result.current).toBe(testData2); + }); + }); + + describe('automatic start', () => { + it('should call start on event store when component mounts', async () => { + renderHook(() => useJsonStream(testUrl, testFetchOptions)); + + // Wait for useEffect to run + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + expect(mockEventStore.start).toHaveBeenCalledTimes(1); + }); + + it('should not call start multiple times on re-renders', async () => { + const { rerender } = renderHook(() => useJsonStream(testUrl, testFetchOptions)); + + // Wait for initial useEffect + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + // Re-render with same dependencies + rerender(); + + // Wait for any additional effects + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + expect(mockEventStore.start).toHaveBeenCalledTimes(1); + }); + + it('should handle start promise rejection gracefully', async () => { + const error = new Error('Start failed'); + mockEventStore.start.mockRejectedValue(error); + + // Should not throw + expect(() => { + renderHook(() => useJsonStream(testUrl, testFetchOptions)); + }).not.toThrow(); + + // Wait for useEffect to run + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + expect(mockEventStore.start).toHaveBeenCalledTimes(1); + }); + }); + + describe('memoization', () => { + it('should memoize event store creation with same URL', () => { + const { rerender } = renderHook(() => useJsonStream(testUrl, testFetchOptions)); + + expect(JsonEventStore).toHaveBeenCalledTimes(1); + + // Re-render with same URL + rerender(); + + expect(JsonEventStore).toHaveBeenCalledTimes(1); + }); + + it('should create new event store when URL changes', () => { + const { rerender } = renderHook( + ({ url }) => useJsonStream(url, testFetchOptions), + { initialProps: { url: testUrl } } + ); + + expect(JsonEventStore).toHaveBeenCalledTimes(1); + + // Change URL + const newUrl = 'https://example.com/api/other'; + rerender({ url: newUrl }); + + expect(JsonEventStore).toHaveBeenCalledTimes(2); + expect(JsonEventStore).toHaveBeenLastCalledWith(newUrl, testFetchOptions); + }); + + it('should handle URL object changes', () => { + const url1 = new URL('https://example.com/api/data'); + const url2 = new URL('https://example.com/api/other'); + + const { rerender } = renderHook( + ({ url }) => useJsonStream(url, testFetchOptions), + { initialProps: { url: url1 } } + ); + + expect(JsonEventStore).toHaveBeenCalledTimes(1); + + rerender({ url: url2 }); + + expect(JsonEventStore).toHaveBeenCalledTimes(2); + expect(JsonEventStore).toHaveBeenLastCalledWith(url2, testFetchOptions); + }); + + it('should handle Request object changes', () => { + const request1 = new Request('https://example.com/api/data'); + const request2 = new Request('https://example.com/api/other'); + + const { rerender } = renderHook( + ({ url }) => useJsonStream(url, testFetchOptions), + { initialProps: { url: request1 } } + ); + + expect(JsonEventStore).toHaveBeenCalledTimes(1); + + rerender({ url: request2 }); + + expect(JsonEventStore).toHaveBeenCalledTimes(2); + expect(JsonEventStore).toHaveBeenLastCalledWith(request2, testFetchOptions); + }); + }); + + describe('data flow', () => { + + it('should handle null and undefined data', () => { + // Test with null + mockEventStore.getSnapshot.mockReturnValue(null); + const { result: result1 } = renderHook(() => useJsonStream(testUrl, testFetchOptions)); + expect(result1.current).toBeNull(); + + // Test with undefined + mockEventStore.getSnapshot.mockReturnValue(undefined); + const { result: result2 } = renderHook(() => useJsonStream(testUrl, testFetchOptions)); + expect(result2.current).toBeUndefined(); + }); + + it('should handle complex nested data structures', () => { + const complexData = { + users: [ + { id: 1, name: 'John', active: true }, + { id: 2, name: 'Jane', active: false } + ], + metadata: { + total: 2, + page: 1 + } + }; + + mockEventStore.getSnapshot.mockReturnValue(complexData); + const { result } = renderHook(() => useJsonStream(testUrl, testFetchOptions)); + + expect(result.current).toEqual(complexData); + }); + }); + + describe('error handling', () => { + it('should handle event store creation errors', () => { + const error = new Error('Event store creation failed'); + (JsonEventStore as any).mockImplementation(() => { + throw error; + }); + + expect(() => { + renderHook(() => useJsonStream(testUrl, testFetchOptions)); + }).toThrow(error); + }); + + it('should handle subscription errors', () => { + const error = new Error('Subscription failed'); + mockEventStore.subscribe.mockImplementation(() => { + throw error; + }); + + expect(() => { + renderHook(() => useJsonStream(testUrl, testFetchOptions)); + }).toThrow(error); + }); + + it('should handle getSnapshot errors', () => { + const error = new Error('Get snapshot failed'); + mockEventStore.getSnapshot.mockImplementation(() => { + throw error; + }); + + expect(() => { + renderHook(() => useJsonStream(testUrl, testFetchOptions)); + }).toThrow(error); + }); + }); + + describe('TypeScript integration', () => { + it('should work with typed data', () => { + interface User { + id: number; + name: string; + email: string; + } + + const userData: User = { + id: 1, + name: 'John Doe', + email: 'john@example.com' + }; + + mockEventStore.getSnapshot.mockReturnValue(userData); + const { result } = renderHook(() => useJsonStream(testUrl, testFetchOptions)); + + expect(result.current).toEqual(userData); + expect(result.current?.id).toBe(1); + expect(result.current?.name).toBe('John Doe'); + expect(result.current?.email).toBe('john@example.com'); + }); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..fd740e4 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + setupFiles: ['./test/setup.ts'], + }, +}); From 60d3cb60b370cfc029a3056b3606ce36516a7b81 Mon Sep 17 00:00:00 2001 From: Alek Merani Date: Sun, 14 Sep 2025 16:53:07 -0700 Subject: [PATCH 3/4] changelog --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 892f9c7..3362a5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # react-json-chunked +## 0.3.7 + +### Patch Changes + +- feat: add unit tests + ## 0.3.6 ### Patch Changes diff --git a/package.json b/package.json index bdb753d..e64ddbc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-json-chunked", - "version": "0.3.6", + "version": "0.3.7", "description": "A React component for streaming JSON using chunked Transfer-Encoding", "type": "module", "files": [ From 7853ef3f6d1885533a296120281c414dd8ad5831 Mon Sep 17 00:00:00 2001 From: Alek Merani Date: Sun, 14 Sep 2025 16:54:47 -0700 Subject: [PATCH 4/4] run tests in ci --- .github/workflows/ci-build.yml | 3 +++ .github/workflows/release.yml | 3 +++ package.json | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index b25ea8d..8a8f611 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -29,3 +29,6 @@ jobs: - name: Build run: npm run build + + - name: Test + run: npm run test:ci diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c063b1b..2a8c5b4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,6 +27,9 @@ jobs: - name: Build run: npm run build + - name: Test + run: npm run test:ci + - name: Release uses: changesets/action@v1 with: diff --git a/package.json b/package.json index e64ddbc..ad0cc18 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "watch:src": "vite build --watch", "watch:dist": "nodemon -w dist --exec 'vite'", "test": "vitest", - "test:run": "vitest run", + "test:ci": "vitest run", "version": "changeset && changeset version", "release": "changeset publish" },