@csszyx/dynamic 0.4.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.
@@ -0,0 +1,177 @@
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
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
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
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ },
8
+ });