edsger 0.45.0 → 0.45.1

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.
Files changed (95) hide show
  1. package/package.json +3 -3
  2. package/tsconfig.build.json +4 -0
  3. package/tsconfig.json +3 -9
  4. package/dist/api/__tests__/app-store.test.d.ts +0 -7
  5. package/dist/api/__tests__/app-store.test.js +0 -60
  6. package/dist/api/__tests__/intelligence.test.d.ts +0 -11
  7. package/dist/api/__tests__/intelligence.test.js +0 -315
  8. package/dist/api/features/__tests__/feature-utils.test.d.ts +0 -4
  9. package/dist/api/features/__tests__/feature-utils.test.js +0 -370
  10. package/dist/api/features/__tests__/status-updater.test.d.ts +0 -4
  11. package/dist/api/features/__tests__/status-updater.test.js +0 -88
  12. package/dist/commands/build/__tests__/build.test.d.ts +0 -5
  13. package/dist/commands/build/__tests__/build.test.js +0 -206
  14. package/dist/commands/build/__tests__/detect-project.test.d.ts +0 -6
  15. package/dist/commands/build/__tests__/detect-project.test.js +0 -160
  16. package/dist/commands/build/__tests__/run-build.test.d.ts +0 -6
  17. package/dist/commands/build/__tests__/run-build.test.js +0 -433
  18. package/dist/commands/intelligence/__tests__/command.test.d.ts +0 -4
  19. package/dist/commands/intelligence/__tests__/command.test.js +0 -48
  20. package/dist/commands/workflow/core/__tests__/feature-filter.test.d.ts +0 -5
  21. package/dist/commands/workflow/core/__tests__/feature-filter.test.js +0 -316
  22. package/dist/commands/workflow/core/__tests__/pipeline-evaluator.test.d.ts +0 -4
  23. package/dist/commands/workflow/core/__tests__/pipeline-evaluator.test.js +0 -397
  24. package/dist/commands/workflow/core/__tests__/state-manager.test.d.ts +0 -4
  25. package/dist/commands/workflow/core/__tests__/state-manager.test.js +0 -384
  26. package/dist/config/__tests__/config.test.d.ts +0 -4
  27. package/dist/config/__tests__/config.test.js +0 -286
  28. package/dist/config/__tests__/feature-status.test.d.ts +0 -4
  29. package/dist/config/__tests__/feature-status.test.js +0 -111
  30. package/dist/errors/__tests__/index.test.d.ts +0 -4
  31. package/dist/errors/__tests__/index.test.js +0 -349
  32. package/dist/phases/app-store-generation/__tests__/agent.test.d.ts +0 -5
  33. package/dist/phases/app-store-generation/__tests__/agent.test.js +0 -142
  34. package/dist/phases/app-store-generation/__tests__/context.test.d.ts +0 -4
  35. package/dist/phases/app-store-generation/__tests__/context.test.js +0 -284
  36. package/dist/phases/app-store-generation/__tests__/prompts.test.d.ts +0 -4
  37. package/dist/phases/app-store-generation/__tests__/prompts.test.js +0 -122
  38. package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.d.ts +0 -5
  39. package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.js +0 -826
  40. package/dist/phases/code-review/__tests__/diff-utils.test.d.ts +0 -1
  41. package/dist/phases/code-review/__tests__/diff-utils.test.js +0 -101
  42. package/dist/phases/intelligence-analysis/__tests__/context.test.d.ts +0 -4
  43. package/dist/phases/intelligence-analysis/__tests__/context.test.js +0 -192
  44. package/dist/phases/intelligence-analysis/__tests__/matching.test.d.ts +0 -13
  45. package/dist/phases/intelligence-analysis/__tests__/matching.test.js +0 -154
  46. package/dist/phases/intelligence-analysis/__tests__/orchestration.test.d.ts +0 -5
  47. package/dist/phases/intelligence-analysis/__tests__/orchestration.test.js +0 -378
  48. package/dist/phases/intelligence-analysis/__tests__/prompts.test.d.ts +0 -4
  49. package/dist/phases/intelligence-analysis/__tests__/prompts.test.js +0 -33
  50. package/dist/phases/pr-execution/__tests__/file-assigner.test.d.ts +0 -1
  51. package/dist/phases/pr-execution/__tests__/file-assigner.test.js +0 -303
  52. package/dist/phases/pr-resolve/__tests__/checklist-learner.test.d.ts +0 -1
  53. package/dist/phases/pr-resolve/__tests__/checklist-learner.test.js +0 -157
  54. package/dist/phases/pr-resolve/__tests__/prompts.test.d.ts +0 -1
  55. package/dist/phases/pr-resolve/__tests__/prompts.test.js +0 -116
  56. package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.d.ts +0 -1
  57. package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.js +0 -138
  58. package/dist/phases/pr-resolve/__tests__/types.test.d.ts +0 -1
  59. package/dist/phases/pr-resolve/__tests__/types.test.js +0 -43
  60. package/dist/phases/pr-resolve/__tests__/workspace.test.d.ts +0 -1
  61. package/dist/phases/pr-resolve/__tests__/workspace.test.js +0 -111
  62. package/dist/phases/pr-review/__tests__/prompts.test.d.ts +0 -1
  63. package/dist/phases/pr-review/__tests__/prompts.test.js +0 -49
  64. package/dist/phases/pr-review/__tests__/review-comments.test.d.ts +0 -1
  65. package/dist/phases/pr-review/__tests__/review-comments.test.js +0 -110
  66. package/dist/phases/pr-shared/__tests__/agent-utils.test.d.ts +0 -1
  67. package/dist/phases/pr-shared/__tests__/agent-utils.test.js +0 -91
  68. package/dist/phases/pr-shared/__tests__/context.test.d.ts +0 -1
  69. package/dist/phases/pr-shared/__tests__/context.test.js +0 -94
  70. package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.d.ts +0 -1
  71. package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.js +0 -331
  72. package/dist/phases/release-sync/__tests__/github.test.d.ts +0 -9
  73. package/dist/phases/release-sync/__tests__/github.test.js +0 -123
  74. package/dist/phases/release-sync/__tests__/snapshot.test.d.ts +0 -8
  75. package/dist/phases/release-sync/__tests__/snapshot.test.js +0 -93
  76. package/dist/phases/smoke-test/__tests__/agent.test.d.ts +0 -4
  77. package/dist/phases/smoke-test/__tests__/agent.test.js +0 -85
  78. package/dist/services/coaching/__tests__/coaching-agent.test.d.ts +0 -1
  79. package/dist/services/coaching/__tests__/coaching-agent.test.js +0 -74
  80. package/dist/services/coaching/__tests__/coaching-loop.test.d.ts +0 -1
  81. package/dist/services/coaching/__tests__/coaching-loop.test.js +0 -59
  82. package/dist/services/coaching/__tests__/self-rating.test.d.ts +0 -1
  83. package/dist/services/coaching/__tests__/self-rating.test.js +0 -188
  84. package/dist/services/phase-hooks/__tests__/bindings-fetcher.test.d.ts +0 -1
  85. package/dist/services/phase-hooks/__tests__/bindings-fetcher.test.js +0 -122
  86. package/dist/services/phase-hooks/__tests__/hook-executor.test.d.ts +0 -1
  87. package/dist/services/phase-hooks/__tests__/hook-executor.test.js +0 -321
  88. package/dist/services/phase-hooks/__tests__/hook-runner.test.d.ts +0 -1
  89. package/dist/services/phase-hooks/__tests__/hook-runner.test.js +0 -261
  90. package/dist/services/phase-hooks/__tests__/plugin-loader.test.d.ts +0 -1
  91. package/dist/services/phase-hooks/__tests__/plugin-loader.test.js +0 -158
  92. package/dist/services/video/__tests__/video-pipeline.test.d.ts +0 -6
  93. package/dist/services/video/__tests__/video-pipeline.test.js +0 -249
  94. package/dist/workspace/__tests__/workspace-manager.test.d.ts +0 -7
  95. package/dist/workspace/__tests__/workspace-manager.test.js +0 -52
