@bookklik/senangstart-css 0.2.10 → 0.2.12
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.
- package/.agent/skills/add-utility/SKILL.md +65 -0
- package/.agent/workflows/add-utility.md +2 -0
- package/.agent/workflows/build.md +2 -0
- package/.agent/workflows/dev.md +2 -0
- package/AGENTS.md +30 -0
- package/dist/senangstart-css.js +362 -151
- package/dist/senangstart-css.min.js +175 -174
- package/dist/senangstart-tw.js +4 -4
- package/dist/senangstart-tw.min.js +1 -1
- package/docs/ms/reference/visual/ring-color.md +2 -2
- package/docs/ms/reference/visual/ring-offset.md +3 -3
- package/docs/ms/reference/visual/ring.md +5 -5
- package/docs/public/assets/senangstart-css.min.js +175 -174
- package/docs/public/llms.txt +10 -10
- package/docs/reference/visual/ring-color.md +2 -2
- package/docs/reference/visual/ring-offset.md +3 -3
- package/docs/reference/visual/ring.md +5 -5
- package/package.json +1 -1
- package/src/cdn/tw-conversion-engine.js +4 -4
- package/src/cli/commands/build.js +42 -14
- package/src/cli/commands/dev.js +157 -93
- package/src/compiler/generators/css.js +371 -199
- package/src/compiler/tokenizer.js +25 -23
- package/src/core/tokenizer-core.js +46 -19
- package/src/definitions/visual-borders.js +10 -10
- package/src/utils/common.js +456 -39
- package/src/utils/node-io.js +82 -0
- package/tests/integration/dev-recovery.test.js +231 -0
- package/tests/unit/cli/memory-limits.test.js +169 -0
- package/tests/unit/compiler/css-generation-error-handling.test.js +204 -0
- package/tests/unit/compiler/generators/css-errors.test.js +102 -0
- package/tests/unit/convert-tailwind.test.js +518 -442
- package/tests/unit/utils/common.test.js +376 -26
- package/tests/unit/utils/file-timeout.test.js +154 -0
- package/tests/unit/utils/theme-validation.test.js +181 -0
- package/tests/unit/compiler/generators/css.coverage.test.js +0 -833
- package/tests/unit/convert-tailwind.cli.test.js +0 -95
- package/tests/unit/security.test.js +0 -206
- /package/tests/unit/{convert-tailwind.coverage.test.js → convert-tailwind-edgecases.test.js} +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangStart CSS - Node.js I/O Utilities
|
|
3
|
+
* Node-specific helper functions (not for browser use)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Read file with timeout protection
|
|
8
|
+
* @param {string} filePath - Path to file
|
|
9
|
+
* @param {number} timeoutMs - Timeout in milliseconds
|
|
10
|
+
* @returns {Promise<string>} - File contents
|
|
11
|
+
* @throws {Error} - On timeout or read failure
|
|
12
|
+
*/
|
|
13
|
+
export async function readFileWithTimeout(filePath, timeoutMs = 5000) {
|
|
14
|
+
const { promises: fsPromises, statSync } = await import('fs');
|
|
15
|
+
|
|
16
|
+
// Check file size first
|
|
17
|
+
let fileSize;
|
|
18
|
+
try {
|
|
19
|
+
const stats = statSync(filePath);
|
|
20
|
+
fileSize = stats.size;
|
|
21
|
+
|
|
22
|
+
// Reject files larger than 10MB
|
|
23
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
24
|
+
if (fileSize > MAX_FILE_SIZE) {
|
|
25
|
+
throw new Error(`File too large: ${filePath} (${Math.round(fileSize / 1024)}KB)`);
|
|
26
|
+
}
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw new Error(`Cannot stat file: ${filePath} - ${error.message}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const timeout = setTimeout(() => {
|
|
33
|
+
reject(new Error(`Read timeout for ${filePath} (exceeded ${timeoutMs}ms)`));
|
|
34
|
+
}, timeoutMs);
|
|
35
|
+
|
|
36
|
+
fsPromises.readFile(filePath, 'utf-8')
|
|
37
|
+
.then((content) => {
|
|
38
|
+
clearTimeout(timeout);
|
|
39
|
+
resolve(content);
|
|
40
|
+
})
|
|
41
|
+
.catch((error) => {
|
|
42
|
+
clearTimeout(timeout);
|
|
43
|
+
reject(new Error(`Cannot read file: ${filePath} - ${error.message}`));
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Batch read multiple files with timeout protection
|
|
50
|
+
* @param {Array<string>} filePaths - Paths to files
|
|
51
|
+
* @param {number} timeoutMs - Timeout per file
|
|
52
|
+
* @returns {Promise<Array<{path: string, content: string, error: Error|null}>>} - File contents
|
|
53
|
+
*/
|
|
54
|
+
export async function readMultipleFilesWithTimeout(filePaths, timeoutMs = 5000) {
|
|
55
|
+
const results = [];
|
|
56
|
+
|
|
57
|
+
// Read files in batches to avoid overwhelming the system
|
|
58
|
+
const BATCH_SIZE = 10;
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < filePaths.length; i += BATCH_SIZE) {
|
|
61
|
+
const batch = filePaths.slice(i, i + BATCH_SIZE);
|
|
62
|
+
|
|
63
|
+
const batchPromises = batch.map(async (filePath) => {
|
|
64
|
+
try {
|
|
65
|
+
const content = await readFileWithTimeout(filePath, timeoutMs);
|
|
66
|
+
return { path: filePath, content, error: null };
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return { path: filePath, content: null, error };
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const batchResults = await Promise.all(batchPromises);
|
|
73
|
+
results.push(...batchResults);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return results;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default {
|
|
80
|
+
readFileWithTimeout,
|
|
81
|
+
readMultipleFilesWithTimeout
|
|
82
|
+
};
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev Mode Error Recovery Tests
|
|
3
|
+
* Tests for error recovery mechanisms in development mode
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, mock } from 'node:test';
|
|
7
|
+
import assert from 'node:assert';
|
|
8
|
+
|
|
9
|
+
describe('Dev Mode Error Recovery', () => {
|
|
10
|
+
describe('Consecutive Error Tracking', () => {
|
|
11
|
+
it('tracks consecutive build failures', async () => {
|
|
12
|
+
let errorCount = 0;
|
|
13
|
+
const MAX_ERRORS = 5;
|
|
14
|
+
|
|
15
|
+
const mockBuild = async () => {
|
|
16
|
+
errorCount++;
|
|
17
|
+
if (errorCount < MAX_ERRORS) {
|
|
18
|
+
throw new Error('Simulated build error');
|
|
19
|
+
}
|
|
20
|
+
return { css: '', tokens: [] };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Simulate consecutive errors
|
|
24
|
+
for (let i = 0; i < MAX_ERRORS; i++) {
|
|
25
|
+
try {
|
|
26
|
+
await mockBuild();
|
|
27
|
+
} catch (e) {
|
|
28
|
+
// Expected error
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
assert.strictEqual(errorCount, MAX_ERRORS);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('resets error count on successful build', async () => {
|
|
36
|
+
let errorCount = 0;
|
|
37
|
+
const MAX_ERRORS = 3;
|
|
38
|
+
|
|
39
|
+
const mockBuild = async (forceError = false) => {
|
|
40
|
+
if (forceError) {
|
|
41
|
+
errorCount++;
|
|
42
|
+
throw new Error('Simulated build error');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Successful build resets count
|
|
46
|
+
errorCount = 0;
|
|
47
|
+
return { css: '', tokens: [] };
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Accumulate errors
|
|
51
|
+
for (let i = 0; i < MAX_ERRORS; i++) {
|
|
52
|
+
try {
|
|
53
|
+
await mockBuild(true);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
// Expected error
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Error count should be MAX_ERRORS after failures
|
|
60
|
+
assert.strictEqual(errorCount, MAX_ERRORS);
|
|
61
|
+
|
|
62
|
+
// Successful build should reset count
|
|
63
|
+
await mockBuild(false);
|
|
64
|
+
assert.strictEqual(errorCount, 0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('prevents builds during cooldown period', async () => {
|
|
68
|
+
let errorCount = 0;
|
|
69
|
+
let cooldownActive = false;
|
|
70
|
+
const MAX_ERRORS = 5;
|
|
71
|
+
|
|
72
|
+
const mockBuild = async () => {
|
|
73
|
+
if (cooldownActive) {
|
|
74
|
+
throw new Error('Cooldown active');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
errorCount++;
|
|
78
|
+
if (errorCount >= MAX_ERRORS) {
|
|
79
|
+
cooldownActive = true;
|
|
80
|
+
}
|
|
81
|
+
throw new Error('Simulated build error');
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Trigger cooldown
|
|
85
|
+
for (let i = 0; i < MAX_ERRORS; i++) {
|
|
86
|
+
try {
|
|
87
|
+
await mockBuild();
|
|
88
|
+
} catch (e) {
|
|
89
|
+
// Expected error
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
assert.strictEqual(cooldownActive, true);
|
|
94
|
+
|
|
95
|
+
// Try to build during cooldown
|
|
96
|
+
let cooldownError = null;
|
|
97
|
+
try {
|
|
98
|
+
await mockBuild();
|
|
99
|
+
} catch (e) {
|
|
100
|
+
cooldownError = e.message;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
assert.strictEqual(cooldownError, 'Cooldown active');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('Error Messages', () => {
|
|
108
|
+
it('logs error count when builds fail', async () => {
|
|
109
|
+
const errorLog = [];
|
|
110
|
+
let errorCount = 0;
|
|
111
|
+
|
|
112
|
+
const mockLogger = {
|
|
113
|
+
error: (msg) => errorLog.push(msg),
|
|
114
|
+
warn: (msg) => errorLog.push(msg),
|
|
115
|
+
info: (msg) => errorLog.push(msg)
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const mockBuild = async () => {
|
|
119
|
+
errorCount++;
|
|
120
|
+
mockLogger.error(`Build failed (${errorCount}/5): Test error`);
|
|
121
|
+
throw new Error('Test error');
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
for (let i = 0; i < 3; i++) {
|
|
125
|
+
try {
|
|
126
|
+
await mockBuild();
|
|
127
|
+
} catch (e) {
|
|
128
|
+
// Expected error
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
assert.ok(errorLog.some(msg => msg.includes('Build failed (1/5)')));
|
|
133
|
+
assert.ok(errorLog.some(msg => msg.includes('Build failed (2/5)')));
|
|
134
|
+
assert.ok(errorLog.some(msg => msg.includes('Build failed (3/5)')));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('logs cooldown activation message', async () => {
|
|
138
|
+
const errorLog = [];
|
|
139
|
+
const COOLDOWN_DURATION = 30000;
|
|
140
|
+
|
|
141
|
+
const mockLogger = {
|
|
142
|
+
error: (msg) => errorLog.push(msg),
|
|
143
|
+
warn: (msg) => errorLog.push(msg)
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const mockBuild = async () => {
|
|
147
|
+
mockLogger.warn(`Maximum consecutive errors (5) reached.`);
|
|
148
|
+
mockLogger.warn(`Entering ${COOLDOWN_DURATION / 1000}s cooldown to prevent resource exhaustion.`);
|
|
149
|
+
throw new Error('Max errors reached');
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
await mockBuild();
|
|
154
|
+
} catch (e) {
|
|
155
|
+
// Expected error
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
assert.ok(errorLog.some(msg => msg.includes('Maximum consecutive errors')));
|
|
159
|
+
assert.ok(errorLog.some(msg => msg.includes('cooldown')));
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('Build Lock Mechanism', () => {
|
|
164
|
+
it('prevents overlapping builds', async () => {
|
|
165
|
+
let buildInProgress = false;
|
|
166
|
+
let pendingBuild = false;
|
|
167
|
+
let buildCount = 0;
|
|
168
|
+
|
|
169
|
+
const mockBuild = async () => {
|
|
170
|
+
if (buildInProgress) {
|
|
171
|
+
pendingBuild = true;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
buildInProgress = true;
|
|
176
|
+
buildCount++;
|
|
177
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
178
|
+
buildInProgress = false;
|
|
179
|
+
|
|
180
|
+
if (pendingBuild) {
|
|
181
|
+
pendingBuild = false;
|
|
182
|
+
await mockBuild();
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Trigger multiple builds in parallel
|
|
187
|
+
await Promise.all([
|
|
188
|
+
mockBuild(),
|
|
189
|
+
mockBuild(),
|
|
190
|
+
mockBuild()
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
// Should execute at least 2 builds (one initial, one from pending)
|
|
194
|
+
assert.ok(buildCount >= 2);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('processes pending builds after current build completes', async () => {
|
|
198
|
+
let buildInProgress = false;
|
|
199
|
+
let pendingBuild = false;
|
|
200
|
+
let buildCount = 0;
|
|
201
|
+
|
|
202
|
+
const mockBuild = async () => {
|
|
203
|
+
if (buildInProgress) {
|
|
204
|
+
pendingBuild = true;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
buildInProgress = true;
|
|
209
|
+
buildCount++;
|
|
210
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
211
|
+
buildInProgress = false;
|
|
212
|
+
|
|
213
|
+
if (pendingBuild) {
|
|
214
|
+
pendingBuild = false;
|
|
215
|
+
await mockBuild();
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// Trigger build
|
|
220
|
+
mockBuild();
|
|
221
|
+
|
|
222
|
+
// Immediately trigger another build while first is in progress
|
|
223
|
+
setTimeout(() => mockBuild(), 10);
|
|
224
|
+
|
|
225
|
+
// Wait for all builds to complete
|
|
226
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
227
|
+
|
|
228
|
+
assert.strictEqual(buildCount, 2);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Usage Limits Tests
|
|
3
|
+
* Tests for memory protection and batch processing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it } from 'node:test';
|
|
7
|
+
import assert from 'node:assert';
|
|
8
|
+
import { getMemoryUsage, isMemorySafe, batchProcessWithMemoryLimit, batchProcessTokens } from '../../../src/utils/common.js';
|
|
9
|
+
|
|
10
|
+
describe('Memory Usage Limits', () => {
|
|
11
|
+
describe('getMemoryUsage', () => {
|
|
12
|
+
it('returns a number', () => {
|
|
13
|
+
const memory = getMemoryUsage();
|
|
14
|
+
assert.ok(typeof memory === 'number');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns positive value', () => {
|
|
18
|
+
const memory = getMemoryUsage();
|
|
19
|
+
assert.ok(memory >= 0);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns value in reasonable range', () => {
|
|
23
|
+
const memory = getMemoryUsage();
|
|
24
|
+
// Should be between 0 and a few GB in typical usage
|
|
25
|
+
assert.ok(memory < 10000); // Less than 10GB
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('isMemorySafe', () => {
|
|
30
|
+
it('returns true for normal memory usage', () => {
|
|
31
|
+
const safe = isMemorySafe(500);
|
|
32
|
+
assert.strictEqual(typeof safe, 'boolean');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('uses default limit of 500MB', () => {
|
|
36
|
+
const safe = isMemorySafe();
|
|
37
|
+
assert.strictEqual(typeof safe, 'boolean');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('respects custom memory limit', () => {
|
|
41
|
+
const safe = isMemorySafe(1000);
|
|
42
|
+
assert.strictEqual(typeof safe, 'boolean');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns false when memory exceeds limit', () => {
|
|
46
|
+
// Use a very low limit to ensure current usage exceeds it
|
|
47
|
+
const safe = isMemorySafe(1);
|
|
48
|
+
assert.strictEqual(safe, false);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('batchProcessWithMemoryLimit', () => {
|
|
53
|
+
it('processes all items in array', async () => {
|
|
54
|
+
const items = [1, 2, 3, 4, 5];
|
|
55
|
+
const processor = (item) => item * 2;
|
|
56
|
+
|
|
57
|
+
const results = await batchProcessWithMemoryLimit(items, processor, 10, 500);
|
|
58
|
+
assert.deepStrictEqual(results, [2, 4, 6, 8, 10]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('processes items in batches', async () => {
|
|
62
|
+
const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
|
63
|
+
const processor = (item) => item * 2;
|
|
64
|
+
|
|
65
|
+
const results = await batchProcessWithMemoryLimit(items, processor, 3, 500);
|
|
66
|
+
assert.deepStrictEqual(results, [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('handles empty array', async () => {
|
|
70
|
+
const results = await batchProcessWithMemoryLimit([], (item) => item, 10, 500);
|
|
71
|
+
assert.deepStrictEqual(results, []);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('handles large arrays without crashing', async () => {
|
|
75
|
+
const items = Array(100).fill(0).map((_, i) => i);
|
|
76
|
+
const processor = (item) => item * 2;
|
|
77
|
+
|
|
78
|
+
const results = await batchProcessWithMemoryLimit(items, processor, 20, 500);
|
|
79
|
+
assert.strictEqual(results.length, 100);
|
|
80
|
+
assert.strictEqual(results[0], 0);
|
|
81
|
+
assert.strictEqual(results[99], 198);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('preserves item order', async () => {
|
|
85
|
+
const items = [5, 2, 8, 1, 9];
|
|
86
|
+
const processor = (item) => item;
|
|
87
|
+
|
|
88
|
+
const results = await batchProcessWithMemoryLimit(items, processor, 2, 500);
|
|
89
|
+
assert.deepStrictEqual(results, [5, 2, 8, 1, 9]);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('batchProcessTokens', () => {
|
|
94
|
+
it('processes token objects', async () => {
|
|
95
|
+
const tokens = [
|
|
96
|
+
{ raw: 'flex', attrType: 'layout' },
|
|
97
|
+
{ raw: 'center', attrType: 'layout' },
|
|
98
|
+
{ raw: 'p:medium', attrType: 'space' }
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const processor = (token) => ({ ...token, processed: true });
|
|
102
|
+
|
|
103
|
+
const results = await batchProcessTokens(tokens, processor, 2);
|
|
104
|
+
assert.strictEqual(results.length, 3);
|
|
105
|
+
assert.ok(results.every(r => r.processed === true));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('handles large token arrays', async () => {
|
|
109
|
+
const tokens = Array(1000).fill(null).map((_, i) => ({
|
|
110
|
+
raw: `test${i}`,
|
|
111
|
+
attrType: 'layout'
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
const processor = (token) => ({ ...token, index: tokens.indexOf(token) });
|
|
115
|
+
|
|
116
|
+
const results = await batchProcessTokens(tokens, processor, 100);
|
|
117
|
+
assert.strictEqual(results.length, 1000);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('preserves token data', async () => {
|
|
121
|
+
const tokens = [
|
|
122
|
+
{ raw: 'flex', attrType: 'layout', property: 'flex' },
|
|
123
|
+
{ raw: 'grid', attrType: 'layout', property: 'grid' }
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const processor = (token) => token;
|
|
127
|
+
|
|
128
|
+
const results = await batchProcessTokens(tokens, processor, 1);
|
|
129
|
+
assert.deepStrictEqual(results, tokens);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('Memory Error Handling', () => {
|
|
134
|
+
it('processes items without memory errors under normal conditions', async () => {
|
|
135
|
+
// This test verifies the memory limit handling works under normal conditions
|
|
136
|
+
// Actual memory limit errors are difficult to test in unit test environment
|
|
137
|
+
|
|
138
|
+
const items = [1, 2, 3];
|
|
139
|
+
const processor = (item) => item;
|
|
140
|
+
|
|
141
|
+
const results = await batchProcessWithMemoryLimit(items, processor, 1, 500);
|
|
142
|
+
assert.strictEqual(results.length, 3);
|
|
143
|
+
assert.deepStrictEqual(results, [1, 2, 3]);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('Batch Size Configuration', () => {
|
|
148
|
+
it('respects custom batch size', async () => {
|
|
149
|
+
const items = [1, 2, 3, 4, 5];
|
|
150
|
+
const batchesProcessed = [];
|
|
151
|
+
|
|
152
|
+
const processor = (item) => {
|
|
153
|
+
batchesProcessed.push(item);
|
|
154
|
+
return item * 2;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
await batchProcessWithMemoryLimit(items, processor, 2, 500);
|
|
158
|
+
assert.strictEqual(batchesProcessed.length, 5);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('uses default batch size of 1000', async () => {
|
|
162
|
+
const items = Array(500).fill(0).map((_, i) => i);
|
|
163
|
+
const processor = (item) => item * 2;
|
|
164
|
+
|
|
165
|
+
const results = await batchProcessTokens(items, processor);
|
|
166
|
+
assert.strictEqual(results.length, 500);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS Generator Error Handling Tests
|
|
3
|
+
* Tests for error boundaries and graceful degradation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it } from 'node:test';
|
|
7
|
+
import assert from 'node:assert';
|
|
8
|
+
import { generateRule, generateCSS } from '../../../src/compiler/generators/css.js';
|
|
9
|
+
import { defaultConfig } from '../../../src/config/defaults.js';
|
|
10
|
+
|
|
11
|
+
describe('CSS Generator Error Handling', () => {
|
|
12
|
+
describe('generateRule', () => {
|
|
13
|
+
it('handles null token gracefully', () => {
|
|
14
|
+
const result = generateRule(null, defaultConfig);
|
|
15
|
+
assert.strictEqual(result, '');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('handles undefined token gracefully', () => {
|
|
19
|
+
const result = generateRule(undefined, defaultConfig);
|
|
20
|
+
assert.strictEqual(result, '');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('handles token with missing attrType', () => {
|
|
24
|
+
const token = {
|
|
25
|
+
raw: 'flex',
|
|
26
|
+
property: 'flex',
|
|
27
|
+
value: 'flex'
|
|
28
|
+
};
|
|
29
|
+
const result = generateRule(token, defaultConfig);
|
|
30
|
+
assert.strictEqual(result, '');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('handles token with missing raw value', () => {
|
|
34
|
+
const token = {
|
|
35
|
+
attrType: 'layout',
|
|
36
|
+
property: 'flex',
|
|
37
|
+
value: 'flex'
|
|
38
|
+
};
|
|
39
|
+
const result = generateRule(token, defaultConfig);
|
|
40
|
+
assert.strictEqual(result, '');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('handles token with invalid attrType', () => {
|
|
44
|
+
const token = {
|
|
45
|
+
raw: 'flex',
|
|
46
|
+
attrType: 'invalid',
|
|
47
|
+
property: 'flex',
|
|
48
|
+
value: 'flex'
|
|
49
|
+
};
|
|
50
|
+
const result = generateRule(token, defaultConfig);
|
|
51
|
+
assert.strictEqual(result, '');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('handles malformed layout rule without crashing', () => {
|
|
55
|
+
const token = {
|
|
56
|
+
raw: 'test',
|
|
57
|
+
attrType: 'layout',
|
|
58
|
+
property: null,
|
|
59
|
+
value: null
|
|
60
|
+
};
|
|
61
|
+
const result = generateRule(token, defaultConfig);
|
|
62
|
+
assert.strictEqual(result, '');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('handles malformed space rule without crashing', () => {
|
|
66
|
+
const token = {
|
|
67
|
+
raw: 'test',
|
|
68
|
+
attrType: 'space',
|
|
69
|
+
property: null,
|
|
70
|
+
value: null
|
|
71
|
+
};
|
|
72
|
+
const result = generateRule(token, defaultConfig);
|
|
73
|
+
assert.strictEqual(result, '');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('handles malformed visual rule without crashing', () => {
|
|
77
|
+
const token = {
|
|
78
|
+
raw: 'test',
|
|
79
|
+
attrType: 'visual',
|
|
80
|
+
property: null,
|
|
81
|
+
value: null
|
|
82
|
+
};
|
|
83
|
+
const result = generateRule(token, defaultConfig);
|
|
84
|
+
assert.strictEqual(result, '');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('generateCSS', () => {
|
|
89
|
+
it('handles null tokens array', () => {
|
|
90
|
+
const result = generateCSS(null, defaultConfig);
|
|
91
|
+
assert.strictEqual(result, '');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('handles undefined tokens array', () => {
|
|
95
|
+
const result = generateCSS(undefined, defaultConfig);
|
|
96
|
+
assert.strictEqual(result, '');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('handles empty tokens array', () => {
|
|
100
|
+
const result = generateCSS([], defaultConfig);
|
|
101
|
+
assert.ok(typeof result === 'string');
|
|
102
|
+
assert.ok(result.includes(':root'));
|
|
103
|
+
assert.ok(!result.includes('[layout~="'));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('handles malformed tokens without crashing', () => {
|
|
107
|
+
const malformedTokens = [
|
|
108
|
+
null,
|
|
109
|
+
undefined,
|
|
110
|
+
{ raw: 'test' },
|
|
111
|
+
{ attrType: 'layout' },
|
|
112
|
+
{ raw: 'test', attrType: 'invalid' }
|
|
113
|
+
];
|
|
114
|
+
const result = generateCSS(malformedTokens, defaultConfig);
|
|
115
|
+
assert.ok(typeof result === 'string');
|
|
116
|
+
assert.ok(result.includes(':root'));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('handles valid tokens mixed with malformed ones', () => {
|
|
120
|
+
const mixedTokens = [
|
|
121
|
+
{ raw: 'flex', attrType: 'layout', property: 'flex', value: 'flex' },
|
|
122
|
+
null,
|
|
123
|
+
{ raw: 'center', attrType: 'layout', property: 'items', value: 'center' },
|
|
124
|
+
undefined,
|
|
125
|
+
{ raw: 'p:medium', attrType: 'space', property: 'p', value: 'medium' }
|
|
126
|
+
];
|
|
127
|
+
const result = generateCSS(mixedTokens, defaultConfig);
|
|
128
|
+
assert.ok(typeof result === 'string');
|
|
129
|
+
assert.ok(result.includes('[layout~="flex"]'));
|
|
130
|
+
assert.ok(result.includes('[layout~="center"]'));
|
|
131
|
+
assert.ok(result.includes('[space~="p:medium"]'));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('handles null config gracefully', () => {
|
|
135
|
+
const result = generateCSS([], null);
|
|
136
|
+
assert.strictEqual(result, '');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('handles invalid config gracefully', () => {
|
|
140
|
+
const result = generateCSS([], 'invalid');
|
|
141
|
+
assert.strictEqual(result, '');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('handles config with missing theme', () => {
|
|
145
|
+
const invalidConfig = { ...defaultConfig, theme: null };
|
|
146
|
+
const result = generateCSS([], invalidConfig);
|
|
147
|
+
assert.ok(typeof result === 'string');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('handles tokens with invalid state values', () => {
|
|
151
|
+
const invalidTokens = [
|
|
152
|
+
{ raw: 'flex:invalid', attrType: 'layout', property: 'flex', value: 'flex', state: 'invalid-state' }
|
|
153
|
+
];
|
|
154
|
+
const result = generateCSS(invalidTokens, defaultConfig);
|
|
155
|
+
assert.ok(typeof result === 'string');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('handles tokens with invalid breakpoint values', () => {
|
|
159
|
+
const invalidTokens = [
|
|
160
|
+
{ raw: 'invalid:flex', attrType: 'layout', property: 'flex', value: 'flex', breakpoint: 'invalid-bp' }
|
|
161
|
+
];
|
|
162
|
+
const result = generateCSS(invalidTokens, defaultConfig);
|
|
163
|
+
assert.ok(typeof result === 'string');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('CSS Rule Validation', () => {
|
|
168
|
+
it('always generates valid declarations with semicolons', () => {
|
|
169
|
+
const token = {
|
|
170
|
+
raw: 'flex',
|
|
171
|
+
attrType: 'layout',
|
|
172
|
+
property: 'flex',
|
|
173
|
+
value: 'flex'
|
|
174
|
+
};
|
|
175
|
+
const result = generateRule(token, defaultConfig);
|
|
176
|
+
assert.ok(result.includes('{'));
|
|
177
|
+
assert.ok(result.includes('}'));
|
|
178
|
+
assert.ok(result.includes(';'));
|
|
179
|
+
assert.ok(result.endsWith('}\n'));
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('rejects declarations without property', () => {
|
|
183
|
+
const token = {
|
|
184
|
+
raw: 'test',
|
|
185
|
+
attrType: 'layout',
|
|
186
|
+
property: '',
|
|
187
|
+
value: 'flex'
|
|
188
|
+
};
|
|
189
|
+
const result = generateRule(token, defaultConfig);
|
|
190
|
+
assert.strictEqual(result, '');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('rejects declarations without value', () => {
|
|
194
|
+
const token = {
|
|
195
|
+
raw: 'test',
|
|
196
|
+
attrType: 'layout',
|
|
197
|
+
property: 'display',
|
|
198
|
+
value: ''
|
|
199
|
+
};
|
|
200
|
+
const result = generateRule(token, defaultConfig);
|
|
201
|
+
assert.strictEqual(result, '');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|