Skip to content

Commit c52965c

Browse files
committed
feat: migrate to ESLint flat config and implement smart spyOn proxy
1 parent 6294892 commit c52965c

File tree

14 files changed

+1850
-1856
lines changed

14 files changed

+1850
-1856
lines changed

.eslintignore

Lines changed: 0 additions & 1 deletion
This file was deleted.

.eslintrc.cjs

Lines changed: 0 additions & 23 deletions
This file was deleted.

README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ A set of tools for emulating browser behavior in jsdom environment
1919
[matchMedia](#mock-viewport)
2020
[Intersection Observer](#mock-intersectionobserver)
2121
[Resize Observer](#mock-resizeobserver)
22-
[Web Animations API](#mock-web-animations-api)
22+
[Web Animations API](#mock-web-animations-api)
23+
[CSS Typed OM](#mock-css-typed-om)
2324

2425
## Installation
2526

@@ -410,6 +411,37 @@ it('adds an element into the dom and fades it in', async () => {
410411

411412
It's perfectly usable with fake timers, except for the [issue with promises](https://github.com/facebook/jest/issues/2157). Also note that you would need to manually advance timers by the duration of the animation taking frame duration (which currently is set to 16ms in `jest`/`sinon.js`) into account. So if you, say, have an animation with a duration of `300ms`, you will need to advance your timers by the value that is at least the closest multiple of the frame duration, which in this case is `304ms` (`19` frames \* `16ms`). Otherwise the last frame may not fire and the animation won't finish.
412413

414+
## Mock CSS Typed OM
415+
416+
Provides a complete implementation of the CSS Typed Object Model Level 1 specification for testing CSS numeric values and calculations. While primarily used internally by the Web Animations API mock, it's available as a standalone feature supporting all major CSS units, mathematical operations, and type checking.
417+
418+
```jsx
419+
import { mockCSSTypedOM } from 'jsdom-testing-mocks';
420+
421+
mockCSSTypedOM();
422+
423+
it('performs CSS calculations correctly', () => {
424+
const width = CSS.px(100);
425+
const height = CSS.px(200);
426+
427+
expect(width.add(CSS.px(50)).toString()).toBe('150px');
428+
expect(width.mul(height).toString()).toBe('calc(100px * 200px)');
429+
expect(CSS.cm(2.54).to('in').toString()).toBe('1in');
430+
expect(CSS.px(100).min(CSS.px(200), CSS.px(50)).toString()).toBe('50px');
431+
});
432+
433+
it('enforces type safety', () => {
434+
// Cannot add incompatible units or use raw numbers
435+
expect(() => CSS.px(10).add(CSS.em(5))).toThrow();
436+
expect(() => CSS.px(10).add(5)).toThrow();
437+
438+
// Use CSS.number() for dimensionless values
439+
expect(CSS.px(10).add(CSS.number(5)).toString()).toBe('calc(10px + 5)');
440+
});
441+
```
442+
443+
Supports all CSS units (length, angle, time, frequency, resolution, flex, percentage), mathematical operations, and enforces type compatibility rules as defined in the [W3C specification](https://www.w3.org/TR/css-typed-om-1/).
444+
413445
## Current issues
414446

415447
- Needs more tests

eslint.config.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import js from '@eslint/js';
2+
import typescript from '@typescript-eslint/eslint-plugin';
3+
import typescriptParser from '@typescript-eslint/parser';
4+
import jsxA11y from 'eslint-plugin-jsx-a11y';
5+
import pluginJest from 'eslint-plugin-jest';
6+
import prettier from 'eslint-config-prettier';
7+
import vitest from '@vitest/eslint-plugin'
8+
import globals from 'globals';
9+
10+
export default [
11+
js.configs.recommended,
12+
{
13+
files: ['**/*.{ts,tsx}'],
14+
languageOptions: {
15+
parser: typescriptParser,
16+
parserOptions: {
17+
ecmaVersion: 'latest',
18+
sourceType: 'module',
19+
ecmaFeatures: {
20+
jsx: true,
21+
},
22+
},
23+
},
24+
plugins: {
25+
'@typescript-eslint': typescript,
26+
'jsx-a11y': jsxA11y,
27+
},
28+
rules: {
29+
...typescript.configs.recommended.rules,
30+
...typescript.configs.strict.rules,
31+
...jsxA11y.configs.recommended.rules,
32+
'no-unused-vars': 'off',
33+
'@typescript-eslint/no-unused-vars': [
34+
'error',
35+
{ ignoreRestSiblings: true },
36+
],
37+
// TS handles undefined types, turn off for TS files
38+
'no-undef': 'off',
39+
'@typescript-eslint/ban-ts-comment': 'off',
40+
},
41+
},
42+
{
43+
files: ['**/*.test.{js,jsx,ts,tsx}', '**/*.spec.{js,jsx,ts,tsx}'],
44+
plugins: {
45+
jest: pluginJest,
46+
vitest,
47+
},
48+
languageOptions: {
49+
globals: {
50+
...globals.node,
51+
...pluginJest.environments.globals.globals,
52+
...vitest.environments.env.globals,
53+
},
54+
},
55+
rules: {
56+
...vitest.configs.recommended.rules,
57+
// You can also bring in Jest recommended rules if desired
58+
// ...pluginJest.configs.recommended.rules,
59+
}
60+
},
61+
{
62+
ignores: ['dist/**'],
63+
},
64+
prettier,
65+
];

global.d.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ declare module 'vitest' {
2424

2525
export {};
2626

27+
// Smart proxy type that only implements what we need
28+
interface SmartSpy {
29+
mockImplementation: (fn: () => void) => SmartSpy;
30+
toHaveBeenCalledWith: (...args: unknown[]) => void;
31+
mockRestore: () => void;
32+
[key: string]: unknown; // For any other properties, will pass through to underlying spy
33+
}
34+
2735
interface Runner {
2836
name: 'vi' | 'jest';
2937
useFakeTimers: () => void;
@@ -32,7 +40,7 @@ interface Runner {
3240
/** A generic function to create a mock function, compatible with both runners. */
3341
fn: () => jest.Mock<unknown[], unknown>;
3442
/** A generic function to spy on a method, compatible with both runners. */
35-
spyOn: typeof jest.spyOn;
43+
spyOn: <T extends object, K extends keyof T>(object: T, method: K) => SmartSpy;
3644
}
3745

3846
declare global {

jest-setup.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,33 @@ function fn() {
1414
return jest.fn();
1515
}
1616

17-
function spyOn(...args: Parameters<typeof jest.spyOn>) {
18-
return jest.spyOn(...args);
17+
// Smart proxy that only implements what we need
18+
function createSmartSpy(realSpy: unknown) {
19+
return new Proxy(realSpy as object, {
20+
get(target, prop) {
21+
// Only implement the methods we actually use
22+
if (prop === 'mockImplementation') {
23+
return (fn: () => void) => {
24+
(target as { mockImplementation: (fn: () => void) => unknown }).mockImplementation(fn);
25+
return createSmartSpy(target);
26+
};
27+
}
28+
if (prop === 'toHaveBeenCalledWith') {
29+
return (target as { toHaveBeenCalledWith: (...args: unknown[]) => unknown }).toHaveBeenCalledWith.bind(target);
30+
}
31+
if (prop === 'mockRestore') {
32+
return (target as { mockRestore: () => void }).mockRestore.bind(target);
33+
}
34+
35+
// For everything else, just pass through to the real spy
36+
return (target as Record<string | symbol, unknown>)[prop];
37+
}
38+
});
39+
}
40+
41+
function spyOn<T extends object, K extends keyof T>(object: T, method: K) {
42+
const realSpy = (jest.spyOn as (obj: T, method: K) => unknown)(object, method);
43+
return createSmartSpy(realSpy);
1944
}
2045

2146
globalThis.runner = {

jest.config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ module.exports = {
44
testEnvironment: 'jsdom',
55
setupFilesAfterEnv: ['<rootDir>/jest-setup.ts'],
66
testPathIgnorePatterns: ['/node_modules/', '/examples/', '.*\\.browser\\.test\\.ts$'],
7-
globals: {
8-
'ts-jest': {
7+
transform: {
8+
'^.+\\.(t|j)sx?$': ['ts-jest', {
99
tsconfig: {
1010
jsx: 'react-jsx',
1111
},
1212
diagnostics: {
1313
warnOnly: true,
1414
},
15-
},
15+
}],
1616
},
1717
};

0 commit comments

Comments
 (0)