synthos 0.10.1 → 0.11.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.
- package/README.md +5 -5
- package/default-pages/elevenlabs_effects_studio/chat-history.json +1 -0
- package/default-pages/elevenlabs_effects_studio/page.html +1345 -1363
- package/default-pages/elevenlabs_effects_studio/page.json +13 -11
- package/default-pages/elevenlabs_voice_studio/chat-history.json +1 -0
- package/default-pages/elevenlabs_voice_studio/page.html +782 -801
- package/default-pages/elevenlabs_voice_studio/page.json +13 -11
- package/default-pages/json_tools/chat-history.json +1 -0
- package/default-pages/json_tools/page.html +70 -90
- package/default-pages/json_tools/page.json +12 -10
- package/default-pages/my_notes/chat-history.json +1 -0
- package/default-pages/my_notes/page.html +115 -131
- package/default-pages/my_notes/page.json +14 -12
- package/default-pages/neon_asteroids/chat-history.json +1 -0
- package/default-pages/neon_asteroids/page.html +1777 -1803
- package/default-pages/neon_asteroids/page.json +14 -12
- package/default-pages/oregon_trail/chat-history.json +1 -0
- package/default-pages/oregon_trail/page.html +290 -307
- package/default-pages/oregon_trail/page.json +14 -12
- package/default-pages/solar_explorer/chat-history.json +1 -0
- package/default-pages/solar_explorer/page.html +1929 -1951
- package/default-pages/solar_explorer/page.json +14 -12
- package/default-pages/solar_tutorial/chat-history.json +1 -0
- package/default-pages/solar_tutorial/page.html +464 -478
- package/default-pages/solar_tutorial/page.json +12 -10
- package/default-pages/us_map/chat-history.json +1 -0
- package/default-pages/us_map/page.html +170 -193
- package/default-pages/us_map/page.json +14 -12
- package/default-pages/us_map/page.light.png +0 -0
- package/default-pages/us_map_1850/chat-history.json +1 -0
- package/default-pages/us_map_1850/page.html +302 -326
- package/default-pages/us_map_1850/page.json +14 -12
- package/default-pages/western_cities_1850/chat-history.json +1 -0
- package/default-pages/western_cities_1850/page.html +503 -527
- package/default-pages/western_cities_1850/page.json +14 -12
- package/default-themes/aurora-dawn.v3.css +15 -14
- package/default-themes/aurora-dusk.v3.css +26 -26
- package/default-themes/cosmos-dawn.v3.css +15 -14
- package/default-themes/cosmos-dusk.v3.css +26 -26
- package/default-themes/elemental-dawn.v3.css +200 -0
- package/default-themes/nebula-dawn.v3.css +15 -14
- package/default-themes/nebula-dusk.v3.css +24 -24
- package/default-themes/solar-flare-dawn.v3.css +15 -14
- package/default-themes/solar-flare-dusk.v3.css +26 -26
- package/dist/builders/anthropic.d.ts +26 -2
- package/dist/builders/anthropic.d.ts.map +1 -1
- package/dist/builders/anthropic.js +132 -31
- package/dist/builders/anthropic.js.map +1 -1
- package/dist/builders/claudecode.d.ts +13 -0
- package/dist/builders/claudecode.d.ts.map +1 -0
- package/dist/builders/claudecode.js +253 -0
- package/dist/builders/claudecode.js.map +1 -0
- package/dist/builders/index.d.ts +2 -1
- package/dist/builders/index.d.ts.map +1 -1
- package/dist/builders/index.js +8 -1
- package/dist/builders/index.js.map +1 -1
- package/dist/builders/openai.js +2 -1
- package/dist/builders/openai.js.map +1 -1
- package/dist/builders/types.d.ts +31 -7
- package/dist/builders/types.d.ts.map +1 -1
- package/dist/builders/types.js +60 -28
- package/dist/builders/types.js.map +1 -1
- package/dist/connectors/types.d.ts +8 -0
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +13 -6
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +161 -14
- package/dist/migrations.js.map +1 -1
- package/dist/models/anthropic.d.ts +1 -0
- package/dist/models/anthropic.d.ts.map +1 -1
- package/dist/models/anthropic.js +129 -29
- package/dist/models/anthropic.js.map +1 -1
- package/dist/models/chainOfThought.d.ts.map +1 -1
- package/dist/models/chainOfThought.js +32 -19
- package/dist/models/chainOfThought.js.map +1 -1
- package/dist/models/index.d.ts +2 -2
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +2 -1
- package/dist/models/index.js.map +1 -1
- package/dist/models/providers.d.ts +1 -0
- package/dist/models/providers.d.ts.map +1 -1
- package/dist/models/providers.js +12 -4
- package/dist/models/providers.js.map +1 -1
- package/dist/models/types.d.ts +15 -1
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js.map +1 -1
- package/dist/pages.d.ts +57 -8
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +258 -45
- package/dist/pages.js.map +1 -1
- package/dist/service/createCompletePrompt.d.ts.map +1 -1
- package/dist/service/createCompletePrompt.js +5 -0
- package/dist/service/createCompletePrompt.js.map +1 -1
- package/dist/service/mediaCache.d.ts +36 -0
- package/dist/service/mediaCache.d.ts.map +1 -0
- package/dist/service/mediaCache.js +182 -0
- package/dist/service/mediaCache.js.map +1 -0
- package/dist/service/pageValidator.d.ts +25 -0
- package/dist/service/pageValidator.d.ts.map +1 -0
- package/dist/service/pageValidator.js +315 -0
- package/dist/service/pageValidator.js.map +1 -0
- package/dist/service/server.d.ts.map +1 -1
- package/dist/service/server.js +4 -0
- package/dist/service/server.js.map +1 -1
- package/dist/service/sharedTableSchema.d.ts +73 -0
- package/dist/service/sharedTableSchema.d.ts.map +1 -0
- package/dist/service/sharedTableSchema.js +206 -0
- package/dist/service/sharedTableSchema.js.map +1 -0
- package/dist/service/transformPage.d.ts +49 -11
- package/dist/service/transformPage.d.ts.map +1 -1
- package/dist/service/transformPage.js +354 -241
- package/dist/service/transformPage.js.map +1 -1
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +288 -34
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.d.ts.map +1 -1
- package/dist/service/useConnectorRoutes.js +170 -32
- package/dist/service/useConnectorRoutes.js.map +1 -1
- package/dist/service/useDataRoutes.d.ts.map +1 -1
- package/dist/service/useDataRoutes.js +59 -2
- package/dist/service/useDataRoutes.js.map +1 -1
- package/dist/service/useExtractRoutes.d.ts +4 -0
- package/dist/service/useExtractRoutes.d.ts.map +1 -0
- package/dist/service/useExtractRoutes.js +304 -0
- package/dist/service/useExtractRoutes.js.map +1 -0
- package/dist/service/usePageRoutes.d.ts +17 -0
- package/dist/service/usePageRoutes.d.ts.map +1 -1
- package/dist/service/usePageRoutes.js +1385 -483
- package/dist/service/usePageRoutes.js.map +1 -1
- package/dist/service/useSharedDataRoutes.d.ts.map +1 -1
- package/dist/service/useSharedDataRoutes.js +54 -2
- package/dist/service/useSharedDataRoutes.js.map +1 -1
- package/dist/settings.d.ts +27 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +40 -1
- package/dist/settings.js.map +1 -1
- package/dist/themes.d.ts +0 -5
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +3 -95
- package/dist/themes.js.map +1 -1
- package/migration-rules/v2-to-v3.md +277 -119
- package/package.json +5 -1
- package/{default-pages/application → required-pages/_shell}/page.html +56 -42
- package/required-pages/_shell/page.json +14 -0
- package/required-pages/_starters/page.html +534 -0
- package/required-pages/_starters/page.json +12 -0
- package/required-pages/builder/page.html +353 -43
- package/required-pages/builder/page.json +12 -10
- package/required-pages/pages/page.html +697 -924
- package/required-pages/pages/page.json +12 -10
- package/required-pages/settings/page.html +1879 -1753
- package/required-pages/settings/page.json +12 -10
- package/required-pages/synthos_apis/page.html +834 -845
- package/required-pages/synthos_apis/page.json +12 -10
- package/required-pages/synthos_scripts/page.html +74 -88
- package/required-pages/synthos_scripts/page.json +12 -10
- package/scripts/append-instructions.py +90 -0
- package/scripts/audit-instructions.py +76 -0
- package/scripts/cleanup-shell-markup.mjs +112 -0
- package/service-connectors/buffer/connector.json +46 -0
- package/service-connectors/canva/connector.json +67 -0
- package/service-connectors/elevenlabs/connector.json +1 -1
- package/src/builders/anthropic.ts +150 -25
- package/src/builders/claudecode.ts +310 -0
- package/src/builders/index.ts +7 -1
- package/src/builders/openai.ts +2 -1
- package/src/builders/types.ts +93 -32
- package/src/connectors/types.ts +8 -0
- package/src/init.ts +13 -7
- package/src/migrations.ts +187 -16
- package/src/models/anthropic.ts +140 -30
- package/src/models/chainOfThought.ts +33 -18
- package/src/models/index.ts +2 -2
- package/src/models/providers.ts +10 -1
- package/src/models/types.ts +21 -1
- package/src/pages.ts +271 -35
- package/src/service/createCompletePrompt.ts +6 -0
- package/src/service/mediaCache.ts +206 -0
- package/src/service/pageValidator.ts +337 -0
- package/src/service/server.ts +4 -0
- package/src/service/sharedTableSchema.ts +236 -0
- package/src/service/transformPage.ts +370 -260
- package/src/service/useApiRoutes.ts +282 -32
- package/src/service/useConnectorRoutes.ts +189 -34
- package/src/service/useDataRoutes.ts +198 -116
- package/src/service/useExtractRoutes.ts +331 -0
- package/src/service/usePageRoutes.ts +1411 -394
- package/src/service/useSharedDataRoutes.ts +184 -109
- package/src/settings.ts +65 -0
- package/src/themes.ts +78 -180
- package/starters/blank_starter/chat-history.json +1 -0
- package/starters/blank_starter/page.dark.png +0 -0
- package/starters/blank_starter/page.html +47 -0
- package/starters/blank_starter/page.json +13 -0
- package/starters/blank_starter/page.light.png +0 -0
- package/starters/calculator_starter/chat-history.json +1 -0
- package/starters/calculator_starter/page.dark.png +0 -0
- package/starters/calculator_starter/page.html +232 -0
- package/starters/calculator_starter/page.json +13 -0
- package/starters/calculator_starter/page.light.png +0 -0
- package/starters/calendar_starter/chat-history.json +1 -0
- package/starters/calendar_starter/page.dark.png +0 -0
- package/starters/calendar_starter/page.html +495 -0
- package/starters/calendar_starter/page.json +13 -0
- package/starters/calendar_starter/page.light.png +0 -0
- package/starters/chat_starter/chat-history.json +1 -0
- package/starters/chat_starter/page.dark.png +0 -0
- package/starters/chat_starter/page.html +351 -0
- package/starters/chat_starter/page.json +13 -0
- package/starters/chat_starter/page.light.png +0 -0
- package/starters/checklist_starter/chat-history.json +1 -0
- package/starters/checklist_starter/page.dark.png +0 -0
- package/starters/checklist_starter/page.html +437 -0
- package/starters/checklist_starter/page.json +13 -0
- package/starters/checklist_starter/page.light.png +0 -0
- package/starters/dashboard_starter/chat-history.json +1 -0
- package/starters/dashboard_starter/page.dark.png +0 -0
- package/starters/dashboard_starter/page.html +195 -0
- package/starters/dashboard_starter/page.json +13 -0
- package/starters/dashboard_starter/page.light.png +0 -0
- package/starters/form_starter/chat-history.json +1 -0
- package/starters/form_starter/page.dark.png +0 -0
- package/starters/form_starter/page.html +313 -0
- package/starters/form_starter/page.json +13 -0
- package/starters/form_starter/page.light.png +0 -0
- package/starters/gallery_starter/chat-history.json +1 -0
- package/starters/gallery_starter/page.dark.png +0 -0
- package/starters/gallery_starter/page.html +418 -0
- package/starters/gallery_starter/page.json +13 -0
- package/starters/gallery_starter/page.light.png +0 -0
- package/starters/generator_starter/chat-history.json +1 -0
- package/starters/generator_starter/page.dark.png +0 -0
- package/starters/generator_starter/page.html +261 -0
- package/starters/generator_starter/page.json +13 -0
- package/starters/generator_starter/page.light.png +0 -0
- package/starters/index.html +538 -0
- package/starters/kanban_starter/chat-history.json +1 -0
- package/starters/kanban_starter/page.dark.png +0 -0
- package/starters/kanban_starter/page.html +432 -0
- package/starters/kanban_starter/page.json +13 -0
- package/starters/kanban_starter/page.light.png +0 -0
- package/starters/presentation_builder/chat-history.json +1 -0
- package/starters/presentation_builder/page.dark.png +0 -0
- package/starters/presentation_builder/page.html +970 -0
- package/starters/presentation_builder/page.json +15 -0
- package/starters/presentation_builder/page.light.png +0 -0
- package/starters/presentation_builder/presentation_voice/voice_config.json +9 -0
- package/starters/pulse_starter/chat-history.json +1 -0
- package/starters/pulse_starter/page.dark.png +0 -0
- package/starters/pulse_starter/page.html +698 -0
- package/starters/pulse_starter/page.json +13 -0
- package/starters/pulse_starter/page.light.png +0 -0
- package/starters/quiz_starter/chat-history.json +1 -0
- package/starters/quiz_starter/page.dark.png +0 -0
- package/starters/quiz_starter/page.html +292 -0
- package/starters/quiz_starter/page.json +13 -0
- package/starters/quiz_starter/page.light.png +0 -0
- package/starters/reference_starter/chat-history.json +1 -0
- package/starters/reference_starter/page.dark.png +0 -0
- package/starters/reference_starter/page.html +250 -0
- package/starters/reference_starter/page.json +13 -0
- package/starters/reference_starter/page.light.png +0 -0
- package/starters/retro_game_starter/chat-history.json +1 -0
- package/starters/retro_game_starter/page.dark.png +0 -0
- package/{default-pages → starters}/retro_game_starter/page.html +1281 -1308
- package/starters/retro_game_starter/page.json +15 -0
- package/starters/retro_game_starter/page.light.png +0 -0
- package/starters/roster_starter/chat-history.json +1 -0
- package/starters/roster_starter/page.dark.png +0 -0
- package/starters/roster_starter/page.html +600 -0
- package/starters/roster_starter/page.json +13 -0
- package/starters/roster_starter/page.light.png +0 -0
- package/starters/server.js +182 -0
- package/starters/start.cmd +1 -0
- package/starters/timeline_starter/chat-history.json +1 -0
- package/starters/timeline_starter/page.dark.png +0 -0
- package/starters/timeline_starter/page.html +446 -0
- package/starters/timeline_starter/page.json +13 -0
- package/starters/timeline_starter/page.light.png +0 -0
- package/starters/tutorial_starter/chat-history.json +1 -0
- package/starters/tutorial_starter/page.dark.png +0 -0
- package/starters/tutorial_starter/page.html +283 -0
- package/starters/tutorial_starter/page.json +13 -0
- package/starters/tutorial_starter/page.light.png +0 -0
- package/static-files/agent.v3.js +122 -0
- package/static-files/connector.v3.js +48 -0
- package/static-files/extract.v3.js +188 -0
- package/static-files/helpers.v3.js +50 -6
- package/static-files/page-bridge.js +114 -0
- package/static-files/page.v3.js +1292 -1290
- package/static-files/script.v3.js +32 -0
- package/static-files/server.v3.js +89 -0
- package/static-files/shell-bridge.v3.js +174 -0
- package/static-files/shell-modals.v3.js +521 -0
- package/static-files/{shell.css → shell.v3.css} +271 -22
- package/static-files/shell.v3.js +1865 -0
- package/static-files/storage.v3.js +176 -0
- package/tests/anthropic.spec.ts +42 -7
- package/tests/builders.spec.ts +70 -2
- package/tests/pageValidator.spec.ts +548 -0
- package/tests/profiles.spec.ts +122 -0
- package/tests/sharedTableSchema.spec.ts +242 -0
- package/tests/transformPage.spec.ts +62 -81
- package/default-pages/application/page.json +0 -10
- package/default-pages/retro_game_starter/page.json +0 -12
- package/default-pages/sidebar_page/page.html +0 -51
- package/default-pages/sidebar_page/page.json +0 -10
- package/default-pages/two-panel_page/page.html +0 -68
- package/default-pages/two-panel_page/page.json +0 -10
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import {
|
|
3
|
+
validatePage,
|
|
4
|
+
PageValidationResult,
|
|
5
|
+
PageValidationError,
|
|
6
|
+
} from '../src/service/pageValidator';
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Helpers
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
/** Wrap inline script(s) in a minimal HTML page. */
|
|
13
|
+
function page(body: string, scripts: string | string[] = []): string {
|
|
14
|
+
const arr = Array.isArray(scripts) ? scripts : [scripts];
|
|
15
|
+
const scriptTags = arr.map(s => `<script>${s}</script>`).join('\n');
|
|
16
|
+
return `<html><head></head><body>${body}\n${scriptTags}</body></html>`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function pageWithIds(ids: string[], scripts: string | string[] = []): string {
|
|
20
|
+
const divs = ids.map(id => `<div id="${id}"></div>`).join('\n');
|
|
21
|
+
return page(divs, scripts);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function errorTypes(result: PageValidationResult): string[] {
|
|
25
|
+
return result.errors.map(e => e.type);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Layer 1: Syntax Errors (Acorn Parse)
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
describe('pageValidator', () => {
|
|
33
|
+
|
|
34
|
+
describe('Layer 1 — Syntax errors', () => {
|
|
35
|
+
it('catches unclosed brace', () => {
|
|
36
|
+
const html = page('', 'const x = {');
|
|
37
|
+
const result = validatePage(html);
|
|
38
|
+
assert.strictEqual(result.valid, false);
|
|
39
|
+
assert.strictEqual(result.errors.length, 1);
|
|
40
|
+
assert.strictEqual(result.errors[0].type, 'syntax-error');
|
|
41
|
+
assert.ok(result.errors[0].line !== undefined, 'should include line number');
|
|
42
|
+
assert.ok(result.errors[0].col !== undefined, 'should include column number');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('catches unexpected token', () => {
|
|
46
|
+
const html = page('', 'function() {}');
|
|
47
|
+
const result = validatePage(html);
|
|
48
|
+
assert.strictEqual(result.valid, false);
|
|
49
|
+
assert.ok(result.errors.some(e => e.type === 'syntax-error'));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('reports source as inline-script-N', () => {
|
|
53
|
+
const html = page('', ['const a = 1;', 'const b = {']);
|
|
54
|
+
const result = validatePage(html);
|
|
55
|
+
const err = result.errors.find(e => e.type === 'syntax-error')!;
|
|
56
|
+
assert.ok(err);
|
|
57
|
+
assert.strictEqual(err.source, 'inline-script-1');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('catches errors in multiple scripts independently', () => {
|
|
61
|
+
const html = page('', ['const a = {', 'const b = {']);
|
|
62
|
+
const result = validatePage(html);
|
|
63
|
+
assert.strictEqual(result.valid, false);
|
|
64
|
+
assert.strictEqual(result.errors.filter(e => e.type === 'syntax-error').length, 2);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('parses module scripts correctly', () => {
|
|
68
|
+
const html = `<html><head></head><body>
|
|
69
|
+
<script type="module">
|
|
70
|
+
import { foo } from './bar.js';
|
|
71
|
+
export const x = 1;
|
|
72
|
+
</script>
|
|
73
|
+
</body></html>`;
|
|
74
|
+
const result = validatePage(html);
|
|
75
|
+
assert.strictEqual(result.valid, true);
|
|
76
|
+
assert.strictEqual(result.errors.length, 0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('skips empty scripts', () => {
|
|
80
|
+
const html = `<html><head></head><body>
|
|
81
|
+
<script></script>
|
|
82
|
+
<script> </script>
|
|
83
|
+
<script>\n\t\n</script>
|
|
84
|
+
</body></html>`;
|
|
85
|
+
const result = validatePage(html);
|
|
86
|
+
assert.strictEqual(result.valid, true);
|
|
87
|
+
assert.strictEqual(result.errors.length, 0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('skips external scripts (with src attribute)', () => {
|
|
91
|
+
const html = `<html><head></head><body>
|
|
92
|
+
<script src="https://cdn.example.com/broken.js"></script>
|
|
93
|
+
</body></html>`;
|
|
94
|
+
const result = validatePage(html);
|
|
95
|
+
assert.strictEqual(result.valid, true);
|
|
96
|
+
assert.strictEqual(result.errors.length, 0);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('allows top-level await', () => {
|
|
100
|
+
const html = page('', 'const data = await fetch("/api");');
|
|
101
|
+
const result = validatePage(html);
|
|
102
|
+
assert.strictEqual(result.valid, true);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// -----------------------------------------------------------------------
|
|
107
|
+
// Injected Scripts — Should Not Produce False Positives
|
|
108
|
+
// -----------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
describe('Injected scripts — no false positives', () => {
|
|
111
|
+
it('skips page-bridge script', () => {
|
|
112
|
+
const html = `<html><head></head><body>
|
|
113
|
+
<script id="page-bridge">
|
|
114
|
+
synthos.generate.video();
|
|
115
|
+
const x = {;
|
|
116
|
+
</script>
|
|
117
|
+
</body></html>`;
|
|
118
|
+
const result = validatePage(html);
|
|
119
|
+
assert.strictEqual(result.valid, true);
|
|
120
|
+
assert.strictEqual(result.errors.length, 0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('skips page-helpers script', () => {
|
|
124
|
+
const html = `<html><head></head><body>
|
|
125
|
+
<script id="page-helpers">
|
|
126
|
+
synthos.nonexistent.method();
|
|
127
|
+
</script>
|
|
128
|
+
</body></html>`;
|
|
129
|
+
const result = validatePage(html);
|
|
130
|
+
assert.strictEqual(result.valid, true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('skips page-info script', () => {
|
|
134
|
+
const html = `<html><head></head><body>
|
|
135
|
+
<script id="page-info">
|
|
136
|
+
const info = { broken: };
|
|
137
|
+
</script>
|
|
138
|
+
</body></html>`;
|
|
139
|
+
const result = validatePage(html);
|
|
140
|
+
assert.strictEqual(result.valid, true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('skips page-script (legacy)', () => {
|
|
144
|
+
const html = `<html><head></head><body>
|
|
145
|
+
<script id="page-script">
|
|
146
|
+
synthos.fake.api();
|
|
147
|
+
</script>
|
|
148
|
+
</body></html>`;
|
|
149
|
+
const result = validatePage(html);
|
|
150
|
+
assert.strictEqual(result.valid, true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('skips synthos-error-capture (legacy)', () => {
|
|
154
|
+
const html = `<html><head></head><body>
|
|
155
|
+
<script id="synthos-error-capture">
|
|
156
|
+
window.onerror = function(;
|
|
157
|
+
</script>
|
|
158
|
+
</body></html>`;
|
|
159
|
+
const result = validatePage(html);
|
|
160
|
+
assert.strictEqual(result.valid, true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('still validates scripts without known IDs', () => {
|
|
164
|
+
const html = `<html><head></head><body>
|
|
165
|
+
<script id="page-bridge">synthos.fake.call();</script>
|
|
166
|
+
<script id="my-app">synthos.fake.call();</script>
|
|
167
|
+
</body></html>`;
|
|
168
|
+
const result = validatePage(html);
|
|
169
|
+
assert.strictEqual(result.valid, false);
|
|
170
|
+
assert.strictEqual(result.errors.length, 1);
|
|
171
|
+
assert.strictEqual(result.errors[0].source, 'inline-script-0');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// -----------------------------------------------------------------------
|
|
176
|
+
// Layer 2: DOM Element ID Inventory
|
|
177
|
+
// -----------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
describe('Layer 2 — Missing element IDs', () => {
|
|
180
|
+
it('catches getElementById referencing missing ID', () => {
|
|
181
|
+
const html = pageWithIds(['header'], "document.getElementById('chartArea');");
|
|
182
|
+
const result = validatePage(html);
|
|
183
|
+
assert.strictEqual(result.valid, false);
|
|
184
|
+
assert.strictEqual(result.errors.length, 1);
|
|
185
|
+
assert.strictEqual(result.errors[0].type, 'missing-element');
|
|
186
|
+
assert.ok(result.errors[0].message.includes('#chartArea'));
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('catches querySelector referencing missing ID', () => {
|
|
190
|
+
const html = pageWithIds(['app'], "document.querySelector('#missing');");
|
|
191
|
+
const result = validatePage(html);
|
|
192
|
+
assert.strictEqual(result.valid, false);
|
|
193
|
+
assert.ok(result.errors.some(e => e.type === 'missing-element' && e.message.includes('#missing')));
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('catches querySelectorAll referencing missing ID', () => {
|
|
197
|
+
const html = pageWithIds([], "document.querySelectorAll('#gone');");
|
|
198
|
+
const result = validatePage(html);
|
|
199
|
+
assert.strictEqual(result.valid, false);
|
|
200
|
+
assert.ok(result.errors.some(e => e.type === 'missing-element'));
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('catches jQuery-style $() referencing missing ID', () => {
|
|
204
|
+
const html = pageWithIds(['content'], "$('#sidebar');");
|
|
205
|
+
const result = validatePage(html);
|
|
206
|
+
assert.strictEqual(result.valid, false);
|
|
207
|
+
assert.ok(result.errors.some(e => e.type === 'missing-element' && e.message.includes('#sidebar')));
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('passes when all referenced IDs exist', () => {
|
|
211
|
+
const html = pageWithIds(
|
|
212
|
+
['chartArea', 'sidebar', 'content'],
|
|
213
|
+
[
|
|
214
|
+
"document.getElementById('chartArea');",
|
|
215
|
+
"document.querySelector('#sidebar');",
|
|
216
|
+
"$('#content');",
|
|
217
|
+
]
|
|
218
|
+
);
|
|
219
|
+
const result = validatePage(html);
|
|
220
|
+
assert.strictEqual(result.valid, true);
|
|
221
|
+
assert.strictEqual(result.errors.length, 0);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('does NOT flag IDs created by innerHTML string literals', () => {
|
|
225
|
+
// A script renders a form at runtime via innerHTML, then queries
|
|
226
|
+
// its own generated IDs. Static inventory would flag #btnSave
|
|
227
|
+
// and #mapDate as missing — dynamic inventory covers them.
|
|
228
|
+
const html = pageWithIds(['root'],
|
|
229
|
+
"document.getElementById('root').innerHTML = '<button id=\"btnSave\">Save</button><input id=\"mapDate\">';" +
|
|
230
|
+
"document.getElementById('btnSave').addEventListener('click', function(){});" +
|
|
231
|
+
"document.querySelector('#mapDate').value = '';"
|
|
232
|
+
);
|
|
233
|
+
const result = validatePage(html);
|
|
234
|
+
assert.strictEqual(result.valid, true,
|
|
235
|
+
'innerHTML-created IDs should not trigger missing-element errors');
|
|
236
|
+
assert.strictEqual(result.errors.length, 0);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('does NOT flag IDs created via template literal innerHTML', () => {
|
|
240
|
+
// Realistic render() pattern using backtick template string.
|
|
241
|
+
const html = pageWithIds(['qbBody'],
|
|
242
|
+
'const rows = `<tr><td><input id="btnSaveAll"></td><td><select id="filterCategory"></select></td></tr>`;' +
|
|
243
|
+
"document.getElementById('qbBody').innerHTML = rows;" +
|
|
244
|
+
"document.getElementById('btnSaveAll').onclick = function(){};" +
|
|
245
|
+
"document.getElementById('filterCategory').onchange = function(){};"
|
|
246
|
+
);
|
|
247
|
+
const result = validatePage(html);
|
|
248
|
+
assert.strictEqual(result.valid, true);
|
|
249
|
+
assert.strictEqual(result.errors.length, 0);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('does NOT flag IDs assigned via .id property', () => {
|
|
253
|
+
const html = pageWithIds(['root'],
|
|
254
|
+
"const el = document.createElement('div');" +
|
|
255
|
+
"el.id = 'dynHeader';" +
|
|
256
|
+
"document.getElementById('root').appendChild(el);" +
|
|
257
|
+
"document.getElementById('dynHeader').textContent = 'x';"
|
|
258
|
+
);
|
|
259
|
+
const result = validatePage(html);
|
|
260
|
+
assert.strictEqual(result.valid, true);
|
|
261
|
+
assert.strictEqual(result.errors.length, 0);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('still flags truly missing IDs when script never creates them', () => {
|
|
265
|
+
// Make sure the widened inventory doesn't swallow every reference.
|
|
266
|
+
const html = pageWithIds(['root'],
|
|
267
|
+
"document.getElementById('root').innerHTML = '<div id=\"realId\"></div>';" +
|
|
268
|
+
"document.getElementById('bogusId').click();"
|
|
269
|
+
);
|
|
270
|
+
const result = validatePage(html);
|
|
271
|
+
assert.strictEqual(result.valid, false);
|
|
272
|
+
const missing = result.errors.filter(e => e.type === 'missing-element');
|
|
273
|
+
assert.strictEqual(missing.length, 1);
|
|
274
|
+
assert.ok(missing[0].message.includes('#bogusId'));
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('does NOT run when Layer 1 found syntax errors', () => {
|
|
278
|
+
// The script has a syntax error AND references a missing ID.
|
|
279
|
+
// Layer 2 should be skipped, so only the syntax error appears.
|
|
280
|
+
const html = `<html><head></head><body>
|
|
281
|
+
<script>
|
|
282
|
+
document.getElementById('missing');
|
|
283
|
+
const x = {
|
|
284
|
+
</script>
|
|
285
|
+
</body></html>`;
|
|
286
|
+
const result = validatePage(html);
|
|
287
|
+
assert.strictEqual(result.valid, false);
|
|
288
|
+
assert.ok(result.errors.every(e => e.type !== 'missing-element'),
|
|
289
|
+
'missing-element errors should not appear when there are syntax errors');
|
|
290
|
+
assert.ok(result.errors.some(e => e.type === 'syntax-error'));
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// -----------------------------------------------------------------------
|
|
295
|
+
// Layer 3: SynthOS API Pattern Checks
|
|
296
|
+
// -----------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
describe('Layer 3 — SynthOS API checks', () => {
|
|
299
|
+
it('catches unknown synthos API call', () => {
|
|
300
|
+
const html = page('', 'await synthos.generate.video();');
|
|
301
|
+
const result = validatePage(html);
|
|
302
|
+
assert.strictEqual(result.valid, false);
|
|
303
|
+
assert.ok(result.errors.some(e =>
|
|
304
|
+
e.type === 'unknown-api' && e.message.includes('synthos.generate.video')
|
|
305
|
+
));
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('catches multiple unknown API calls', () => {
|
|
309
|
+
const html = page('', [
|
|
310
|
+
'synthos.ai.think();',
|
|
311
|
+
'synthos.render.pdf();',
|
|
312
|
+
]);
|
|
313
|
+
const result = validatePage(html);
|
|
314
|
+
const unknownApis = result.errors.filter(e => e.type === 'unknown-api');
|
|
315
|
+
assert.strictEqual(unknownApis.length, 2);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('deduplicates same unknown API call across scripts', () => {
|
|
319
|
+
const html = page('', [
|
|
320
|
+
'synthos.generate.video();',
|
|
321
|
+
'synthos.generate.video();',
|
|
322
|
+
]);
|
|
323
|
+
const result = validatePage(html);
|
|
324
|
+
const unknownApis = result.errors.filter(e => e.type === 'unknown-api');
|
|
325
|
+
assert.strictEqual(unknownApis.length, 1);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('deduplicates same unknown API call within one script', () => {
|
|
329
|
+
const html = page('', `
|
|
330
|
+
synthos.generate.video();
|
|
331
|
+
synthos.generate.video();
|
|
332
|
+
synthos.generate.video();
|
|
333
|
+
`);
|
|
334
|
+
const result = validatePage(html);
|
|
335
|
+
const unknownApis = result.errors.filter(e => e.type === 'unknown-api');
|
|
336
|
+
assert.strictEqual(unknownApis.length, 1);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('runs even when there are syntax errors (Layer 3 always runs)', () => {
|
|
340
|
+
const html = page('', [
|
|
341
|
+
'const x = {', // syntax error
|
|
342
|
+
'synthos.generate.video();', // unknown API
|
|
343
|
+
]);
|
|
344
|
+
const result = validatePage(html);
|
|
345
|
+
assert.ok(result.errors.some(e => e.type === 'syntax-error'), 'should have syntax error');
|
|
346
|
+
assert.ok(result.errors.some(e => e.type === 'unknown-api'), 'should have unknown-api error');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe('known methods pass', () => {
|
|
350
|
+
const knownCalls = [
|
|
351
|
+
'synthos.data.save',
|
|
352
|
+
'synthos.data.get',
|
|
353
|
+
'synthos.data.list',
|
|
354
|
+
'synthos.data.remove',
|
|
355
|
+
'synthos.files.upload',
|
|
356
|
+
'synthos.files.url',
|
|
357
|
+
'synthos.files.list',
|
|
358
|
+
'synthos.files.remove',
|
|
359
|
+
'synthos.generate.image',
|
|
360
|
+
'synthos.generate.completion',
|
|
361
|
+
'synthos.script.run',
|
|
362
|
+
'synthos.page.list',
|
|
363
|
+
'synthos.page.get',
|
|
364
|
+
'synthos.page.update',
|
|
365
|
+
'synthos.page.remove',
|
|
366
|
+
'synthos.page.ask',
|
|
367
|
+
'synthos.search.web',
|
|
368
|
+
'synthos.connector.call',
|
|
369
|
+
'synthos.connector.list',
|
|
370
|
+
'synthos.agent.list',
|
|
371
|
+
'synthos.agent.send',
|
|
372
|
+
'synthos.agent.sendStream',
|
|
373
|
+
'synthos.agent.chat.send',
|
|
374
|
+
'synthos.agent.chat.sendStream',
|
|
375
|
+
'synthos.agent.chat.history',
|
|
376
|
+
'synthos.agent.chat.abort',
|
|
377
|
+
'synthos.agent.chat.clear',
|
|
378
|
+
'synthos.agent.isEnabled',
|
|
379
|
+
'synthos.agent.getCapabilities',
|
|
380
|
+
'synthos.shell.navigate',
|
|
381
|
+
'synthos.shell.submitChat',
|
|
382
|
+
'synthos.shell.setDirty',
|
|
383
|
+
'synthos.shell.showLoading',
|
|
384
|
+
'synthos.shell.hideLoading',
|
|
385
|
+
'synthos.shell.focusChat',
|
|
386
|
+
'synthos.shell.on',
|
|
387
|
+
'synthos.shared.data.save',
|
|
388
|
+
'synthos.shared.data.get',
|
|
389
|
+
'synthos.shared.files.upload',
|
|
390
|
+
'synthos.shared.files.url',
|
|
391
|
+
];
|
|
392
|
+
|
|
393
|
+
for (const method of knownCalls) {
|
|
394
|
+
it(`${method} → no error`, () => {
|
|
395
|
+
const html = page('', `await ${method}('test');`);
|
|
396
|
+
const result = validatePage(html);
|
|
397
|
+
const unknownApis = result.errors.filter(e => e.type === 'unknown-api');
|
|
398
|
+
assert.strictEqual(unknownApis.length, 0, `${method} should be recognized`);
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// -----------------------------------------------------------------------
|
|
405
|
+
// Clean Pages — Should Pass
|
|
406
|
+
// -----------------------------------------------------------------------
|
|
407
|
+
|
|
408
|
+
describe('Clean pages pass validation', () => {
|
|
409
|
+
it('simple page with valid script', () => {
|
|
410
|
+
const html = pageWithIds(['app'], "document.getElementById('app').textContent = 'Hello';");
|
|
411
|
+
const result = validatePage(html);
|
|
412
|
+
assert.strictEqual(result.valid, true);
|
|
413
|
+
assert.strictEqual(result.errors.length, 0);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('page with no scripts', () => {
|
|
417
|
+
const html = '<html><head></head><body><h1>Static page</h1></body></html>';
|
|
418
|
+
const result = validatePage(html);
|
|
419
|
+
assert.strictEqual(result.valid, true);
|
|
420
|
+
assert.strictEqual(result.errors.length, 0);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('page with known synthos calls and matching IDs', () => {
|
|
424
|
+
const html = pageWithIds(
|
|
425
|
+
['output', 'canvas'],
|
|
426
|
+
[
|
|
427
|
+
"const data = await synthos.data.get('myKey');",
|
|
428
|
+
"document.getElementById('output').textContent = data;",
|
|
429
|
+
"const img = await synthos.generate.image('a cat');",
|
|
430
|
+
]
|
|
431
|
+
);
|
|
432
|
+
const result = validatePage(html);
|
|
433
|
+
assert.strictEqual(result.valid, true);
|
|
434
|
+
assert.strictEqual(result.errors.length, 0);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('page with only external scripts', () => {
|
|
438
|
+
const html = `<html><head>
|
|
439
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
440
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
441
|
+
</head><body></body></html>`;
|
|
442
|
+
const result = validatePage(html);
|
|
443
|
+
assert.strictEqual(result.valid, true);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('page mixing external and valid inline scripts', () => {
|
|
447
|
+
const html = `<html><head>
|
|
448
|
+
<script src="https://cdn.example.com/lib.js"></script>
|
|
449
|
+
</head><body>
|
|
450
|
+
<div id="chart"></div>
|
|
451
|
+
<script>document.getElementById('chart').style.height = '400px';</script>
|
|
452
|
+
</body></html>`;
|
|
453
|
+
const result = validatePage(html);
|
|
454
|
+
assert.strictEqual(result.valid, true);
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// -----------------------------------------------------------------------
|
|
459
|
+
// Result structure
|
|
460
|
+
// -----------------------------------------------------------------------
|
|
461
|
+
|
|
462
|
+
describe('Result structure', () => {
|
|
463
|
+
it('includes durationMs', () => {
|
|
464
|
+
const html = page('', 'const x = 1;');
|
|
465
|
+
const result = validatePage(html);
|
|
466
|
+
assert.ok(typeof result.durationMs === 'number');
|
|
467
|
+
assert.ok(result.durationMs >= 0);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('valid is true when errors is empty', () => {
|
|
471
|
+
const html = page('', 'const x = 1;');
|
|
472
|
+
const result = validatePage(html);
|
|
473
|
+
assert.strictEqual(result.valid, true);
|
|
474
|
+
assert.strictEqual(result.errors.length, 0);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('valid is false when errors is non-empty', () => {
|
|
478
|
+
const html = page('', 'const x = {');
|
|
479
|
+
const result = validatePage(html);
|
|
480
|
+
assert.strictEqual(result.valid, false);
|
|
481
|
+
assert.ok(result.errors.length > 0);
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// -----------------------------------------------------------------------
|
|
486
|
+
// Combined scenarios
|
|
487
|
+
// -----------------------------------------------------------------------
|
|
488
|
+
|
|
489
|
+
describe('Combined scenarios', () => {
|
|
490
|
+
it('syntax error + unknown API → both reported (no missing-element)', () => {
|
|
491
|
+
const html = `<html><head></head><body>
|
|
492
|
+
<script>
|
|
493
|
+
document.getElementById('nope');
|
|
494
|
+
const x = {
|
|
495
|
+
</script>
|
|
496
|
+
<script>
|
|
497
|
+
synthos.generate.video();
|
|
498
|
+
</script>
|
|
499
|
+
</body></html>`;
|
|
500
|
+
const result = validatePage(html);
|
|
501
|
+
assert.strictEqual(result.valid, false);
|
|
502
|
+
const types = errorTypes(result);
|
|
503
|
+
assert.ok(types.includes('syntax-error'));
|
|
504
|
+
assert.ok(types.includes('unknown-api'));
|
|
505
|
+
assert.ok(!types.includes('missing-element'), 'Layer 2 should be skipped');
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('missing element + unknown API → both reported', () => {
|
|
509
|
+
const html = pageWithIds(['header'], [
|
|
510
|
+
"document.getElementById('footer');",
|
|
511
|
+
"synthos.fake.method();",
|
|
512
|
+
]);
|
|
513
|
+
const result = validatePage(html);
|
|
514
|
+
assert.strictEqual(result.valid, false);
|
|
515
|
+
const types = errorTypes(result);
|
|
516
|
+
assert.ok(types.includes('missing-element'));
|
|
517
|
+
assert.ok(types.includes('unknown-api'));
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('realistic page with bridge, helpers, and user script', () => {
|
|
521
|
+
const html = `<html><head></head><body>
|
|
522
|
+
<div id="viewerPanel">
|
|
523
|
+
<div id="chatArea"></div>
|
|
524
|
+
<div id="output"></div>
|
|
525
|
+
</div>
|
|
526
|
+
<script id="page-bridge">
|
|
527
|
+
// Bridge code — should be skipped
|
|
528
|
+
window.parent.postMessage({ type: 'shell:ready' }, '*');
|
|
529
|
+
synthos.internal.hook();
|
|
530
|
+
</script>
|
|
531
|
+
<script id="page-helpers">
|
|
532
|
+
// Helpers — should be skipped
|
|
533
|
+
window.synthos = new Proxy({}, { get: () => () => {} });
|
|
534
|
+
</script>
|
|
535
|
+
<script>
|
|
536
|
+
// User code — should be validated
|
|
537
|
+
const area = document.getElementById('chatArea');
|
|
538
|
+
const out = document.getElementById('output');
|
|
539
|
+
const data = await synthos.data.get('key');
|
|
540
|
+
out.textContent = JSON.stringify(data);
|
|
541
|
+
</script>
|
|
542
|
+
</body></html>`;
|
|
543
|
+
const result = validatePage(html);
|
|
544
|
+
assert.strictEqual(result.valid, true);
|
|
545
|
+
assert.strictEqual(result.errors.length, 0);
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import { UserProfile, renderUserProfile } from '../src/settings';
|
|
3
|
+
|
|
4
|
+
describe('renderUserProfile', () => {
|
|
5
|
+
it('returns empty string when profile is undefined', () => {
|
|
6
|
+
assert.strictEqual(renderUserProfile(undefined), '');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('returns empty string when profile has no filled fields', () => {
|
|
10
|
+
const profile: UserProfile = { personal: {}, business: {} };
|
|
11
|
+
assert.strictEqual(renderUserProfile(profile), '');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('treats whitespace-only fields as empty', () => {
|
|
15
|
+
const profile: UserProfile = {
|
|
16
|
+
personal: { name: ' ', address: '\n\t' },
|
|
17
|
+
business: { name: '' },
|
|
18
|
+
};
|
|
19
|
+
assert.strictEqual(renderUserProfile(profile), '');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('renders only Personal section when business is empty', () => {
|
|
23
|
+
const profile: UserProfile = {
|
|
24
|
+
personal: { name: 'Jane Doe' },
|
|
25
|
+
};
|
|
26
|
+
const out = renderUserProfile(profile);
|
|
27
|
+
assert.ok(out.startsWith('## Personal Information'));
|
|
28
|
+
assert.ok(out.includes('**Name:**\nJane Doe'));
|
|
29
|
+
assert.ok(!out.includes('## Business Information'));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('renders only Business section when personal is empty', () => {
|
|
33
|
+
const profile: UserProfile = {
|
|
34
|
+
business: { name: 'My Bakery', hours: 'Mon-Fri 9-5' },
|
|
35
|
+
};
|
|
36
|
+
const out = renderUserProfile(profile);
|
|
37
|
+
assert.ok(out.startsWith('## Business Information'));
|
|
38
|
+
assert.ok(out.includes('**Name:**\nMy Bakery'));
|
|
39
|
+
assert.ok(out.includes('**Hours:**\nMon-Fri 9-5'));
|
|
40
|
+
assert.ok(!out.includes('## Personal Information'));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('renders both sections with Personal first', () => {
|
|
44
|
+
const profile: UserProfile = {
|
|
45
|
+
personal: { name: 'Jane', address: '123 Main St' },
|
|
46
|
+
business: { name: 'My Bakery', locations: '123 Main St' },
|
|
47
|
+
};
|
|
48
|
+
const out = renderUserProfile(profile);
|
|
49
|
+
assert.ok(out.indexOf('## Personal Information') < out.indexOf('## Business Information'));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('renders fields in the declared order within a section', () => {
|
|
53
|
+
const profile: UserProfile = {
|
|
54
|
+
business: {
|
|
55
|
+
details: 'Family-owned',
|
|
56
|
+
hours: 'Mon-Fri 9-5',
|
|
57
|
+
name: 'My Bakery',
|
|
58
|
+
locations: '123 Main St',
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
const out = renderUserProfile(profile);
|
|
62
|
+
const iName = out.indexOf('**Name:**');
|
|
63
|
+
const iLocations = out.indexOf('**Locations:**');
|
|
64
|
+
const iHours = out.indexOf('**Hours:**');
|
|
65
|
+
const iDetails = out.indexOf('**Details:**');
|
|
66
|
+
assert.ok(iName < iLocations);
|
|
67
|
+
assert.ok(iLocations < iHours);
|
|
68
|
+
assert.ok(iHours < iDetails);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('omits individual empty fields within a section', () => {
|
|
72
|
+
const profile: UserProfile = {
|
|
73
|
+
personal: { name: 'Jane', address: '', details: 'vegetarian' },
|
|
74
|
+
};
|
|
75
|
+
const out = renderUserProfile(profile);
|
|
76
|
+
assert.ok(out.includes('**Name:**\nJane'));
|
|
77
|
+
assert.ok(!out.includes('**Address:**'));
|
|
78
|
+
assert.ok(out.includes('**Details:**\nvegetarian'));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('preserves multi-line field values verbatim (trimmed)', () => {
|
|
82
|
+
const profile: UserProfile = {
|
|
83
|
+
business: { locations: '123 Main St\nSpringfield, IL\nSuite 4' },
|
|
84
|
+
};
|
|
85
|
+
const out = renderUserProfile(profile);
|
|
86
|
+
assert.ok(out.includes('**Locations:**\n123 Main St\nSpringfield, IL\nSuite 4'));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('trims leading/trailing whitespace on values', () => {
|
|
90
|
+
const profile: UserProfile = {
|
|
91
|
+
personal: { name: ' Jane Doe \n' },
|
|
92
|
+
};
|
|
93
|
+
const out = renderUserProfile(profile);
|
|
94
|
+
assert.ok(out.includes('**Name:**\nJane Doe'));
|
|
95
|
+
assert.ok(!out.includes(' Jane Doe'));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('renders a fully populated profile with both sections', () => {
|
|
99
|
+
const profile: UserProfile = {
|
|
100
|
+
personal: {
|
|
101
|
+
name: 'Jane Doe',
|
|
102
|
+
address: '123 Main St, Springfield IL',
|
|
103
|
+
details: 'Vegetarian, prefers dark mode',
|
|
104
|
+
},
|
|
105
|
+
business: {
|
|
106
|
+
name: 'My Bakery',
|
|
107
|
+
locations: '123 Main St, Springfield IL',
|
|
108
|
+
hours: 'Mon-Fri 7am-6pm',
|
|
109
|
+
details: 'Family-owned since 2012',
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
const out = renderUserProfile(profile);
|
|
113
|
+
assert.ok(out.includes('## Personal Information'));
|
|
114
|
+
assert.ok(out.includes('## Business Information'));
|
|
115
|
+
assert.ok(out.includes('**Name:**\nJane Doe'));
|
|
116
|
+
assert.ok(out.includes('**Address:**\n123 Main St, Springfield IL'));
|
|
117
|
+
assert.ok(out.includes('**Details:**\nVegetarian, prefers dark mode'));
|
|
118
|
+
assert.ok(out.includes('**Name:**\nMy Bakery'));
|
|
119
|
+
assert.ok(out.includes('**Hours:**\nMon-Fri 7am-6pm'));
|
|
120
|
+
assert.ok(out.includes('**Details:**\nFamily-owned since 2012'));
|
|
121
|
+
});
|
|
122
|
+
});
|