From 9254cec57e7f6f2f8adf68a6902ea3b25d4b123d Mon Sep 17 00:00:00 2001 From: krutoo Date: Sat, 15 Aug 2026 00:50:52 +0500 Subject: [PATCH] test: Coverage bump to 80% --- package-lock.json | 28 ++++++-- package.json | 1 - scripts/setup-tests.ts | 4 +- scripts/utils.ts | 13 +++- src/di/__test__/container.test.ts | 51 +++++++++++++ src/misc/__test__/timer-pool.test.ts | 9 +++ .../di/__test__/use-dependency.web.test.tsx | 72 +++++++++++++++++++ .../router/__test__/use-location.web.test.tsx | 41 +++++++++++ .../router/__test__/use-navigate.web.test.tsx | 43 +++++++++++ .../__test__/use-route-params.web.test.tsx | 32 +++++++++ 10 files changed, 285 insertions(+), 9 deletions(-) create mode 100644 src/misc/__test__/timer-pool.test.ts create mode 100644 src/react/di/__test__/use-dependency.web.test.tsx create mode 100644 src/react/router/__test__/use-location.web.test.tsx create mode 100644 src/react/router/__test__/use-navigate.web.test.tsx create mode 100644 src/react/router/__test__/use-route-params.web.test.tsx diff --git a/package-lock.json b/package-lock.json index 4815345..886f86e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,6 @@ "@rspack/core": "^2.0.3", "@std/expect": "npm:@jsr/std__expect@^1.0.17", "@testing-library/react": "^16.1.0", - "@trivago/prettier-plugin-sort-imports": "^6.0.2", "@types/node": "^24.2.1", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -136,6 +135,7 @@ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", @@ -182,6 +182,7 @@ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -226,6 +227,7 @@ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -272,6 +274,7 @@ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/types": "^7.29.7" }, @@ -298,6 +301,7 @@ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", @@ -313,6 +317,7 @@ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -332,6 +337,7 @@ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" @@ -1047,6 +1053,7 @@ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" @@ -1070,6 +1077,7 @@ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.0.0" } @@ -1079,7 +1087,8 @@ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", @@ -1087,6 +1096,7 @@ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -1480,6 +1490,7 @@ "integrity": "sha512-3DgfkukFyC/sE/VuYjaUUWoFfuVjPK55vOFDsxD56XXynFMCZDYFogH2l/hDfOsQAm1myoU/1xByJ3tWqtulXA==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@babel/generator": "^7.28.0", "@babel/parser": "^7.28.0", @@ -2246,6 +2257,7 @@ "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -5073,7 +5085,8 @@ "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/jiti": { "version": "2.6.1", @@ -5132,6 +5145,7 @@ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jsesc": "bin/jsesc" }, @@ -5284,7 +5298,8 @@ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.camelcase": { "version": "4.3.0", @@ -5422,6 +5437,7 @@ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^2.0.2" }, @@ -5765,6 +5781,7 @@ "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "parse-statements": "1.0.11" } @@ -5806,7 +5823,8 @@ "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/path-exists": { "version": "5.0.0", diff --git a/package.json b/package.json index 4751723..4f99e2a 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,6 @@ "@rspack/core": "^2.0.3", "@std/expect": "npm:@jsr/std__expect@^1.0.17", "@testing-library/react": "^16.1.0", - "@trivago/prettier-plugin-sort-imports": "^6.0.2", "@types/node": "^24.2.1", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", diff --git a/scripts/setup-tests.ts b/scripts/setup-tests.ts index f6032d0..7382b38 100644 --- a/scripts/setup-tests.ts +++ b/scripts/setup-tests.ts @@ -11,7 +11,9 @@ before(async context => { return; } - GlobalRegistrator.register(); + GlobalRegistrator.register({ + url: 'http://localhost:8080', + }); // dynamic import because https://github.com/capricorn86/happy-dom/issues/1636#issuecomment-2568308938 const { configure } = await import('@testing-library/react'); diff --git a/scripts/utils.ts b/scripts/utils.ts index 6629c86..3edc3e0 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -24,14 +24,23 @@ export function $(cmd: string): Promise { env: { ...process.env, FORCE_COLOR: '3' }, }); - child.on('error', fail); + const handleSigint = () => child.kill(); + + process.on('SIGINT', handleSigint); + + child.on('error', error => { + process.off('SIGINT', handleSigint); + fail(error); + }); child.on('close', (code, signal) => { + process.off('SIGINT', handleSigint); + if (code === 0) { done(); } else { fail( - new Error(signal ? `Process killed, signal ${signal}` : `Process exited, code ${code}`), + new Error(signal ? `Process closed, signal: ${signal}` : `Process exited, code: ${code}`), ); } }); diff --git a/src/di/__test__/container.test.ts b/src/di/__test__/container.test.ts index 9b0aa2e..ec93bce 100644 --- a/src/di/__test__/container.test.ts +++ b/src/di/__test__/container.test.ts @@ -11,6 +11,10 @@ interface Calculator { sum(a: number, b: number): number; } +interface HttpClient { + request(method: string, resource: string): Promise<{ status: number }>; +} + describe('Container', () => { test('basic behavior', () => { const TOKEN = { @@ -115,4 +119,51 @@ describe('Container', () => { new Error('Cycle dependency found: Token(logger), Token(calculator), Token(logger)'), ); }); + + test('should not throw circular dep error when dep used in two components', () => { + const TOKEN = { + main: createToken('main'), + logger: createToken('logger'), + http: { + client: createToken('httpClient'), + }, + }; + + const container = createContainer(); + + container.set(TOKEN.main, resolve => { + const logger = resolve(TOKEN.logger); + const client = resolve(TOKEN.http.client); + + return () => { + logger.info('App started'); + client.request('POST', '/api/analytics?event=app_start'); + }; + }); + + container.set(TOKEN.http.client, resolve => { + const logger = resolve(TOKEN.logger); + + return { + request() { + const status = 201; + + logger.info(`incoming request done, status: ${status}`); + + return Promise.resolve({ status }); + }, + }; + }); + + container.set(TOKEN.logger, () => { + return { + info(message) { + // eslint-disable-next-line no-console + console.log(`msg: ${message}`); + }, + }; + }); + + expect(() => container.get(TOKEN.main)).not.toThrow(); + }); }); diff --git a/src/misc/__test__/timer-pool.test.ts b/src/misc/__test__/timer-pool.test.ts new file mode 100644 index 0000000..d4a9604 --- /dev/null +++ b/src/misc/__test__/timer-pool.test.ts @@ -0,0 +1,9 @@ +import { describe, test } from 'node:test'; +import { expect } from '@std/expect'; +import { TimerPool } from '../timer-pool.ts'; + +describe('TimerPool', () => { + test('constructor should not fail in env without request/cancelAnimationFrame', () => { + expect(() => new TimerPool()).not.toThrow(); + }); +}); diff --git a/src/react/di/__test__/use-dependency.web.test.tsx b/src/react/di/__test__/use-dependency.web.test.tsx new file mode 100644 index 0000000..2ba3d1f --- /dev/null +++ b/src/react/di/__test__/use-dependency.web.test.tsx @@ -0,0 +1,72 @@ +import { type Mock, describe, mock, test } from 'node:test'; +import { useEffect } from 'react'; +import { expect } from '@std/expect'; +import { render } from '@testing-library/react'; +import { createContainer } from '../../../di/container.ts'; +import { createToken } from '../../../di/token.ts'; +import { ContainerProvider } from '../container-provider.tsx'; +import { useDependency } from '../use-dependency.ts'; + +interface Logger { + info(message: string): void; +} + +const TOKEN = { + logger: createToken('logger'), +} as const; + +describe('useDependency', () => { + const TestComponent = () => { + const logger = useDependency(TOKEN.logger); + + useEffect(() => { + logger.info('Component mounted'); + }, [logger]); + + return
This is of useDependency
; + }; + + test('should return component from container', () => { + const container = createContainer(); + + container.set(TOKEN.logger, () => { + return { + info: mock.fn(), + }; + }); + + const logger = container.get(TOKEN.logger); + + expect((logger.info as Mock).mock.callCount()).toBe(0); + + render( + + + , + ); + + expect((logger.info as Mock).mock.callCount()).toBe(1); + }); + + test('should throw if component is not defined', () => { + const container = createContainer(); + + const mount = () => { + render( + + + , + ); + }; + + expect(mount).toThrow(); + }); + + test('should throw if container is not provided', () => { + const mount = () => { + render(); + }; + + expect(mount).toThrow(); + }); +}); diff --git a/src/react/router/__test__/use-location.web.test.tsx b/src/react/router/__test__/use-location.web.test.tsx new file mode 100644 index 0000000..609b310 --- /dev/null +++ b/src/react/router/__test__/use-location.web.test.tsx @@ -0,0 +1,41 @@ +import { describe, test } from 'node:test'; +import { act } from 'react'; +import { expect } from '@std/expect'; +import { render } from '@testing-library/react'; +import { BrowserRouter } from '../../../router/browser-router.ts'; +import { RouterContext } from '../router-context.ts'; +import { useLocation } from '../use-location.ts'; + +describe('useLocation', () => { + test('should return actual location', () => { + const TestComponent = () => { + const location = useLocation(); + + return ( +
+ pathname: {location.pathname} +
+ ); + }; + + const router = new BrowserRouter(); + + const disconnect = router.connect(); + + const { container } = render( + + + , + ); + + expect(container.textContent).toBe('pathname: /'); + + act(() => { + router.navigate('/hello/world'); + }); + + expect(container.textContent).toBe('pathname: /hello/world'); + + disconnect(); + }); +}); diff --git a/src/react/router/__test__/use-navigate.web.test.tsx b/src/react/router/__test__/use-navigate.web.test.tsx new file mode 100644 index 0000000..1451ada --- /dev/null +++ b/src/react/router/__test__/use-navigate.web.test.tsx @@ -0,0 +1,43 @@ +import { describe, test } from 'node:test'; +import { expect } from '@std/expect'; +import { fireEvent, render } from '@testing-library/react'; +import { BrowserRouter } from '../../../router/browser-router.ts'; +import { RouterContext } from '../router-context.ts'; +import { useNavigate } from '../use-navigate.ts'; + +describe('useNavigate', () => { + test('should return navigate function', () => { + const TestComponent = () => { + const navigate = useNavigate(); + + return ( +
+ + +
+ ); + }; + + const router = new BrowserRouter(); + + const disconnect = router.connect(); + + const { getByTestId } = render( + + + , + ); + + fireEvent.click(getByTestId('profile-button')); + expect(router.getLocation().pathname).toBe('/profile'); + + fireEvent.click(getByTestId('back-button')); + expect(router.getLocation().pathname).toBe('/'); + + disconnect(); + }); +}); diff --git a/src/react/router/__test__/use-route-params.web.test.tsx b/src/react/router/__test__/use-route-params.web.test.tsx new file mode 100644 index 0000000..33f4d35 --- /dev/null +++ b/src/react/router/__test__/use-route-params.web.test.tsx @@ -0,0 +1,32 @@ +import { describe, test } from 'node:test'; +import { expect } from '@std/expect'; +import { render } from '@testing-library/react'; +import { BrowserRouter } from '../../../router/browser-router.ts'; +import { RouterContext } from '../router-context.ts'; +import { useRouteParams } from '../use-route-params.ts'; + +describe('useRouteParams', () => { + test('should return actual route params', () => { + const TestComponent = () => { + const { userId } = useRouteParams('/profile/:userId'); + + return
User ID: {userId}
; + }; + + const router = new BrowserRouter(); + + const disconnect = router.connect(); + + router.navigate('/profile/1005002'); + + const { container } = render( + + + , + ); + + expect(container.textContent).toBe('User ID: 1005002'); + + disconnect(); + }); +});