@@ -1,826 +0,0 @@
1
- /* eslint-disable max-lines */
2
- /**
3
- * Unit tests for screenshot composer pure functions.
4
- * Non-exported functions are re-implemented inline for testing (project pattern).
5
- */
6
- import assert from 'node:assert';
7
- import { describe, it } from 'node:test';
8
- import { STORE_SCREENSHOT_SIZES } from '../prompts.js';
9
- function escapeHtml(str) {
10
- return str
11
- .replace(/&/g, '&')
12
- .replace(/</g, '&lt;')
13
- .replace(/>/g, '&gt;')
14
- .replace(/"/g, '&quot;');
15
- }
16
- function mapDeviceFrame(frame) {
17
- switch (frame.toLowerCase()) {
18
- case 'iphone':
19
- return 'iphone';
20
- case 'ipad':
21
- return 'ipad';
22
- case 'macbook':
23
- return 'macbook';
24
- case 'browser':
25
- return 'browser';
26
- default:
27
- return 'iphone';
28
- }
29
- }
30
- const NAMED_COLORS = {
31
- white: [255, 255, 255],
32
- black: [0, 0, 0],
33
- red: [255, 0, 0],
34
- green: [0, 128, 0],
35
- blue: [0, 0, 255],
36
- yellow: [255, 255, 0],
37
- cyan: [0, 255, 255],
38
- magenta: [255, 0, 255],
39
- gray: [128, 128, 128],
40
- grey: [128, 128, 128],
41
- orange: [255, 165, 0],
42
- purple: [128, 0, 128],
43
- pink: [255, 192, 203],
44
- };
45
- function parseColor(color) {
46
- const s = color.trim().toLowerCase();
47
- if (NAMED_COLORS[s]) {
48
- return NAMED_COLORS[s];
49
- }
50
- const rgbMatch = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/.exec(s);
51
- if (rgbMatch) {
52
- return [parseInt(rgbMatch[1]), parseInt(rgbMatch[2]), parseInt(rgbMatch[3])];
53
- }
54
- const hexMatch = /^#?([0-9a-f]{3,8})$/i.exec(s);
55
- if (!hexMatch) {
56
- return null;
57
- }
58
- const h = hexMatch[1];
59
- if (h.length === 3) {
60
- return [
61
- parseInt(h[0] + h[0], 16),
62
- parseInt(h[1] + h[1], 16),
63
- parseInt(h[2] + h[2], 16),
64
- ];
65
- }
66
- if (h.length >= 6) {
67
- return [
68
- parseInt(h.slice(0, 2), 16),
69
- parseInt(h.slice(2, 4), 16),
70
- parseInt(h.slice(4, 6), 16),
71
- ];
72
- }
73
- return null;
74
- }
75
- function colorLuminance(color) {
76
- const rgb = parseColor(color);
77
- if (!rgb) {
78
- return 1.0;
79
- }
80
- const toLinear = (c) => {
81
- const s = c / 255;
82
- return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
83
- };
84
- return (0.2126 * toLinear(rgb[0]) +
85
- 0.7152 * toLinear(rgb[1]) +
86
- 0.0722 * toLinear(rgb[2]));
87
- }
88
- function smoothstep(edge0, edge1, x) {
89
- const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
90
- return t * t * (3 - 2 * t);
91
- }
92
- function contrastStyles(textColor) {
93
- const lum = colorLuminance(textColor);
94
- const lightness = smoothstep(0.15, 0.65, lum);
95
- const ch = Math.round(255 * (1 - lightness));
96
- const certainty = Math.abs(lightness - 0.5) * 2;
97
- const baseOverlay = 0.25 * lightness + 0.2 * (1 - lightness);
98
- const baseShadow = 0.35 * lightness + 0.4 * (1 - lightness);
99
- const baseShadowLight = 0.25 * lightness + 0.3 * (1 - lightness);
100
- const scale = 0.5 + 0.5 * certainty;
101
- const fmt = (alpha) => (alpha * scale).toFixed(2);
102
- return {
103
- overlayColor: `rgba(${ch},${ch},${ch},${fmt(baseOverlay)})`,
104
- shadowColor: `rgba(${ch},${ch},${ch},${fmt(baseShadow)})`,
105
- shadowColorLight: `rgba(${ch},${ch},${ch},${fmt(baseShadowLight)})`,
106
- };
107
- }
108
- // Simulates getInterFontStyle() output for testing (without real base64 font data)
109
- const testFontStyle = `<style>
110
- @font-face {
111
- font-family: 'Inter';
112
- font-style: normal;
113
- font-weight: 100 900;
114
- font-display: swap;
115
- src: url(data:font/woff2;base64,TEST) format('woff2');
116
- }
117
- </style>`;
118
- function createCompositionHtml(framedScreenshotDataUrl, spec, targetWidth, targetHeight) {
119
- const isPortrait = targetHeight > targetWidth;
120
- const textColor = spec.text_color || '#ffffff';
121
- const fontStyle = testFontStyle;
122
- const fontFamily = `'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', sans-serif`;
123
- const { overlayColor, shadowColor, shadowColorLight } = contrastStyles(textColor);
124
- if (isPortrait) {
125
- return `<!DOCTYPE html>
126
- <html>
127
- <head>
128
- ${fontStyle}
129
- <style>
130
- * { margin: 0; padding: 0; box-sizing: border-box; }
131
- body {
132
- width: ${targetWidth}px;
133
- height: ${targetHeight}px;
134
- background: ${spec.background_gradient};
135
- display: flex;
136
- flex-direction: column;
137
- align-items: center;
138
- justify-content: flex-start;
139
- font-family: ${fontFamily};
140
- overflow: hidden;
141
- }
142
- .text-area {
143
- padding: ${Math.round(targetHeight * 0.05)}px ${Math.round(targetWidth * 0.08)}px;
144
- text-align: center;
145
- flex-shrink: 0;
146
- max-height: 28%;
147
- position: relative;
148
- }
149
- .text-area::after {
150
- content: '';
151
- position: absolute;
152
- inset: 0;
153
- background: linear-gradient(to bottom, ${overlayColor} 0%, transparent 100%);
154
- border-radius: inherit;
155
- pointer-events: none;
156
- }
157
- .headline {
158
- font-size: ${Math.round(targetWidth * 0.1)}px;
159
- font-weight: 800;
160
- color: ${textColor};
161
- line-height: 1.15;
162
- letter-spacing: -0.02em;
163
- margin-bottom: ${Math.round(targetHeight * 0.01)}px;
164
- text-shadow: 0 2px 12px ${shadowColor};
165
- position: relative;
166
- z-index: 1;
167
- display: -webkit-box;
168
- -webkit-line-clamp: 3;
169
- -webkit-box-orient: vertical;
170
- overflow: hidden;
171
- }
172
- .subheadline {
173
- font-size: ${Math.round(targetWidth * 0.05)}px;
174
- font-weight: 400;
175
- color: ${textColor};
176
- opacity: 0.85;
177
- line-height: 1.3;
178
- text-shadow: 0 1px 8px ${shadowColorLight};
179
- position: relative;
180
- z-index: 1;
181
- display: -webkit-box;
182
- -webkit-line-clamp: 2;
183
- -webkit-box-orient: vertical;
184
- overflow: hidden;
185
- }
186
- .device-area {
187
- flex: 1;
188
- display: flex;
189
- align-items: center;
190
- justify-content: center;
191
- overflow: hidden;
192
- min-height: 0;
193
- padding: 0 ${Math.round(targetWidth * 0.02)}px;
194
- }
195
- .device-area img {
196
- width: 92%;
197
- }
198
- </style>
199
- </head>
200
- <body>
201
- <div class="text-area">
202
- <div class="headline">${escapeHtml(spec.headline)}</div>
203
- ${spec.subheadline ? `<div class="subheadline">${escapeHtml(spec.subheadline)}</div>` : ''}
204
- </div>
205
- <div class="device-area">
206
- <img src="${framedScreenshotDataUrl}" alt="screenshot" />
207
- </div>
208
- </body>
209
- </html>`;
210
- }
211
- return `<!DOCTYPE html>
212
- <html>
213
- <head>
214
- ${fontStyle}
215
- <style>
216
- * { margin: 0; padding: 0; box-sizing: border-box; }
217
- body {
218
- width: ${targetWidth}px;
219
- height: ${targetHeight}px;
220
- background: ${spec.background_gradient};
221
- display: flex;
222
- align-items: center;
223
- font-family: ${fontFamily};
224
- overflow: hidden;
225
- }
226
- .text-area {
227
- flex: 0 1 auto;
228
- min-width: 28%;
229
- max-width: 44%;
230
- padding: 0 ${Math.round(targetWidth * 0.04)}px;
231
- display: flex;
232
- flex-direction: column;
233
- justify-content: center;
234
- text-align: left;
235
- position: relative;
236
- }
237
- .text-area::after {
238
- content: '';
239
- position: absolute;
240
- inset: 0;
241
- background: linear-gradient(to right, ${overlayColor} 0%, transparent 100%);
242
- border-radius: inherit;
243
- pointer-events: none;
244
- }
245
- .headline {
246
- font-size: ${Math.round(targetWidth * 0.055)}px;
247
- font-weight: 800;
248
- color: ${textColor};
249
- line-height: 1.15;
250
- letter-spacing: -0.02em;
251
- margin-bottom: ${Math.round(targetHeight * 0.02)}px;
252
- text-shadow: 0 2px 12px ${shadowColor};
253
- position: relative;
254
- z-index: 1;
255
- display: -webkit-box;
256
- -webkit-line-clamp: 3;
257
- -webkit-box-orient: vertical;
258
- overflow: hidden;
259
- }
260
- .subheadline {
261
- font-size: ${Math.round(targetWidth * 0.028)}px;
262
- font-weight: 400;
263
- color: ${textColor};
264
- opacity: 0.85;
265
- line-height: 1.4;
266
- text-shadow: 0 1px 8px ${shadowColorLight};
267
- position: relative;
268
- z-index: 1;
269
- display: -webkit-box;
270
- -webkit-line-clamp: 2;
271
- -webkit-box-orient: vertical;
272
- overflow: hidden;
273
- }
274
- .device-area {
275
- flex: 1 1 0;
276
- min-width: 0;
277
- height: 100%;
278
- display: flex;
279
- align-items: center;
280
- justify-content: center;
281
- overflow: hidden;
282
- padding: ${Math.round(targetHeight * 0.03)}px;
283
- }
284
- .device-area img {
285
- max-width: 100%;
286
- max-height: 95%;
287
- object-fit: contain;
288
- }
289
- </style>
290
- </head>
291
- <body>
292
- <div class="text-area">
293
- <div class="headline">${escapeHtml(spec.headline)}</div>
294
- ${spec.subheadline ? `<div class="subheadline">${escapeHtml(spec.subheadline)}</div>` : ''}
295
- </div>
296
- <div class="device-area">
297
- <img src="${framedScreenshotDataUrl}" alt="screenshot" />
298
- </div>
299
- </body>
300
- </html>`;
301
- }
302
- // ============================================================
303
- // colorLuminance + parseColor tests
304
- // ============================================================
305
- void describe('colorLuminance', () => {
306
- void it('should return ~1.0 for white (#ffffff)', () => {
307
- const lum = colorLuminance('#ffffff');
308
- assert.ok(lum > 0.99, `White luminance should be ~1.0, got ${lum}`);
309
- });
310
- void it('should return ~0.0 for black (#000000)', () => {
311
- const lum = colorLuminance('#000000');
312
- assert.ok(lum < 0.01, `Black luminance should be ~0.0, got ${lum}`);
313
- });
314
- void it('should handle 3-digit hex (#fff)', () => {
315
- const lum = colorLuminance('#fff');
316
- assert.ok(lum > 0.99, `#fff luminance should be ~1.0, got ${lum}`);
317
- });
318
- void it('should handle hex without # prefix', () => {
319
- const lum = colorLuminance('ff0000');
320
- assert.ok(lum > 0.1 && lum < 0.3, `Red luminance should be ~0.21, got ${lum}`);
321
- });
322
- void it('should return 1.0 for invalid input (safe default)', () => {
323
- assert.strictEqual(colorLuminance('notacolor'), 1.0);
324
- assert.strictEqual(colorLuminance(''), 1.0);
325
- });
326
- void it('should calculate mid-range luminance for gray (#808080)', () => {
327
- const lum = colorLuminance('#808080');
328
- assert.ok(lum > 0.15 && lum < 0.25, `Gray luminance should be ~0.22, got ${lum}`);
329
- });
330
- void it('should parse rgb() format', () => {
331
- const lum = colorLuminance('rgb(255, 0, 0)');
332
- assert.ok(lum > 0.1 && lum < 0.3, `rgb red luminance should be ~0.21, got ${lum}`);
333
- });
334
- void it('should parse rgba() format', () => {
335
- const lum = colorLuminance('rgba(255, 255, 255, 0.5)');
336
- assert.ok(lum > 0.99, `rgba white luminance should be ~1.0, got ${lum}`);
337
- });
338
- void it('should parse named colors', () => {
339
- assert.ok(colorLuminance('white') > 0.99, 'Named "white" should be ~1.0');
340
- assert.ok(colorLuminance('black') < 0.01, 'Named "black" should be ~0.0');
341
- assert.ok(colorLuminance('Red') > 0.1, 'Named "Red" (case insensitive) should parse');
342
- });
343
- });
344
- // ============================================================
345
- // contrastStyles tests
346
- // ============================================================
347
- void describe('contrastStyles', () => {
348
- void it('should return dark overlay for white text', () => {
349
- const styles = contrastStyles('#ffffff');
350
- assert.strictEqual(styles.overlayColor, 'rgba(0,0,0,0.25)');
351
- assert.strictEqual(styles.shadowColor, 'rgba(0,0,0,0.35)');
352
- });
353
- void it('should return light overlay for black text', () => {
354
- const styles = contrastStyles('#000000');
355
- assert.strictEqual(styles.overlayColor, 'rgba(255,255,255,0.20)');
356
- assert.strictEqual(styles.shadowColor, 'rgba(255,255,255,0.40)');
357
- });
358
- void it('should return dark overlay for light colors like #f0f0f0', () => {
359
- const styles = contrastStyles('#f0f0f0');
360
- assert.ok(styles.overlayColor.startsWith('rgba(0,0,0,'), 'Light gray text should get dark overlay');
361
- });
362
- void it('should return light overlay for dark colors like #333333', () => {
363
- const styles = contrastStyles('#333333');
364
- assert.ok(styles.overlayColor.startsWith('rgba(255,255,255,'), 'Dark gray text should get light overlay');
365
- });
366
- void it('should reduce overlay strength for mid-range text', () => {
367
- // #aaaaaa has luminance ~0.40, right in the smoothstep transition zone
368
- const midStyles = contrastStyles('#aaaaaa');
369
- const midAlpha = parseFloat(midStyles.overlayColor.split(',')[3]);
370
- const extremeAlpha = parseFloat(contrastStyles('#ffffff').overlayColor.split(',')[3]);
371
- assert.ok(midAlpha < extremeAlpha, `Mid-range alpha ${midAlpha} should be less than extreme alpha ${extremeAlpha}`);
372
- });
373
- void it('should produce smooth channel gradient for mid-range text', () => {
374
- // Mid-range text should produce a gray channel, not pure black or white
375
- const styles = contrastStyles('#aaaaaa');
376
- const ch = parseInt(styles.overlayColor.split('(')[1]);
377
- assert.ok(ch > 10 && ch < 245, `Mid-range text should produce gray channel, got ${ch}`);
378
- });
379
- void it('should default to dark overlay for unparseable color', () => {
380
- const styles = contrastStyles('invalid');
381
- assert.strictEqual(styles.overlayColor, 'rgba(0,0,0,0.25)');
382
- });
383
- void it('should work with rgb() input', () => {
384
- const styles = contrastStyles('rgb(255, 255, 255)');
385
- assert.strictEqual(styles.overlayColor, 'rgba(0,0,0,0.25)');
386
- });
387
- void it('should work with named color input', () => {
388
- const styles = contrastStyles('black');
389
- assert.strictEqual(styles.overlayColor, 'rgba(255,255,255,0.20)');
390
- });
391
- });
392
- // ============================================================
393
- // createCompositionHtml - Adaptive contrast
394
- // ============================================================
395
- void describe('createCompositionHtml - Adaptive contrast', () => {
396
- const dataUrl = 'data:image/png;base64,x';
397
- const baseSpec = {
398
- feature_highlight: 'Test',
399
- headline: 'Test',
400
- subheadline: 'Sub',
401
- background_gradient: 'linear-gradient(#000, #111)',
402
- text_color: '#ffffff',
403
- device_frame: 'iphone',
404
- html_template: '<html></html>',
405
- };
406
- void it('should use dark overlay/shadow for white text (#ffffff)', () => {
407
- const html = createCompositionHtml(dataUrl, baseSpec, 1290, 2796);
408
- assert.ok(html.includes('rgba(0,0,0,0.25)'), 'White text should produce dark overlay');
409
- assert.ok(html.includes('rgba(0,0,0,0.35)'), 'White text should produce dark text-shadow');
410
- });
411
- void it('should use light overlay/shadow for dark text (#1a1a1a)', () => {
412
- const darkTextSpec = { ...baseSpec, text_color: '#1a1a1a' };
413
- const html = createCompositionHtml(dataUrl, darkTextSpec, 1290, 2796);
414
- assert.ok(html.includes('rgba(255,255,255,0.20)'), 'Dark text should produce light overlay');
415
- assert.ok(html.includes('rgba(255,255,255,0.40)'), 'Dark text should produce light text-shadow');
416
- });
417
- void it('should use light overlay for landscape with dark text', () => {
418
- const darkTextSpec = { ...baseSpec, text_color: '#000000' };
419
- const html = createCompositionHtml(dataUrl, darkTextSpec, 1920, 1200);
420
- assert.ok(html.includes('rgba(255,255,255,0.20)'), 'Landscape dark text should produce light overlay');
421
- });
422
- void it('should default to dark overlay when text_color is empty', () => {
423
- const emptySpec = { ...baseSpec, text_color: '' };
424
- const html = createCompositionHtml(dataUrl, emptySpec, 1290, 2796);
425
- assert.ok(html.includes('rgba(0,0,0,0.25)'), 'Empty text_color defaults to #ffffff → dark overlay');
426
- });
427
- });
428
- // ============================================================
429
- // escapeHtml tests
430
- // ============================================================
431
- void describe('escapeHtml', () => {
432
- void it('should escape & to &amp;', () => {
433
- assert.strictEqual(escapeHtml('A & B'), 'A &amp; B');
434
- });
435
- void it('should escape < to &lt;', () => {
436
- assert.strictEqual(escapeHtml('<div>'), '&lt;div&gt;');
437
- });
438
- void it('should escape > to &gt;', () => {
439
- assert.strictEqual(escapeHtml('a > b'), 'a &gt; b');
440
- });
441
- void it('should escape " to &quot;', () => {
442
- assert.strictEqual(escapeHtml('say "hello"'), 'say &quot;hello&quot;');
443
- });
444
- void it('should handle string with all special characters', () => {
445
- assert.strictEqual(escapeHtml('<div class="test">&</div>'), '&lt;div class=&quot;test&quot;&gt;&amp;&lt;/div&gt;');
446
- });
447
- void it('should return empty string for empty input', () => {
448
- assert.strictEqual(escapeHtml(''), '');
449
- });
450
- void it('should not modify strings without special characters', () => {
451
- assert.strictEqual(escapeHtml('hello world'), 'hello world');
452
- });
453
- void it('should handle multiple occurrences of same character', () => {
454
- assert.strictEqual(escapeHtml('a & b & c'), 'a &amp; b &amp; c');
455
- });
456
- });
457
- // ============================================================
458
- // mapDeviceFrame tests
459
- // ============================================================
460
- void describe('mapDeviceFrame', () => {
461
- void it('should map "iphone" to "iphone"', () => {
462
- assert.strictEqual(mapDeviceFrame('iphone'), 'iphone');
463
- });
464
- void it('should map "ipad" to "ipad"', () => {
465
- assert.strictEqual(mapDeviceFrame('ipad'), 'ipad');
466
- });
467
- void it('should map "macbook" to "macbook"', () => {
468
- assert.strictEqual(mapDeviceFrame('macbook'), 'macbook');
469
- });
470
- void it('should map "browser" to "browser"', () => {
471
- assert.strictEqual(mapDeviceFrame('browser'), 'browser');
472
- });
473
- void it('should default unknown strings to "iphone"', () => {
474
- assert.strictEqual(mapDeviceFrame('android'), 'iphone');
475
- assert.strictEqual(mapDeviceFrame('tablet'), 'iphone');
476
- assert.strictEqual(mapDeviceFrame('desktop'), 'iphone');
477
- assert.strictEqual(mapDeviceFrame('xyz'), 'iphone');
478
- });
479
- void it('should be case insensitive for "iPhone"', () => {
480
- assert.strictEqual(mapDeviceFrame('iPhone'), 'iphone');
481
- });
482
- void it('should be case insensitive for "iPad"', () => {
483
- assert.strictEqual(mapDeviceFrame('iPad'), 'ipad');
484
- });
485
- void it('should be case insensitive for "MacBook"', () => {
486
- assert.strictEqual(mapDeviceFrame('MacBook'), 'macbook');
487
- });
488
- void it('should be case insensitive for "BROWSER"', () => {
489
- assert.strictEqual(mapDeviceFrame('BROWSER'), 'browser');
490
- });
491
- });
492
- // ============================================================
493
- // createCompositionHtml - Portrait mode tests
494
- // ============================================================
495
- void describe('createCompositionHtml - Portrait mode', () => {
496
- const spec = {
497
- feature_highlight: 'Dashboard',
498
- headline: 'Track Everything',
499
- subheadline: 'Real-time analytics at your fingertips',
500
- background_gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
501
- text_color: '#ffffff',
502
- device_frame: 'iphone',
503
- html_template: '<html><body>Mock</body></html>',
504
- };
505
- const dataUrl = 'data:image/png;base64,iVBORw0KGgo=';
506
- // Portrait: height > width
507
- const width = 1290;
508
- const height = 2796;
509
- void it('should contain flex-direction: column for portrait', () => {
510
- const html = createCompositionHtml(dataUrl, spec, width, height);
511
- assert.ok(html.includes('flex-direction: column'), 'Portrait mode should use flex-direction: column');
512
- });
513
- void it('should contain the headline text', () => {
514
- const html = createCompositionHtml(dataUrl, spec, width, height);
515
- assert.ok(html.includes('Track Everything'), 'Should contain the headline text');
516
- });
517
- void it('should contain the subheadline text when provided', () => {
518
- const html = createCompositionHtml(dataUrl, spec, width, height);
519
- assert.ok(html.includes('Real-time analytics at your fingertips'), 'Should contain the subheadline text');
520
- assert.ok(html.includes('class="subheadline"'), 'Should contain subheadline div');
521
- });
522
- void it('should not contain subheadline div when subheadline is undefined', () => {
523
- const specNoSub = { ...spec, subheadline: undefined };
524
- const html = createCompositionHtml(dataUrl, specNoSub, width, height);
525
- assert.ok(!html.includes('class="subheadline"'), 'Should NOT contain subheadline div when subheadline is undefined');
526
- });
527
- void it('should not contain subheadline div when subheadline is empty string', () => {
528
- const specEmptySub = { ...spec, subheadline: '' };
529
- const html = createCompositionHtml(dataUrl, specEmptySub, width, height);
530
- assert.ok(!html.includes('class="subheadline"'), 'Should NOT contain subheadline div when subheadline is empty');
531
- });
532
- void it('should contain the gradient background', () => {
533
- const html = createCompositionHtml(dataUrl, spec, width, height);
534
- assert.ok(html.includes(spec.background_gradient), 'Should contain the gradient background');
535
- });
536
- void it('should contain the framed screenshot data URL in img src', () => {
537
- const html = createCompositionHtml(dataUrl, spec, width, height);
538
- assert.ok(html.includes(`src="${dataUrl}"`), 'Should contain the framed screenshot data URL');
539
- });
540
- void it('should set width and height in body style', () => {
541
- const html = createCompositionHtml(dataUrl, spec, width, height);
542
- assert.ok(html.includes(`width: ${width}px`), 'Should set target width in body style');
543
- assert.ok(html.includes(`height: ${height}px`), 'Should set target height in body style');
544
- });
545
- void it('should calculate headline font size as Math.round(width * 0.10)', () => {
546
- const html = createCompositionHtml(dataUrl, spec, width, height);
547
- const expectedFontSize = Math.round(width * 0.1);
548
- assert.ok(html.includes(`font-size: ${expectedFontSize}px`), `Headline font size should be ${expectedFontSize}px (width * 0.10)`);
549
- });
550
- void it('should use the specified text color', () => {
551
- const html = createCompositionHtml(dataUrl, spec, width, height);
552
- assert.ok(html.includes(`color: ${spec.text_color}`), 'Should use the specified text color');
553
- });
554
- void it('should use custom text color when specified', () => {
555
- const specCustomColor = { ...spec, text_color: '#ff0000' };
556
- const html = createCompositionHtml(dataUrl, specCustomColor, width, height);
557
- assert.ok(html.includes('color: #ff0000'), 'Should use custom text color');
558
- });
559
- void it('should default text color to #ffffff when empty', () => {
560
- const specNoColor = { ...spec, text_color: '' };
561
- const html = createCompositionHtml(dataUrl, specNoColor, width, height);
562
- assert.ok(html.includes('color: #ffffff'), 'Should default text color to #ffffff');
563
- });
564
- });
565
- // ============================================================
566
- // createCompositionHtml - Landscape mode tests
567
- // ============================================================
568
- void describe('createCompositionHtml - Landscape mode', () => {
569
- const spec = {
570
- feature_highlight: 'Analytics',
571
- headline: 'Powerful Insights',
572
- subheadline: 'See trends across your data',
573
- background_gradient: 'linear-gradient(45deg, #00c6ff, #0072ff)',
574
- text_color: '#f0f0f0',
575
- device_frame: 'browser',
576
- html_template: '<html><body>Mock</body></html>',
577
- };
578
- const dataUrl = 'data:image/png;base64,abc123=';
579
- // Landscape: width > height
580
- const width = 1920;
581
- const height = 1200;
582
- void it('should use adaptive text-area width with min/max bounds', () => {
583
- const html = createCompositionHtml(dataUrl, spec, width, height);
584
- assert.ok(html.includes('min-width: 28%'), 'Landscape text-area should have min-width: 28%');
585
- assert.ok(html.includes('max-width: 44%'), 'Landscape text-area should have max-width: 44%');
586
- });
587
- void it('should use flexible device-area that fills remaining space', () => {
588
- const html = createCompositionHtml(dataUrl, spec, width, height);
589
- assert.ok(html.includes('flex: 1 1 0'), 'Landscape device-area should flex to fill remaining space');
590
- });
591
- void it('should NOT use portrait body layout for landscape', () => {
592
- const html = createCompositionHtml(dataUrl, spec, width, height);
593
- assert.ok(!html.includes('justify-content: flex-start'), 'Landscape body should not use portrait-style vertical layout');
594
- });
595
- void it('should calculate headline font size as Math.round(width * 0.055)', () => {
596
- const html = createCompositionHtml(dataUrl, spec, width, height);
597
- const expectedFontSize = Math.round(width * 0.055);
598
- assert.ok(html.includes(`font-size: ${expectedFontSize}px`), `Landscape headline font size should be ${expectedFontSize}px (width * 0.055)`);
599
- });
600
- void it('should contain the headline and subheadline text', () => {
601
- const html = createCompositionHtml(dataUrl, spec, width, height);
602
- assert.ok(html.includes('Powerful Insights'), 'Should contain headline');
603
- assert.ok(html.includes('See trends across your data'), 'Should contain subheadline');
604
- });
605
- void it('should contain the data URL in img src', () => {
606
- const html = createCompositionHtml(dataUrl, spec, width, height);
607
- assert.ok(html.includes(`src="${dataUrl}"`), 'Should contain the data URL');
608
- });
609
- void it('should use text-align: left for landscape', () => {
610
- const html = createCompositionHtml(dataUrl, spec, width, height);
611
- assert.ok(html.includes('text-align: left'), 'Landscape text should be left-aligned');
612
- });
613
- });
614
- // ============================================================
615
- // createCompositionHtml - Font embedding
616
- // ============================================================
617
- void describe('createCompositionHtml - Font embedding', () => {
618
- const spec = {
619
- feature_highlight: 'Test',
620
- headline: 'Test Headline',
621
- subheadline: undefined,
622
- background_gradient: 'linear-gradient(#000, #111)',
623
- text_color: '#fff',
624
- device_frame: 'iphone',
625
- html_template: '<html></html>',
626
- };
627
- const dataUrl = 'data:image/png;base64,x';
628
- void it('should embed @font-face declaration for Inter', () => {
629
- const html = createCompositionHtml(dataUrl, spec, 1290, 2796);
630
- assert.ok(html.includes("font-family: 'Inter'"), 'Should declare Inter in @font-face');
631
- assert.ok(html.includes('font/woff2;base64,'), 'Should embed font as base64 WOFF2 data URI');
632
- });
633
- void it('should use Inter as primary font-family on body', () => {
634
- const html = createCompositionHtml(dataUrl, spec, 1290, 2796);
635
- assert.ok(html.includes("font-family: 'Inter', -apple-system"), 'Body font-family should start with Inter');
636
- });
637
- void it('should not reference external font CDN', () => {
638
- const html = createCompositionHtml(dataUrl, spec, 1290, 2796);
639
- assert.ok(!html.includes('fonts.googleapis.com'), 'Should not reference Google Fonts CDN');
640
- });
641
- });
642
- // ============================================================
643
- // createCompositionHtml - Text readability protection
644
- // ============================================================
645
- void describe('createCompositionHtml - Text readability protection', () => {
646
- const spec = {
647
- feature_highlight: 'Test',
648
- headline: 'Test Headline',
649
- subheadline: 'Test Sub',
650
- background_gradient: 'linear-gradient(#fff, #eee)',
651
- text_color: '#ffffff',
652
- device_frame: 'iphone',
653
- html_template: '<html></html>',
654
- };
655
- const dataUrl = 'data:image/png;base64,x';
656
- void it('should apply text-shadow on headline for portrait', () => {
657
- const html = createCompositionHtml(dataUrl, spec, 1290, 2796);
658
- assert.ok(html.includes('text-shadow: 0 2px 12px rgba(0,0,0,0.35)'), 'Headline should have text-shadow for readability');
659
- });
660
- void it('should apply text-shadow on subheadline for portrait', () => {
661
- const html = createCompositionHtml(dataUrl, spec, 1290, 2796);
662
- assert.ok(html.includes('text-shadow: 0 1px 8px rgba(0,0,0,0.25)'), 'Subheadline should have lighter text-shadow');
663
- });
664
- void it('should apply text-shadow on headline for landscape', () => {
665
- const html = createCompositionHtml(dataUrl, spec, 1920, 1200);
666
- assert.ok(html.includes('text-shadow: 0 2px 12px rgba(0,0,0,0.35)'), 'Landscape headline should have text-shadow');
667
- });
668
- void it('should include gradient overlay on text-area for portrait', () => {
669
- const html = createCompositionHtml(dataUrl, spec, 1290, 2796);
670
- assert.ok(html.includes('.text-area::after'), 'Portrait should have ::after pseudo-element on text-area');
671
- assert.ok(html.includes('linear-gradient(to bottom, rgba(0,0,0,0.25)'), 'Portrait overlay should gradient top-to-bottom');
672
- });
673
- void it('should include gradient overlay on text-area for landscape', () => {
674
- const html = createCompositionHtml(dataUrl, spec, 1920, 1200);
675
- assert.ok(html.includes('.text-area::after'), 'Landscape should have ::after pseudo-element on text-area');
676
- assert.ok(html.includes('linear-gradient(to right, rgba(0,0,0,0.25)'), 'Landscape overlay should gradient left-to-right');
677
- });
678
- void it('should set z-index on headline/subheadline above overlay', () => {
679
- const html = createCompositionHtml(dataUrl, spec, 1290, 2796);
680
- assert.ok(html.includes('z-index: 1'), 'Text elements should have z-index above the overlay');
681
- });
682
- });
683
- // ============================================================
684
- // createCompositionHtml - Line clamping
685
- // ============================================================
686
- void describe('createCompositionHtml - Line clamping', () => {
687
- const spec = {
688
- feature_highlight: 'Test',
689
- headline: 'Very Long Headline That Could Potentially Overflow',
690
- subheadline: 'A subheadline that is also quite long and may overflow',
691
- background_gradient: 'linear-gradient(#000, #111)',
692
- text_color: '#fff',
693
- device_frame: 'iphone',
694
- html_template: '<html></html>',
695
- };
696
- const dataUrl = 'data:image/png;base64,x';
697
- void it('should clamp headline to 3 lines in portrait', () => {
698
- const html = createCompositionHtml(dataUrl, spec, 1290, 2796);
699
- assert.ok(html.includes('-webkit-line-clamp: 3'), 'Portrait headline should be clamped to 3 lines');
700
- });
701
- void it('should clamp subheadline to 2 lines in portrait', () => {
702
- const html = createCompositionHtml(dataUrl, spec, 1290, 2796);
703
- assert.ok(html.includes('-webkit-line-clamp: 2'), 'Portrait subheadline should be clamped to 2 lines');
704
- });
705
- void it('should clamp headline to 3 lines in landscape', () => {
706
- const html = createCompositionHtml(dataUrl, spec, 1920, 1200);
707
- assert.ok(html.includes('-webkit-line-clamp: 3'), 'Landscape headline should be clamped to 3 lines');
708
- });
709
- void it('should use -webkit-box for line clamping', () => {
710
- const html = createCompositionHtml(dataUrl, spec, 1290, 2796);
711
- assert.ok(html.includes('display: -webkit-box'), 'Should use -webkit-box display for line clamping');
712
- assert.ok(html.includes('-webkit-box-orient: vertical'), 'Should set -webkit-box-orient: vertical');
713
- });
714
- });
715
- // ============================================================
716
- // createCompositionHtml - Portrait layout details
717
- // ============================================================
718
- void describe('createCompositionHtml - Portrait layout details', () => {
719
- const spec = {
720
- feature_highlight: 'Test',
721
- headline: 'Test',
722
- subheadline: undefined,
723
- background_gradient: 'linear-gradient(#000, #111)',
724
- text_color: '#fff',
725
- device_frame: 'iphone',
726
- html_template: '<html></html>',
727
- };
728
- const dataUrl = 'data:image/png;base64,x';
729
- void it('should limit text-area max-height to 28%', () => {
730
- const html = createCompositionHtml(dataUrl, spec, 1290, 2796);
731
- assert.ok(html.includes('max-height: 28%'), 'Portrait text-area should have max-height: 28%');
732
- });
733
- void it('should center device vertically in device-area', () => {
734
- const html = createCompositionHtml(dataUrl, spec, 1290, 2796);
735
- assert.ok(html.includes('align-items: center') && html.includes('min-height: 0'), 'Device-area should center content vertically with min-height: 0');
736
- });
737
- });
738
- // ============================================================
739
- // createCompositionHtml - Landscape layout details
740
- // ============================================================
741
- void describe('createCompositionHtml - Landscape layout details', () => {
742
- const spec = {
743
- feature_highlight: 'Test',
744
- headline: 'Test',
745
- subheadline: undefined,
746
- background_gradient: 'linear-gradient(#000, #111)',
747
- text_color: '#fff',
748
- device_frame: 'browser',
749
- html_template: '<html></html>',
750
- };
751
- const dataUrl = 'data:image/png;base64,x';
752
- void it('should vertically center text inside text-area', () => {
753
- const html = createCompositionHtml(dataUrl, spec, 1920, 1200);
754
- assert.ok(html.includes('justify-content: center'), 'Text-area should vertically center its content');
755
- });
756
- void it('should prevent device-area overflow with min-width: 0', () => {
757
- const html = createCompositionHtml(dataUrl, spec, 1920, 1200);
758
- assert.ok(html.includes('min-width: 0'), 'Device-area should have min-width: 0 to prevent flex overflow');
759
- });
760
- });
761
- // ============================================================
762
- // createCompositionHtml - HTML escaping
763
- // ============================================================
764
- void describe('createCompositionHtml - HTML escaping', () => {
765
- void it('should escape HTML in headline', () => {
766
- const spec = {
767
- feature_highlight: 'Test',
768
- headline: '<script>alert("xss")</script>',
769
- subheadline: undefined,
770
- background_gradient: 'linear-gradient(#000, #111)',
771
- text_color: '#fff',
772
- device_frame: 'iphone',
773
- html_template: '<html></html>',
774
- };
775
- const html = createCompositionHtml('data:image/png;base64,x', spec, 1290, 2796);
776
- assert.ok(html.includes('&lt;script&gt;'), 'Headline should have escaped < and >');
777
- assert.ok(!html.includes('<script>alert'), 'Headline should not contain unescaped script tag');
778
- });
779
- void it('should escape HTML in subheadline', () => {
780
- const spec = {
781
- feature_highlight: 'Test',
782
- headline: 'Safe Headline',
783
- subheadline: 'A & B "quoted" <value>',
784
- background_gradient: 'linear-gradient(#000, #111)',
785
- text_color: '#fff',
786
- device_frame: 'iphone',
787
- html_template: '<html></html>',
788
- };
789
- const html = createCompositionHtml('data:image/png;base64,x', spec, 1290, 2796);
790
- assert.ok(html.includes('A &amp; B &quot;quoted&quot; &lt;value&gt;'), 'Subheadline should have escaped special characters');
791
- });
792
- });
793
- // ============================================================
794
- // STORE_SCREENSHOT_SIZES - Orientation tests
795
- // ============================================================
796
- void describe('STORE_SCREENSHOT_SIZES - Orientation', () => {
797
- void it('should have all Apple sizes in portrait (height > width)', () => {
798
- for (const [device, size] of Object.entries(STORE_SCREENSHOT_SIZES.apple)) {
799
- assert.ok(size.height > size.width, `Apple ${device} should be portrait: ${size.width}x${size.height}`);
800
- }
801
- });
802
- void it('should have Google phone in portrait (height > width)', () => {
803
- const { phone } = STORE_SCREENSHOT_SIZES.google;
804
- assert.ok(phone.height > phone.width, `Google phone should be portrait: ${phone.width}x${phone.height}`);
805
- });
806
- void it('should have Google tablet_7 in portrait (height > width)', () => {
807
- const tablet7 = STORE_SCREENSHOT_SIZES.google.tablet_7;
808
- assert.ok(tablet7.height > tablet7.width, `Google tablet_7 should be portrait: ${tablet7.width}x${tablet7.height}`);
809
- });
810
- void it('should have Google tablet_10 in landscape (width > height)', () => {
811
- const tablet10 = STORE_SCREENSHOT_SIZES.google.tablet_10;
812
- assert.ok(tablet10.width > tablet10.height, `Google tablet_10 should be landscape: ${tablet10.width}x${tablet10.height}`);
813
- });
814
- void it('should have exactly one landscape size across all stores (google.tablet_10)', () => {
815
- const allSizes = [];
816
- for (const [store, devices] of Object.entries(STORE_SCREENSHOT_SIZES)) {
817
- for (const [device, size] of Object.entries(devices)) {
818
- allSizes.push({ store, device, w: size.width, h: size.height });
819
- }
820
- }
821
- const landscapeSizes = allSizes.filter((s) => s.w > s.h);
822
- assert.strictEqual(landscapeSizes.length, 1, `Should have exactly 1 landscape size, found ${landscapeSizes.length}`);
823
- assert.strictEqual(landscapeSizes[0].store, 'google', 'The landscape size should be from google');
824
- assert.strictEqual(landscapeSizes[0].device, 'tablet_10', 'The landscape size should be tablet_10');
825
- });
826
- });