@csszyx/dynamic 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,177 +0,0 @@
1
- /**
2
- * Tests for manifest module.
3
- * Verifies manifest loading, caching, and delta check logic.
4
- */
5
-
6
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
-
8
- import {
9
- type CSSManifest,
10
- ensureManifest,
11
- isManifestLoaded,
12
- lookupManifest,
13
- preloadManifest,
14
- resetManifest,
15
- } from '../src/manifest.js';
16
-
17
- // Mock fetch globally
18
- const mockFetch = vi.fn();
19
- vi.stubGlobal('fetch', mockFetch);
20
-
21
- /**
22
- * Creates a complete CSSManifest fixture with sensible defaults for testing.
23
- *
24
- * @param partial - optional overrides to merge into the default manifest
25
- * @returns a fully-formed CSSManifest object suitable for use in tests
26
- */
27
- function makeMockManifest(partial: Partial<CSSManifest> = {}): CSSManifest {
28
- return {
29
- version: '0.4.0',
30
- buildId: 'test-build-id',
31
- classes: ['p-4', 'bg-blue-500', 'flex', 'text-white'],
32
- ...partial,
33
- };
34
- }
35
-
36
- /**
37
- * Configures the global fetch mock to resolve successfully with the given manifest.
38
- *
39
- * @param manifest - the CSSManifest object that the mocked fetch should return
40
- * @returns void
41
- */
42
- function setupFetchSuccess(manifest: CSSManifest): void {
43
- mockFetch.mockResolvedValueOnce({
44
- ok: true,
45
- json: () => Promise.resolve(manifest),
46
- });
47
- }
48
-
49
- describe('manifest loading', () => {
50
- beforeEach(() => {
51
- resetManifest();
52
- mockFetch.mockReset();
53
- });
54
-
55
- afterEach(() => {
56
- resetManifest();
57
- });
58
-
59
- it('isManifestLoaded returns false before loading', () => {
60
- expect(isManifestLoaded()).toBe(false);
61
- });
62
-
63
- it('lookupManifest returns null before manifest loads', () => {
64
- expect(lookupManifest('p-4')).toBeNull();
65
- });
66
-
67
- it('preloadManifest fetches and caches manifest', async () => {
68
- setupFetchSuccess(makeMockManifest());
69
- await preloadManifest('/csszyx-manifest.json');
70
-
71
- expect(isManifestLoaded()).toBe(true);
72
- expect(mockFetch).toHaveBeenCalledWith('/csszyx-manifest.json');
73
- });
74
-
75
- it('uses custom URL passed to preloadManifest', async () => {
76
- setupFetchSuccess(makeMockManifest());
77
- await preloadManifest('/custom/path/manifest.json');
78
-
79
- expect(mockFetch).toHaveBeenCalledWith('/custom/path/manifest.json');
80
- });
81
-
82
- it('coalesces concurrent ensureManifest calls', async () => {
83
- setupFetchSuccess(makeMockManifest());
84
-
85
- // Trigger two concurrent loads
86
- const p1 = ensureManifest();
87
- const p2 = ensureManifest();
88
- await Promise.all([p1, p2]);
89
-
90
- // Only one fetch should have been made
91
- expect(mockFetch).toHaveBeenCalledTimes(1);
92
- });
93
-
94
- it('gracefully handles fetch failure — treats all classes as new', async () => {
95
- mockFetch.mockRejectedValueOnce(new Error('network error'));
96
- await ensureManifest();
97
-
98
- expect(isManifestLoaded()).toBe(true); // loaded (empty fallback)
99
- expect(lookupManifest('p-4')).toBeNull(); // not in empty set
100
- });
101
-
102
- it('gracefully handles non-ok response', async () => {
103
- mockFetch.mockResolvedValueOnce({ ok: false, status: 404 });
104
- await ensureManifest();
105
-
106
- expect(isManifestLoaded()).toBe(true);
107
- expect(lookupManifest('p-4')).toBeNull();
108
- });
109
- });
110
-
111
- describe('manifest delta check', () => {
112
- beforeEach(async () => {
113
- resetManifest();
114
- mockFetch.mockReset();
115
- setupFetchSuccess(makeMockManifest());
116
- await preloadManifest();
117
- });
118
-
119
- afterEach(() => {
120
- resetManifest();
121
- });
122
-
123
- it('returns original class name for class in manifest (no mangle map)', () => {
124
- expect(lookupManifest('p-4')).toBe('p-4');
125
- expect(lookupManifest('bg-blue-500')).toBe('bg-blue-500');
126
- expect(lookupManifest('flex')).toBe('flex');
127
- });
128
-
129
- it('returns null for class not in manifest', () => {
130
- expect(lookupManifest('p-7')).toBeNull(); // not in test manifest
131
- expect(lookupManifest('bg-red-300')).toBeNull();
132
- });
133
-
134
- it('returns mangled name when mangle map is present', async () => {
135
- resetManifest();
136
- mockFetch.mockReset();
137
- setupFetchSuccess(makeMockManifest({
138
- classes: ['p-4', 'bg-blue-500'],
139
- mangleMap: { 'p-4': 'z', 'bg-blue-500': 'y' },
140
- }));
141
- await preloadManifest();
142
-
143
- expect(lookupManifest('p-4')).toBe('z');
144
- expect(lookupManifest('bg-blue-500')).toBe('y');
145
- });
146
-
147
- it('returns original name for class in manifest but not in mangle map', async () => {
148
- resetManifest();
149
- mockFetch.mockReset();
150
- setupFetchSuccess(makeMockManifest({
151
- classes: ['p-4', 'flex'],
152
- mangleMap: { 'p-4': 'z' }, // 'flex' not in mangle map
153
- }));
154
- await preloadManifest();
155
-
156
- expect(lookupManifest('p-4')).toBe('z');
157
- expect(lookupManifest('flex')).toBe('flex'); // original name
158
- });
159
- });
160
-
161
- describe('resetManifest', () => {
162
- it('clears loaded state and allows re-fetch', async () => {
163
- setupFetchSuccess(makeMockManifest());
164
- await preloadManifest();
165
- expect(isManifestLoaded()).toBe(true);
166
-
167
- resetManifest();
168
- expect(isManifestLoaded()).toBe(false);
169
- expect(lookupManifest('p-4')).toBeNull();
170
-
171
- // Can reload after reset
172
- setupFetchSuccess(makeMockManifest({ classes: ['new-class'] }));
173
- await preloadManifest();
174
- expect(lookupManifest('new-class')).toBe('new-class');
175
- expect(lookupManifest('p-4')).toBeNull(); // old class gone
176
- });
177
- });
@@ -1,221 +0,0 @@
1
- /**
2
- * Tests for the React integration layer (@csszyx/dynamic/react).
3
- *
4
- * React hooks cannot run outside a React render tree, so we mock the `react`
5
- * module to capture the effect callbacks passed to useEffect/useCallback/useContext.
6
- * This lets us invoke those callbacks directly and verify side-effects without
7
- * React Testing Library or a DOM environment.
8
- *
9
- * Coverage:
10
- * - sz export is the dynamic() alias
11
- * - useSz() returns { sz: fn } with a stable reference
12
- * - useSz() calls preloadManifest on mount
13
- * - useSz() schedules deferred cleanup (injectorCleanup + resetManifest) on unmount
14
- * - useSz() cancels pending cleanup when remounted (StrictMode resilience)
15
- * - CsszyxProvider calls setManifestUrl + preloadManifest on mount
16
- */
17
-
18
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
19
-
20
- // ── Captured hook state ───────────────────────────────────────────────────────
21
-
22
- /**
23
- *
24
- */
25
- type EffectFn = () => (() => void) | void;
26
-
27
- let capturedEffects: EffectFn[] = [];
28
- let capturedCallback: ((...args: unknown[]) => unknown) | null = null;
29
- let contextValue = { manifestUrl: '/csszyx-manifest.json' };
30
-
31
- // ── Mock react ────────────────────────────────────────────────────────────────
32
-
33
- vi.mock('react', () => ({
34
- createContext: vi.fn((defaultValue: unknown) => ({ _default: defaultValue, Provider: 'CsszyxContext.Provider' })),
35
- createElement: vi.fn(
36
- (type: unknown, props: Record<string, unknown> | null, ...children: unknown[]) =>
37
- ({ type, props, children }),
38
- ),
39
- useCallback: vi.fn((fn: (...args: unknown[]) => unknown) => {
40
- capturedCallback = fn;
41
- return fn;
42
- }),
43
- useContext: vi.fn(() => contextValue),
44
- useEffect: vi.fn((fn: EffectFn) => {
45
- capturedEffects.push(fn);
46
- }),
47
- }));
48
-
49
- // ── Mock internal modules ─────────────────────────────────────────────────────
50
-
51
- const mockDynamic = vi.fn((props: Record<string, unknown>) => `class-${JSON.stringify(props)}`);
52
- vi.mock('../src/index.js', () => ({ dynamic: mockDynamic }));
53
-
54
- const mockInjectorCleanup = vi.fn();
55
- vi.mock('../src/injector.js', () => ({ cleanup: mockInjectorCleanup }));
56
-
57
- const mockPreloadManifest = vi.fn();
58
- const mockResetManifest = vi.fn();
59
- const mockSetManifestUrl = vi.fn();
60
- vi.mock('../src/manifest.js', () => ({
61
- preloadManifest: mockPreloadManifest,
62
- resetManifest: mockResetManifest,
63
- setManifestUrl: mockSetManifestUrl,
64
- }));
65
-
66
- // ── Import subject under test (after mocks) ───────────────────────────────────
67
-
68
- const { sz, useSz, CsszyxProvider } = await import('../src/react.js');
69
-
70
- // ── Helpers ───────────────────────────────────────────────────────────────────
71
-
72
- /**
73
- * Runs all captured effects and returns their cleanup functions.
74
- * @returns array of cleanup functions returned by each effect (undefined entries filtered)
75
- */
76
- function runCapturedEffects(): Array<() => void> {
77
- return capturedEffects
78
- .map(fn => fn())
79
- .filter((r): r is () => void => typeof r === 'function');
80
- }
81
-
82
- // ── Tests ─────────────────────────────────────────────────────────────────────
83
-
84
- describe('sz export', () => {
85
- it('is the dynamic() alias', () => {
86
- expect(sz).toBe(mockDynamic);
87
- });
88
-
89
- it('passes props through to dynamic()', () => {
90
- sz({ p: 4, bg: 'blue-500' } as never);
91
- expect(mockDynamic).toHaveBeenCalledWith({ p: 4, bg: 'blue-500' });
92
- });
93
- });
94
-
95
- describe('useSz()', () => {
96
- beforeEach(() => {
97
- capturedEffects = [];
98
- capturedCallback = null;
99
- contextValue = { manifestUrl: '/csszyx-manifest.json' };
100
- vi.useFakeTimers();
101
- });
102
-
103
- afterEach(() => {
104
- vi.useRealTimers();
105
- vi.clearAllMocks();
106
- });
107
-
108
- it('returns an object with a sz function', () => {
109
- const result = useSz();
110
- expect(result).toHaveProperty('sz');
111
- expect(typeof result.sz).toBe('function');
112
- });
113
-
114
- it('sz function delegates to dynamic()', () => {
115
- const { sz: hookSz } = useSz();
116
- hookSz({ m: 2 } as never);
117
- expect(mockDynamic).toHaveBeenCalledWith({ m: 2 });
118
- });
119
-
120
- it('sz reference is stable (useCallback wraps it)', () => {
121
- const { sz: hookSz } = useSz();
122
- // capturedCallback is what useCallback returned — same as hookSz
123
- expect(hookSz).toBe(capturedCallback);
124
- });
125
-
126
- it('reads manifestUrl from context', () => {
127
- contextValue = { manifestUrl: '/custom/path.json' };
128
- useSz();
129
- runCapturedEffects();
130
- expect(mockPreloadManifest).toHaveBeenCalledWith('/custom/path.json');
131
- });
132
-
133
- it('calls preloadManifest with default URL on mount', () => {
134
- useSz();
135
- runCapturedEffects();
136
- expect(mockPreloadManifest).toHaveBeenCalledWith('/csszyx-manifest.json');
137
- });
138
-
139
- it('schedules deferred cleanup on unmount', () => {
140
- useSz();
141
- const cleanups = runCapturedEffects();
142
- // Run all cleanup functions (simulate unmount)
143
- cleanups.forEach(fn => fn());
144
- // Cleanup should NOT have fired yet (timer pending)
145
- expect(mockInjectorCleanup).not.toHaveBeenCalled();
146
- expect(mockResetManifest).not.toHaveBeenCalled();
147
- // After timer fires, cleanup runs
148
- vi.runAllTimers();
149
- expect(mockInjectorCleanup).toHaveBeenCalledOnce();
150
- expect(mockResetManifest).toHaveBeenCalledOnce();
151
- });
152
-
153
- it('cancels pending cleanup when remounted (StrictMode resilience)', () => {
154
- // First mount
155
- useSz();
156
- const firstCleanups = runCapturedEffects();
157
-
158
- // StrictMode unmount — schedules cleanup timer
159
- firstCleanups.forEach(fn => fn());
160
- expect(mockInjectorCleanup).not.toHaveBeenCalled();
161
-
162
- // StrictMode remount — second useSz() call, effect runs again, cancels timer
163
- capturedEffects = [];
164
- useSz();
165
- runCapturedEffects();
166
-
167
- // Timer fires — should have been cancelled by the second mount
168
- vi.runAllTimers();
169
- expect(mockInjectorCleanup).not.toHaveBeenCalled();
170
- expect(mockResetManifest).not.toHaveBeenCalled();
171
- });
172
-
173
- it('runs cleanup after true unmount (no remount)', () => {
174
- useSz();
175
- const cleanups = runCapturedEffects();
176
- cleanups.forEach(fn => fn());
177
- // No remount — timer fires
178
- vi.runAllTimers();
179
- expect(mockInjectorCleanup).toHaveBeenCalledOnce();
180
- expect(mockResetManifest).toHaveBeenCalledOnce();
181
- });
182
- });
183
-
184
- describe('CsszyxProvider', () => {
185
- beforeEach(() => {
186
- capturedEffects = [];
187
- vi.clearAllMocks();
188
- });
189
-
190
- it('calls setManifestUrl with the manifest prop', () => {
191
- CsszyxProvider({ manifest: '/api/manifest.json', children: null });
192
- runCapturedEffects();
193
- expect(mockSetManifestUrl).toHaveBeenCalledWith('/api/manifest.json');
194
- });
195
-
196
- it('calls preloadManifest with the manifest prop', () => {
197
- CsszyxProvider({ manifest: '/api/manifest.json', children: null });
198
- runCapturedEffects();
199
- expect(mockPreloadManifest).toHaveBeenCalledWith('/api/manifest.json');
200
- });
201
-
202
- it('re-runs effect when manifest URL changes', () => {
203
- CsszyxProvider({ manifest: '/v1/manifest.json', children: null });
204
- CsszyxProvider({ manifest: '/v2/manifest.json', children: null });
205
- runCapturedEffects();
206
- expect(mockSetManifestUrl).toHaveBeenCalledWith('/v1/manifest.json');
207
- expect(mockSetManifestUrl).toHaveBeenCalledWith('/v2/manifest.json');
208
- expect(mockPreloadManifest).toHaveBeenCalledWith('/v2/manifest.json');
209
- });
210
-
211
- it('renders children via createElement', async () => {
212
- const { createElement } = await import('react');
213
- const children = 'child-content';
214
- CsszyxProvider({ manifest: '/manifest.json', children });
215
- expect(createElement).toHaveBeenCalledWith(
216
- expect.anything(),
217
- expect.objectContaining({ value: { manifestUrl: '/manifest.json' } }),
218
- children,
219
- );
220
- });
221
- });
package/tsconfig.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "rootDir": "./src",
5
- "outDir": "./dist",
6
- "types": ["vitest/globals"]
7
- },
8
- "include": ["src/**/*"],
9
- "exclude": ["dist", "node_modules"]
10
- }
package/vitest.config.ts DELETED
@@ -1,8 +0,0 @@
1
- import { defineConfig } from 'vitest/config';
2
-
3
- export default defineConfig({
4
- test: {
5
- globals: true,
6
- environment: 'node',
7
- },
8
- });