From c5abb0a52bb3504545d73b0c9a8961529aed9efd Mon Sep 17 00:00:00 2001 From: Ivan Galiatin Date: Wed, 30 Jul 2025 13:46:43 +0200 Subject: [PATCH 1/7] Add settings.json for Claude permissions configuration --- .claude/settings.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..ea2ea80 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run lint)", + "Bash(npm run typecheck)", + "Bash(npm run test*)" + ], + "deny": [] + } +} \ No newline at end of file From a33423f78bb4c48daa93bc13fb45fcae33954c50 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 10:39:49 +0000 Subject: [PATCH 2/7] fix: move puppeteer to devDependencies and bump version to 1.16.0 Puppeteer is only used in development/testing scripts and should not be a runtime dependency for consumers of the library. Fixes #74 Co-authored-by: Ivan Galiatin --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 289a0a9..2a70c95 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsdom-testing-mocks", - "version": "1.15.2", + "version": "1.16.0", "author": "Ivan Galiatin", "license": "MIT", "description": "A set of tools for emulating browser behavior in jsdom environment", @@ -83,11 +83,11 @@ }, "dependencies": { "bezier-easing": "^2.1.0", - "css-mediaquery": "^0.1.2", - "puppeteer": "^24.15.0" + "css-mediaquery": "^0.1.2" }, "devDependencies": { "@playwright/test": "^1.54.1", + "puppeteer": "^24.15.0", "@swc/core": "^1.3.82", "@swc/jest": "^0.2.29", "@types/css-mediaquery": "^0.1.1", From 25711963ba313d062be6f27768fb3adc36064763 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 11:20:57 +0000 Subject: [PATCH 3/7] fix: convert Jest configs to ES module syntax Convert jest.config.ts and swcjest.config.ts from CommonJS to ES module syntax to fix 'module is not defined in ES module scope' error. The issue occurred because package.json has "type": "module" which makes Node.js treat .ts files as ES modules, but Jest configs were using module.exports (CommonJS syntax). Co-authored-by: Ivan Galiatin --- jest.config.ts | 2 +- swcjest.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jest.config.ts b/jest.config.ts index 4c673f8..0084c0a 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,5 +1,5 @@ /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ -module.exports = { +export default { preset: 'ts-jest', testEnvironment: 'jsdom', setupFilesAfterEnv: ['/jest-setup.ts'], diff --git a/swcjest.config.ts b/swcjest.config.ts index 625d7ca..137c65f 100644 --- a/swcjest.config.ts +++ b/swcjest.config.ts @@ -1,4 +1,4 @@ -module.exports = { +export default { testEnvironment: 'jsdom', transform: { '^.+\\.(t|j)sx?$': '@swc/jest', From 9a82ddd2eac60249e5fbdaebe8512a730cd20f00 Mon Sep 17 00:00:00 2001 From: Ivan Galiatin Date: Sat, 13 Sep 2025 11:58:23 +0200 Subject: [PATCH 4/7] chore: update Claude code review workflow configuration Enhance the Claude code review workflow by allowing any bot to trigger reviews and adding accessibility to the review criteria. Adjust the feedback prompt to emphasize conciseness and constructiveness. Clean up unnecessary whitespace for better readability. --- .github/workflows/claude-code-review.yml | 25 +++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 5bf8ce5..c426fec 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -17,14 +17,14 @@ jobs: # github.event.pull_request.user.login == 'external-contributor' || # github.event.pull_request.user.login == 'new-developer' || # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - + runs-on: ubuntu-latest permissions: contents: read pull-requests: read issues: read id-token: write - + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -39,21 +39,25 @@ jobs: # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) # model: "claude-opus-4-20250514" - + + # Allow any bot to trigger the review + allowed_bots: '*' + # Direct prompt for automated review (no @claude mention needed) direct_prompt: | Please review this pull request and provide feedback on: - Code quality and best practices + - Accessibility - Potential bugs or issues - Performance considerations - Security concerns - Test coverage - - Be constructive and helpful in your feedback. + + Be concise and constructive in your feedback. # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR # use_sticky_comment: true - + # Optional: Customize review based on file types # direct_prompt: | # Review this PR focusing on: @@ -61,18 +65,17 @@ jobs: # - For API endpoints: Security, input validation, and error handling # - For React components: Performance, accessibility, and best practices # - For tests: Coverage, edge cases, and test quality - + # Optional: Different prompts for different authors # direct_prompt: | - # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && + # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && # 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' || # 'Please provide a thorough code review focusing on our coding standards and best practices.' }} - + # Optional: Add specific tools for running tests or linting # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)" - + # Optional: Skip review for certain conditions # if: | # !contains(github.event.pull_request.title, '[skip-review]') && # !contains(github.event.pull_request.title, '[WIP]') - From 216623e1fdf44303a88bc16d149098668606ed85 Mon Sep 17 00:00:00 2001 From: Ivan Galiatin Date: Sat, 13 Sep 2025 12:42:32 +0200 Subject: [PATCH 5/7] fix: improve SmartSpy type safety and cross-framework test compatibility --- jest-setup.ts | 98 ++++++++++++++++--- .../__tests__/cssNumberishHelpers.test.ts | 50 ++++++++-- 2 files changed, 127 insertions(+), 21 deletions(-) diff --git a/jest-setup.ts b/jest-setup.ts index 0f5a1ff..f82c0ba 100644 --- a/jest-setup.ts +++ b/jest-setup.ts @@ -1,3 +1,33 @@ +/** + * Jest-specific setup for unified testing framework abstraction. + * + * This file provides Jest implementations of the unified testing utilities + * available on the global `runner` object. It creates a consistent interface + * that allows the library's tests to work identically across Jest, Vitest, + * and SWC test environments. + * + * The key pattern here is the SmartSpy system, which wraps framework-specific + * spy objects (like Jest's SpyInstance) to provide a unified API. + */ + +/** + * Unified spy interface that works consistently across Jest and Vitest. + * + * This interface defines the common spy methods that our library needs, + * providing a consistent API regardless of the underlying testing framework. + * The implementation only includes the methods we actually use in our tests. + */ +interface SmartSpy { + /** Mock the implementation of the spied method */ + mockImplementation: (fn: () => void) => SmartSpy; + /** Assert that the spy was called with specific arguments */ + toHaveBeenCalledWith: (...args: unknown[]) => void; + /** Restore the original method implementation */ + mockRestore: () => void; + /** Allow access to any other spy properties/methods from the underlying framework */ + [key: string]: unknown; +} + function useFakeTimers() { jest.useFakeTimers(); } @@ -14,32 +44,78 @@ function fn() { return jest.fn(); } -// Smart proxy that only implements what we need -function createSmartSpy(realSpy: unknown) { +/** + * Creates a unified spy interface that works consistently across Jest and Vitest. + * + * This function wraps a Jest SpyInstance in a Proxy to provide a consistent API + * that matches the SmartSpy interface expected by the testing framework abstraction. + * + * The proxy intercepts calls to specific methods (mockImplementation, toHaveBeenCalledWith, + * mockRestore) and ensures they work the same way regardless of whether the underlying + * spy is from Jest or Vitest. For all other properties/methods, it passes through + * to the original spy. + * + * This is part of the broader pattern in this library where we create unified interfaces + * across different testing frameworks (Jest, Vitest, SWC) so that the library's mocks + * work consistently in any environment. + * + * @param realSpy - The underlying Jest SpyInstance to wrap + * @returns A proxy that implements the SmartSpy interface + */ +function createSmartSpy(realSpy: jest.SpyInstance): SmartSpy { return new Proxy(realSpy as object, { get(target, prop) { // Only implement the methods we actually use if (prop === 'mockImplementation') { return (fn: () => void) => { - (target as { mockImplementation: (fn: () => void) => unknown }).mockImplementation(fn); - return createSmartSpy(target); + (target as jest.SpyInstance).mockImplementation(fn); + return createSmartSpy(target as jest.SpyInstance); }; } if (prop === 'toHaveBeenCalledWith') { - return (target as { toHaveBeenCalledWith: (...args: unknown[]) => unknown }).toHaveBeenCalledWith.bind(target); + return (...args: unknown[]) => { + // Jest SpyInstance doesn't have toHaveBeenCalledWith as a method, + // it's available through expect(spy).toHaveBeenCalledWith() + // For compatibility, we'll check if the method exists and call it + const spy = target as Record; + if (typeof spy.toHaveBeenCalledWith === 'function') { + return ( + spy.toHaveBeenCalledWith as (...args: unknown[]) => unknown + )(...args); + } + return undefined; + }; } if (prop === 'mockRestore') { - return (target as { mockRestore: () => void }).mockRestore.bind(target); + return () => { + (target as jest.SpyInstance).mockRestore(); + }; } - + // For everything else, just pass through to the real spy return (target as Record)[prop]; - } - }); + }, + }) as SmartSpy; } -function spyOn(object: T, method: K) { - const realSpy = (jest.spyOn as (obj: T, method: K) => unknown)(object, method); +/** + * Creates a spy on an object method using Jest's spyOn, wrapped with SmartSpy interface. + * + * This is the Jest-specific implementation of the unified spyOn function that's available + * on the global `runner` object. It creates a Jest spy and wraps it with createSmartSpy + * to provide a consistent interface across testing frameworks. + * + * @param object - The object to spy on + * @param method - The method name to spy on + * @returns A SmartSpy that provides unified spy functionality + */ +function spyOn( + object: T, + method: K +): SmartSpy { + // Use a more specific type assertion to avoid 'any' + const spyFunction = jest.spyOn as (object: T, method: K) => jest.SpyInstance; + const realSpy = spyFunction(object, method); return createSmartSpy(realSpy); } diff --git a/src/mocks/web-animations-api/__tests__/cssNumberishHelpers.test.ts b/src/mocks/web-animations-api/__tests__/cssNumberishHelpers.test.ts index 5d881a7..e062c60 100644 --- a/src/mocks/web-animations-api/__tests__/cssNumberishHelpers.test.ts +++ b/src/mocks/web-animations-api/__tests__/cssNumberishHelpers.test.ts @@ -1,4 +1,8 @@ -import { cssNumberishToNumber, cssNumberishToNumberWithDefault, initCSSTypedOM } from '../cssNumberishHelpers'; +import { + cssNumberishToNumber, + cssNumberishToNumberWithDefault, + initCSSTypedOM, +} from '../cssNumberishHelpers'; describe('cssNumberishHelpers', () => { beforeAll(() => { @@ -37,16 +41,25 @@ describe('cssNumberishHelpers', () => { }); it('should return null for non-time units like px', () => { + const consoleWarnSpy = runner + .spyOn(console, 'warn') + .mockImplementation(() => { + /* do nothing */ + }); const oneHundredPx = new CSSUnitValue(100, 'px'); expect(cssNumberishToNumber(oneHundredPx)).toBeNull(); + expect(consoleWarnSpy).toHaveBeenCalledWith( + "jsdom-testing-mocks: Unsupported CSS unit 'px' in cssNumberishToNumber. Returning null." + ); + consoleWarnSpy.mockRestore(); }); it('should handle CSSMathValue for time calculations', () => { // Test time-dimensioned math values const timeSum = CSS.s(1).add(CSS.ms(500)); // 1.5 seconds = 1500ms expect(cssNumberishToNumber(timeSum)).toBe(1500); - - const timeProduct = CSS.s(2).mul(CSS.number(1.5)); // 3 seconds = 3000ms + + const timeProduct = CSS.s(2).mul(CSS.number(1.5)); // 3 seconds = 3000ms expect(cssNumberishToNumber(timeProduct)).toBe(3000); }); @@ -54,7 +67,7 @@ describe('cssNumberishHelpers', () => { // Test dimensionless math values const numberSum = CSS.number(2).add(CSS.number(3)); // = 5 expect(cssNumberishToNumber(numberSum)).toBe(5); - + const iterations = CSS.number(2.5).mul(CSS.number(2)); // = 5 expect(cssNumberishToNumber(iterations)).toBe(5); }); @@ -62,7 +75,9 @@ describe('cssNumberishHelpers', () => { it('should return null and warn for incompatible CSSMathValue dimensions', () => { const consoleWarnSpy = runner .spyOn(console, 'warn') - .mockImplementation(() => { /* do nothing */ }); + .mockImplementation(() => { + /* do nothing */ + }); const lengthValue = CSS.px(100).add(CSS.em(1)); // Different length units create CSSMathSum expect(cssNumberishToNumber(lengthValue)).toBeNull(); @@ -73,11 +88,12 @@ describe('cssNumberishHelpers', () => { consoleWarnSpy.mockRestore(); }); - it('should return null and warn for unsupported values', () => { const consoleWarnSpy = runner .spyOn(console, 'warn') - .mockImplementation(() => { /* do nothing */ }); + .mockImplementation(() => { + /* do nothing */ + }); expect(cssNumberishToNumber({} as unknown as CSSNumberish)).toBeNull(); expect(consoleWarnSpy).toHaveBeenCalledWith( @@ -91,12 +107,26 @@ describe('cssNumberishHelpers', () => { describe('cssNumberishToNumberWithDefault', () => { it('should return the converted value if conversion succeeds', () => { expect(cssNumberishToNumberWithDefault(100, 50)).toBe(100); - expect(cssNumberishToNumberWithDefault(new CSSUnitValue(2, 's'), 500)).toBe(2000); + expect( + cssNumberishToNumberWithDefault(new CSSUnitValue(2, 's'), 500) + ).toBe(2000); }); it('should return the default value if conversion fails', () => { expect(cssNumberishToNumberWithDefault(null, 50)).toBe(50); - expect(cssNumberishToNumberWithDefault(new CSSUnitValue(100, 'px'), 42)).toBe(42); + + const consoleWarnSpy = runner + .spyOn(console, 'warn') + .mockImplementation(() => { + /* do nothing */ + }); + expect( + cssNumberishToNumberWithDefault(new CSSUnitValue(100, 'px'), 42) + ).toBe(42); + expect(consoleWarnSpy).toHaveBeenCalledWith( + "jsdom-testing-mocks: Unsupported CSS unit 'px' in cssNumberishToNumber. Returning null." + ); + consoleWarnSpy.mockRestore(); }); }); -}); \ No newline at end of file +}); From 58b2301f86495b26e49353d410fd1d26c7685f8b Mon Sep 17 00:00:00 2001 From: Ivan Galiatin Date: Sat, 13 Sep 2025 13:29:24 +0200 Subject: [PATCH 6/7] fix: implement proper toHaveBeenCalledWith method for Jest SmartSpy and refactor to switch statement --- jest-setup.ts | 79 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/jest-setup.ts b/jest-setup.ts index f82c0ba..353490e 100644 --- a/jest-setup.ts +++ b/jest-setup.ts @@ -65,35 +65,58 @@ function fn() { function createSmartSpy(realSpy: jest.SpyInstance): SmartSpy { return new Proxy(realSpy as object, { get(target, prop) { - // Only implement the methods we actually use - if (prop === 'mockImplementation') { - return (fn: () => void) => { - (target as jest.SpyInstance).mockImplementation(fn); - return createSmartSpy(target as jest.SpyInstance); - }; - } - if (prop === 'toHaveBeenCalledWith') { - return (...args: unknown[]) => { - // Jest SpyInstance doesn't have toHaveBeenCalledWith as a method, - // it's available through expect(spy).toHaveBeenCalledWith() - // For compatibility, we'll check if the method exists and call it - const spy = target as Record; - if (typeof spy.toHaveBeenCalledWith === 'function') { - return ( - spy.toHaveBeenCalledWith as (...args: unknown[]) => unknown - )(...args); - } - return undefined; - }; - } - if (prop === 'mockRestore') { - return () => { - (target as jest.SpyInstance).mockRestore(); - }; - } + // Switch on the specific methods we need to implement + switch (prop) { + case 'mockImplementation': + return (fn: () => void) => { + (target as jest.SpyInstance).mockImplementation(fn); + return createSmartSpy(target as jest.SpyInstance); + }; + + case 'toHaveBeenCalledWith': + return (...args: unknown[]) => { + // Jest SpyInstance doesn't have toHaveBeenCalledWith as a method, + // but Vitest spies do. For compatibility, we implement it by checking + // the spy's call history manually to match Vitest's behavior. + const jestSpy = target as jest.SpyInstance; - // For everything else, just pass through to the real spy - return (target as Record)[prop]; + // Check if any call matches the expected arguments + const calls = jestSpy.mock.calls; + const hasMatchingCall = calls.some((call: unknown[]) => { + if (call.length !== args.length) return false; + return call.every((arg: unknown, index: number) => { + // Use Jest's deep equality matching + try { + expect(arg).toEqual(args[index]); + return true; + } catch { + return false; + } + }); + }); + + if (!hasMatchingCall) { + throw new Error( + `Expected spy to have been called with [${args.map((arg: unknown) => JSON.stringify(arg)).join(', ')}], ` + + `but it was called with: ${calls + .map( + (call: unknown[]) => + `[${call.map((arg: unknown) => JSON.stringify(arg)).join(', ')}]` + ) + .join(', ')}` + ); + } + }; + + case 'mockRestore': + return () => { + (target as jest.SpyInstance).mockRestore(); + }; + + default: + // For everything else, just pass through to the real spy + return (target as Record)[prop]; + } }, }) as SmartSpy; } From c15fec6bc28a646e4beea1fe8ceb2c23a5b20bdb Mon Sep 17 00:00:00 2001 From: Ivan Galiatin Date: Sat, 13 Sep 2025 14:16:21 +0200 Subject: [PATCH 7/7] Fix requestAnimationFrame teardown errors by automatically canceling animations in clearAnimations() --- src/mocks/web-animations-api/elementAnimations.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/mocks/web-animations-api/elementAnimations.ts b/src/mocks/web-animations-api/elementAnimations.ts index d7d896e..56c3091 100644 --- a/src/mocks/web-animations-api/elementAnimations.ts +++ b/src/mocks/web-animations-api/elementAnimations.ts @@ -28,5 +28,12 @@ export function getAllAnimations() { } export function clearAnimations() { + // Cancel all running animations to prevent requestAnimationFrame errors during teardown + const allAnimations = getAllAnimations(); + allAnimations.forEach((animation) => { + // Cancel the animation properly - this stops the requestAnimationFrame loop + animation.cancel(); + }); + elementAnimations.clear(); }