@zenuml/core 3.46.4 → 3.46.6
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/.claude/skills/babysit-pr/SKILL.md +203 -0
- package/.claude/skills/babysit-pr/agents/openai.yaml +7 -0
- package/.claude/skills/dia-scoring/SKILL.md +1 -1
- package/.claude/skills/propagate-core-release/SKILL.md +200 -0
- package/.claude/skills/propagate-core-release/agents/openai.yaml +7 -0
- package/.claude/skills/propagate-core-release/references/downstreams.md +41 -0
- package/.claude/skills/ship-branch/SKILL.md +13 -2
- package/dist/stats.html +1 -1
- package/dist/zenuml.esm.mjs +3 -3
- package/dist/zenuml.js +3 -3
- package/docs/superpowers/plans/2026-03-27-e2e-test-reorg.md +698 -0
- package/{cy → e2e/data}/compare-cases.js +70 -37
- package/{cy/smoke-editable-label.html → e2e/fixtures/editable-label.html} +1 -1
- package/{cy/editable-span-test.html → e2e/fixtures/editable-span.html} +1 -1
- package/e2e/fixtures/fixture.html +31 -0
- package/{cy → e2e/tools}/canonical-history.html +1 -1
- package/{cy → e2e/tools}/compare-case.html +3 -3
- package/{cy → e2e/tools}/compare.html +2 -2
- package/{cy → e2e/tools}/native-diff-ext/content.js +2 -2
- package/firebase-debug.log +108 -0
- package/index.html +2 -2
- package/mermaid-zenuml-async-spa-auth.png +0 -0
- package/mermaid-zenuml-async-spa-auth.snapshot.md +96 -0
- package/package.json +1 -1
- package/scripts/analyze-compare-case/collect-data.mjs +1 -1
- package/scripts/analyze-compare-case.mjs +1 -1
- package/skills/dia-scoring/SKILL.md +1 -1
- package/vite.config.ts +5 -5
- package/cy/async-message-1.html +0 -32
- package/cy/async-message-2.html +0 -46
- package/cy/async-message-3.html +0 -41
- package/cy/creation-rtl.html +0 -28
- package/cy/defect-406-alt-under-creation.html +0 -40
- package/cy/demo1.html +0 -28
- package/cy/demo3.html +0 -28
- package/cy/demo4.html +0 -28
- package/cy/element-report.html +0 -705
- package/cy/fragments-with-return.html +0 -35
- package/cy/icons-test.html +0 -29
- package/cy/if-fragment.html +0 -28
- package/cy/legacy-vs-html.html +0 -291
- package/cy/named-parameters.html +0 -30
- package/cy/nested-interaction-with-fragment.html +0 -34
- package/cy/nested-interaction-with-outbound.html +0 -34
- package/cy/parity-test.html +0 -122
- package/cy/return-in-nested-if.html +0 -29
- package/cy/return.html +0 -38
- package/cy/self-sync-message-at-root.html +0 -28
- package/cy/smoke-creation.html +0 -26
- package/cy/smoke-fragment-issue.html +0 -36
- package/cy/smoke-fragment.html +0 -42
- package/cy/smoke-interaction.html +0 -34
- package/cy/smoke.html +0 -40
- package/cy/theme-default-test.html +0 -28
- package/cy/vertical-1.html +0 -25
- package/cy/vertical-10.html +0 -33
- package/cy/vertical-11.html +0 -29
- package/cy/vertical-2.html +0 -23
- package/cy/vertical-3.html +0 -37
- package/cy/vertical-4.html +0 -42
- package/cy/vertical-5.html +0 -40
- package/cy/vertical-6.html +0 -29
- package/cy/vertical-7.html +0 -27
- package/cy/vertical-8.html +0 -32
- package/cy/vertical-9.html +0 -25
- package/cy/xss.html +0 -21
- /package/{cy → e2e/data}/diff-algorithm.js +0 -0
- /package/{cy → e2e/fixtures}/svg-test.html +0 -0
- /package/{cy → e2e/tools}/native-diff-ext/background.js +0 -0
- /package/{cy → e2e/tools}/native-diff-ext/bridge.js +0 -0
- /package/{cy → e2e/tools}/svg-preview.html +0 -0
package/cy/element-report.html
DELETED
|
@@ -1,705 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<title>Element Measurement Report</title>
|
|
6
|
-
<style>
|
|
7
|
-
* { box-sizing: border-box; }
|
|
8
|
-
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: #f1f5f9; }
|
|
9
|
-
.header {
|
|
10
|
-
padding: 12px 20px; background: #1e293b; color: white;
|
|
11
|
-
display: flex; align-items: center; gap: 16px; position: sticky; top: 0; z-index: 100;
|
|
12
|
-
}
|
|
13
|
-
.header h1 { margin: 0; font-size: 16px; font-weight: 600; }
|
|
14
|
-
.header button {
|
|
15
|
-
padding: 6px 16px; background: #7c3aed; border: none; border-radius: 4px;
|
|
16
|
-
color: white; font-size: 13px; cursor: pointer; font-weight: 500;
|
|
17
|
-
}
|
|
18
|
-
.header button:hover { background: #6d28d9; }
|
|
19
|
-
.header button:disabled { background: #475569; cursor: wait; }
|
|
20
|
-
.header .progress { font-size: 12px; color: #94a3b8; }
|
|
21
|
-
.summary {
|
|
22
|
-
margin: 12px 20px; padding: 12px 16px; background: white; border-radius: 6px;
|
|
23
|
-
font-size: 13px; border: 1px solid #e2e8f0;
|
|
24
|
-
}
|
|
25
|
-
.summary table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
26
|
-
.summary th { text-align: left; padding: 4px 8px; border-bottom: 2px solid #e2e8f0; color: #64748b; font-weight: 600; }
|
|
27
|
-
.summary td { padding: 4px 8px; border-bottom: 1px solid #f1f5f9; }
|
|
28
|
-
.summary .case-link { color: #7c3aed; text-decoration: none; cursor: pointer; }
|
|
29
|
-
.summary .case-link:hover { text-decoration: underline; }
|
|
30
|
-
.case-card {
|
|
31
|
-
margin: 12px 20px; background: white; border-radius: 6px;
|
|
32
|
-
border: 1px solid #e2e8f0; overflow: hidden;
|
|
33
|
-
}
|
|
34
|
-
.case-header {
|
|
35
|
-
padding: 8px 16px; background: #f8fafc; border-bottom: 1px solid #e2e8f0;
|
|
36
|
-
display: flex; align-items: center; gap: 12px; cursor: pointer;
|
|
37
|
-
}
|
|
38
|
-
.case-header:hover { background: #f1f5f9; }
|
|
39
|
-
.case-header h3 { margin: 0; font-size: 14px; font-weight: 600; }
|
|
40
|
-
.case-header .badge { font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 500; }
|
|
41
|
-
.case-header .badge.good { background: #dcfce7; color: #166534; }
|
|
42
|
-
.case-header .badge.warn { background: #fef9c3; color: #854d0e; }
|
|
43
|
-
.case-header .badge.bad { background: #fee2e2; color: #991b1b; }
|
|
44
|
-
.case-body { display: none; }
|
|
45
|
-
.case-body.open { display: block; }
|
|
46
|
-
.diagrams {
|
|
47
|
-
display: flex; gap: 0; border-bottom: 1px solid #e2e8f0;
|
|
48
|
-
}
|
|
49
|
-
.diagram-panel { flex: 1; overflow: auto; max-height: 400px; }
|
|
50
|
-
.diagram-panel h4 {
|
|
51
|
-
margin: 0; padding: 6px 12px; background: #f8fafc; font-size: 11px;
|
|
52
|
-
color: #64748b; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;
|
|
53
|
-
border-bottom: 1px solid #e2e8f0; position: sticky; top: 0; z-index: 1;
|
|
54
|
-
}
|
|
55
|
-
.diagram-panel .content { padding: 8px; background: white; }
|
|
56
|
-
.diagram-panel .content img { max-width: 100%; }
|
|
57
|
-
.diagram-panel .content svg { max-width: 100%; height: auto; }
|
|
58
|
-
.diagram-panel + .diagram-panel { border-left: 1px solid #e2e8f0; }
|
|
59
|
-
.measure-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
60
|
-
.measure-table th {
|
|
61
|
-
text-align: left; padding: 4px 8px; border-bottom: 2px solid #e2e8f0;
|
|
62
|
-
color: #64748b; font-weight: 600; font-size: 11px; position: sticky; top: 0;
|
|
63
|
-
background: white;
|
|
64
|
-
}
|
|
65
|
-
.measure-table td { padding: 3px 8px; border-bottom: 1px solid #f8fafc; font-family: 'SF Mono', monospace; font-size: 11px; }
|
|
66
|
-
.measure-table tr:hover { background: #f8fafc; }
|
|
67
|
-
.measure-table .el-name { font-weight: 500; color: #334155; }
|
|
68
|
-
.measure-table .delta { font-weight: 600; }
|
|
69
|
-
.measure-table .delta.ok { color: #16a34a; }
|
|
70
|
-
.measure-table .delta.warn { color: #ca8a04; }
|
|
71
|
-
.measure-table .delta.bad { color: #dc2626; }
|
|
72
|
-
.measure-table .section-row td { background: #f8fafc; font-weight: 600; color: #475569; padding-top: 8px; }
|
|
73
|
-
#workspace { position: absolute; left: 0; top: 0; width: 900px; opacity: 0; pointer-events: none; z-index: -1; }
|
|
74
|
-
</style>
|
|
75
|
-
</head>
|
|
76
|
-
<body>
|
|
77
|
-
<div class="header">
|
|
78
|
-
<h1>Element Measurement Report</h1>
|
|
79
|
-
<button id="run-btn">Generate Report</button>
|
|
80
|
-
<span class="progress" id="progress"></span>
|
|
81
|
-
</div>
|
|
82
|
-
<div id="summary"></div>
|
|
83
|
-
<div id="report"></div>
|
|
84
|
-
<div id="workspace"></div>
|
|
85
|
-
|
|
86
|
-
<script type="module">
|
|
87
|
-
import ZenUml from "/src/core.tsx";
|
|
88
|
-
import { renderToSvg } from "/src/svg/renderToSvg.ts";
|
|
89
|
-
import { toPng } from "html-to-image";
|
|
90
|
-
|
|
91
|
-
const CASES = {
|
|
92
|
-
"empty": ``,
|
|
93
|
-
"smoke": `title ABCD Title\n// Generating Sequence Diagrams from Java code is experimental.\n// Please report errors to https://github.com/ZenUml/jetbrains-zenuml/discussions\nMarkdownJavaFxHtmlPanel\nMarkdownJavaFxHtmlPanel.readFromInputStream(inputStream) {\n StringBuilder resultStringBuilder = new StringBuilder();\n try {\n // String line;\n while((line = br.readLine()) != null) {\n resultStringBuilder.append(line);\n }\n }\n catch(IOException) {\n return "";\n }\n return "resultStringBuilder.toString()";\n}`,
|
|
94
|
-
"simple-messages": `A -> B: hello\nB -> C: process\nC -> B: result\nB -> A: done`,
|
|
95
|
-
"self-call": `A.method() {\n B.method\n}`,
|
|
96
|
-
"if-fragment": `title Issue 232\nClient -> Server:SendRequest\nif(true){\n Server -> Server: processRequest\n}`,
|
|
97
|
-
"fragment-loop": `A -> B: request\nloop(condition) {\n B -> C: process\n}`,
|
|
98
|
-
"fragment-tcf": `A.method {\n try {\n B.process\n } catch(error) {\n C.handle\n } finally {\n D.cleanup\n }\n}`,
|
|
99
|
-
"creation": `title Title 1\nA.m {\n new B(1,2,3,4)\n}`,
|
|
100
|
-
"creation-rtl": `"b:B"\na1 = A.method() {\n // abcde\n b = new B()\n}`,
|
|
101
|
-
"defect-406": `title Title 1\nA.m1 {\n new B(1,2,3,4) {\n if(x) {\n C.m2\n }\n while(y) {\n D.m3\n }\n par {\n E.m4\n F.m5\n }\n opt {\n G.m6\n }\n }\n}`,
|
|
102
|
-
"fragment": `A\nB\nC\nif(x) {\n loop(y) {\n try {\n par {\n A.m();\n B.m();\n }\n } catch(e) {\n opt {\n new C\n }\n } finally {\n C.m\n }\n }\n}`,
|
|
103
|
-
"fragment-issue": `// This sample is carefully crafted. It shows a known issues: fragment stretched to\n// svc (should not), because parser thinks the return statement returns to svc.\ngroup Backend {@VPC svc @RDS rep}\ngroup { Client }\nClient->SGW."Get order by id" {\n svc.Get(id) {\n rep."load order" {\n if(order == null) {\n @return\n SGW->Client:401\n }\n }\n }\n}`,
|
|
104
|
-
"fragments-return": `A.method {\n if(x) {\n return x\n } else {\n return y\n }\n try {\n return 1\n } catch {\n return 2\n } finally {\n return 3\n }\n}`,
|
|
105
|
-
"interaction": `if(x) {\n A.method() {\n B.method() {\n BSelfMethod00000000000\n A.method()\n }\n ASelf {\n B->A.method\n }\n }\n}`,
|
|
106
|
-
"async-1": `A->A: async\nA->B: async\nA->C: async\nB->B: async\nB->C: async\nB->A: async\nC->C: async\nC->B: async\nC->A: async`,
|
|
107
|
-
"async-2": `A.method {\n A->A: async\n A->B: async\n A->C: async\n B->B: async\n B->C: async\n B->A: async\n C->C: async\n C->B: async\n C->A: async\n B.method {\n A->A: async\n A->B: async\n A->C: async\n B->B: async\n B->C: async\n B->A: async\n C->C: async\n C->B: async\n C->A: async\n }\n}`,
|
|
108
|
-
"async-3": `A B C\nC.method {\n A->C: async\n C->A: async\n C->B: async\n B->C: async\n B.method {\n A->A: async\n A->B: async\n A->C: async\n B->B: async\n B->C: async\n B->A: async\n C->C: async\n C->B: async\n C->A: async\n }\n}`,
|
|
109
|
-
"return": `A B C D\nA->B.method() {\n ret0_assign_rtl =C.method_long_to_give_space {\n @return C->D: ret1_annotation_ltr\n ret5_assign_ltr = B.method\n B.method2 {\n return ret2_return_ltr\n }\n }\n return ret2_return_rtl\n @return B->A: ret4_annotation_rtl\n}`,
|
|
110
|
-
"self-sync": `selfSync() {\n A.method {\n B.method\n }\n}`,
|
|
111
|
-
"nested-fragment": `title Nested Interaction\nA.Read() {\n B.Submit() {\n Process() {\n if (true) {\n ProcessCallback() {\n A.method\n }\n }\n }\n }\n}`,
|
|
112
|
-
"nested-outbound": `title Nested Interaction with Outbound\nA.Read() {\n B.Submit() {\n C->B.method {\n if (true) {\n ProcessCallback() {\n A.method\n }\n }\n }\n }\n}`,
|
|
113
|
-
"named-params": `title Named Parameters Test\n// Testing named parameter syntax (param=value)\nA.method(userId=123, name="John")\nB.create(type="User", active=true)\nC.mixedCall(1, name="Mixed", enabled=false)\nD.oldStyle(1, 2, 3)\nE.complex(first="value1", second=42, third=true, fourth="final")`,
|
|
114
|
-
"vertical-1": `// red\n// green\na = A.m111\nnew E`,
|
|
115
|
-
"vertical-2": `// [red]\nnew B`,
|
|
116
|
-
"vertical-3": `if(x) {\n // comment\n new A\n} else {\n new B\n}\nnew C\ntry {\n new D\n} catch {\n par {\n new E\n new F\n }\n}`,
|
|
117
|
-
"vertical-4": `if(x) {\n // comment\n new A\n} else {\n new B\n}\nnew C\ntry {\n new D\n} catch {\n par {\n new E\n new F\n if(x) {\n new X\n } else {\n new Y\n }\n }\n}`,
|
|
118
|
-
"vertical-5": `par {\n new F\n if(x) {\n new X\n } else {\n try {\n new Y\n } catch {\n par {\n new G\n if(x) {\n new H\n } else {\n new I\n }\n }\n }\n }\n}`,
|
|
119
|
-
"vertical-6": `new a\nif(x) {\n\tnew b\n} else {\n\tnew c\n\tnew e\n}\nnew D`,
|
|
120
|
-
"vertical-7": `A.method\nsection(){\n new B\n}`,
|
|
121
|
-
"vertical-8": `new Creation() {\n return from_creation\n}\nreturn "from if to original source"\ntry {\n new AHasAVeryLongNameLongNameLongNameLongName() {\n new CreatWithinCreat()\n }\n}`,
|
|
122
|
-
"vertical-9": `A0->A0: self\nnew A`,
|
|
123
|
-
"vertical-10": `new E\nE.messageA()\nnew A {\n if (x) {\n new D\n }\n new B {\n new C\n }\n}`,
|
|
124
|
-
"vertical-11": `A.call {\n // pre creation\n A->B: prep\n a = new A()\n a->B: post\n}`,
|
|
125
|
-
"demo1-smoke": `// comments at the beginning should be ignored\ntitle This is a title\n@Lambda <<stereotype>> ParticipantName\ngroup "B C" {@EC2 B @ECS C}\n"bg color" #FF0000\n@Starter("OptionalStarter")\nnew B\nReturnType ret = ParticipantName.methodA(a, b) {\n critical("This is a critical message") {\n ReturnType ret2 = selfCall() {\n B.syncCallWithinSelfCall() {\n ParticipantName.rightToLeftCall()\n return B\n }\n "space in name"->"bg color".syncMethod(from, to)\n }\n }\n // A comment for alt\n if (condition) {\n // A comment for creation\n ret = new CreatAndAssign()\n "ret:CreatAndAssign".method(create, and, assign)\n // A comment for async self\n B->B: Self Async\n // A comment for async message\n B->C: Async Message within fragment\n new Creation() {\n return from_creation\n }\n return "from if to original source"\n try {\n new AHasAVeryLongNameLongNameLongNameLongName() {\n new CreatWithinCreat()\n C.rightToLeftFromCreation() {\n B.FurtherRightToLeftFromCreation()\n }\n }\n } catch (Exception) {\n self {\n return C\n }\n } finally {\n C: async call from implied source\n }\n =====divider can be anywhere=====\n } else if ("another condition") {\n par {\n B.method\n C.method\n }\n } else {\n // A comment for loop\n forEach(Z) {\n Z.method() {\n return Z\n }\n }\n }\n}`,
|
|
126
|
-
"demo3-nested-fragments": `ret = A.methodA() {\n if (x) {\n B.methodB()\n if (y) {\n C.methodC()\n }\n }\n while (x) {\n B.methodB()\n while (y) {\n C.methodC()\n }\n }\n if (x) {\n method()\n if (y) {\n method2()\n }\n }\n while (x) {\n method()\n while (y) {\n method2()\n }\n }\n while (x) {\n method()\n if (y) {\n method2()\n }\n }\n if (x) {\n method()\n while (y) {\n method2()\n }\n }\n}`,
|
|
127
|
-
"demo4-fragment-span": `ret = A.methodA() {\n B.method() {\n if (X) {\n C.methodC() {\n a = A.methodA() {\n D.method()\n }\n }\n }\n while (Y) {\n C.methodC() {\n A.methodA()\n }\n }\n }\n }`,
|
|
128
|
-
"demo5-self-named": `A.methodA() { A.methodA1() }`,
|
|
129
|
-
"demo6-async-styled": `A->A:: Hello\nA->B:: Hello B\nB->A: So what`,
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
// ——— Measurement functions ———
|
|
133
|
-
|
|
134
|
-
/** Measure elements in the live HTML DOM, relative to the seq-diagram container */
|
|
135
|
-
function measureHtmlElements(container) {
|
|
136
|
-
const ref = container.getBoundingClientRect();
|
|
137
|
-
const rel = (r) => ({
|
|
138
|
-
x: Math.round((r.left - ref.left) * 10) / 10,
|
|
139
|
-
y: Math.round((r.top - ref.top) * 10) / 10,
|
|
140
|
-
w: Math.round(r.width * 10) / 10,
|
|
141
|
-
h: Math.round(r.height * 10) / 10,
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
const m = { participants: [], occurrences: [], messages: [], selfCalls: [], fragments: [], returns: [], diagram: {} };
|
|
145
|
-
m.diagram = { width: Math.round(ref.width * 10) / 10, height: Math.round(ref.height * 10) / 10 };
|
|
146
|
-
|
|
147
|
-
// Participants
|
|
148
|
-
container.querySelectorAll("[data-participant-id]").forEach((el) => {
|
|
149
|
-
const r = rel(el.getBoundingClientRect());
|
|
150
|
-
m.participants.push({
|
|
151
|
-
name: el.getAttribute("data-participant-id"),
|
|
152
|
-
centerX: Math.round((r.x + r.w / 2) * 10) / 10,
|
|
153
|
-
topY: r.y, width: r.w, height: r.h,
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// Occurrences
|
|
158
|
-
container.querySelectorAll('[data-el-type="occurrence"]').forEach((el, i) => {
|
|
159
|
-
const r = rel(el.getBoundingClientRect());
|
|
160
|
-
m.occurrences.push({
|
|
161
|
-
participant: el.getAttribute("data-belongs-to") || `occ-${i}`,
|
|
162
|
-
idx: i, x: r.x, y: r.y, width: r.w, height: r.h,
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// Messages (non-self sync/async interactions)
|
|
167
|
-
container.querySelectorAll('.interaction:not(.return):not(.creation)').forEach((el, i) => {
|
|
168
|
-
const isSelf = el.classList.contains("self-invocation") ||
|
|
169
|
-
el.querySelector(".self-invocation") !== null ||
|
|
170
|
-
(el.classList.contains("self"));
|
|
171
|
-
if (isSelf) return; // handled separately
|
|
172
|
-
|
|
173
|
-
const msgDiv = el.querySelector(".message");
|
|
174
|
-
if (!msgDiv) return;
|
|
175
|
-
const r = rel(msgDiv.getBoundingClientRect());
|
|
176
|
-
const to = el.getAttribute("data-to") || "";
|
|
177
|
-
const sig = el.getAttribute("data-signature") || "";
|
|
178
|
-
m.messages.push({
|
|
179
|
-
idx: i, label: sig || to,
|
|
180
|
-
fromX: r.x, toX: r.x + r.w,
|
|
181
|
-
y: r.y + r.h, // bottom of div = where the border-bottom line is
|
|
182
|
-
width: r.w, height: r.h,
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// Self-calls
|
|
187
|
-
container.querySelectorAll('.self-invocation').forEach((el, i) => {
|
|
188
|
-
const arrowSvg = el.querySelector("svg.arrow") || el.querySelector("svg");
|
|
189
|
-
if (!arrowSvg) return;
|
|
190
|
-
const r = rel(arrowSvg.getBoundingClientRect());
|
|
191
|
-
const interaction = el.closest(".interaction");
|
|
192
|
-
const sig = interaction?.getAttribute("data-signature") || "";
|
|
193
|
-
m.selfCalls.push({
|
|
194
|
-
idx: i, label: sig,
|
|
195
|
-
x: r.x, y: r.y, width: r.w, height: r.h,
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
// Fragments
|
|
200
|
-
container.querySelectorAll(".fragment").forEach((el, i) => {
|
|
201
|
-
const r = rel(el.getBoundingClientRect());
|
|
202
|
-
const kind = [...el.classList].find(c => c.startsWith("fragment-"))?.replace("fragment-", "") ||
|
|
203
|
-
[...el.classList].find(c => ["alt", "loop", "opt", "par", "tcf", "critical", "section", "ref"].includes(c)) || "?";
|
|
204
|
-
m.fragments.push({
|
|
205
|
-
idx: i, kind, x: r.x, y: r.y, width: r.w, height: r.h,
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// Returns
|
|
210
|
-
container.querySelectorAll('.interaction.return[data-type="return"]').forEach((el, i) => {
|
|
211
|
-
const msgDiv = el.querySelector(".message");
|
|
212
|
-
if (!msgDiv) return;
|
|
213
|
-
const r = rel(msgDiv.getBoundingClientRect());
|
|
214
|
-
const sig = el.getAttribute("data-signature") || "";
|
|
215
|
-
m.returns.push({
|
|
216
|
-
idx: i, label: sig,
|
|
217
|
-
fromX: r.x, toX: r.x + r.w, y: r.y + r.h,
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
return m;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/** Measure elements from the SVG string by parsing attributes */
|
|
225
|
-
function measureSvgElements(svgString) {
|
|
226
|
-
const parser = new DOMParser();
|
|
227
|
-
const doc = parser.parseFromString(svgString, "image/svg+xml");
|
|
228
|
-
const pad = 10; // <g transform="translate(10, 10)">
|
|
229
|
-
|
|
230
|
-
const m = { participants: [], occurrences: [], messages: [], selfCalls: [], fragments: [], returns: [], diagram: {} };
|
|
231
|
-
|
|
232
|
-
// Diagram size
|
|
233
|
-
const svgEl = doc.querySelector("svg");
|
|
234
|
-
if (svgEl) {
|
|
235
|
-
m.diagram = {
|
|
236
|
-
width: +svgEl.getAttribute("width") || 0,
|
|
237
|
-
height: +svgEl.getAttribute("height") || 0,
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Participants
|
|
242
|
-
doc.querySelectorAll("g.participant, g.participant-starter").forEach((g) => {
|
|
243
|
-
const rect = g.querySelector("rect.participant-box");
|
|
244
|
-
const name = g.getAttribute("data-participant");
|
|
245
|
-
if (!rect || !name) return;
|
|
246
|
-
const x = +rect.getAttribute("x") + pad;
|
|
247
|
-
const y = +rect.getAttribute("y") + pad;
|
|
248
|
-
const w = +rect.getAttribute("width");
|
|
249
|
-
const h = +rect.getAttribute("height");
|
|
250
|
-
m.participants.push({
|
|
251
|
-
name,
|
|
252
|
-
centerX: Math.round((x + w / 2) * 10) / 10,
|
|
253
|
-
topY: y, width: w, height: h,
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
// Occurrences
|
|
258
|
-
doc.querySelectorAll("rect.occurrence").forEach((rect, i) => {
|
|
259
|
-
m.occurrences.push({
|
|
260
|
-
participant: rect.getAttribute("data-participant") || `occ-${i}`,
|
|
261
|
-
idx: i,
|
|
262
|
-
x: +rect.getAttribute("x") + pad,
|
|
263
|
-
y: +rect.getAttribute("y") + pad,
|
|
264
|
-
width: +rect.getAttribute("width"),
|
|
265
|
-
height: +rect.getAttribute("height"),
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
// Messages (non-self)
|
|
270
|
-
let msgIdx = 0;
|
|
271
|
-
doc.querySelectorAll("g.message").forEach((g) => {
|
|
272
|
-
if (g.classList.contains("self-call")) return;
|
|
273
|
-
const line = g.querySelector("line.message-line");
|
|
274
|
-
const text = g.querySelector("text.message-label");
|
|
275
|
-
if (!line) return;
|
|
276
|
-
const x1 = +line.getAttribute("x1") + pad;
|
|
277
|
-
const x2 = +line.getAttribute("x2") + pad;
|
|
278
|
-
const y = +line.getAttribute("y1") + pad;
|
|
279
|
-
m.messages.push({
|
|
280
|
-
idx: msgIdx++,
|
|
281
|
-
label: text ? text.textContent.trim() : "",
|
|
282
|
-
fromX: Math.min(x1, x2),
|
|
283
|
-
toX: Math.max(x1, x2),
|
|
284
|
-
y,
|
|
285
|
-
width: Math.abs(x2 - x1),
|
|
286
|
-
height: 0,
|
|
287
|
-
});
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
// Self-calls
|
|
291
|
-
let scIdx = 0;
|
|
292
|
-
doc.querySelectorAll("g.message.self-call").forEach((g) => {
|
|
293
|
-
const path = g.querySelector("path.message-line, path");
|
|
294
|
-
const text = g.querySelector("text.message-label");
|
|
295
|
-
if (!path) return;
|
|
296
|
-
const d = path.getAttribute("d") || "";
|
|
297
|
-
// Parse "M x1 y1 L ..." to get origin
|
|
298
|
-
const mMatch = d.match(/M\s*([\d.]+)\s+([\d.]+)/);
|
|
299
|
-
if (!mMatch) return;
|
|
300
|
-
const x = +mMatch[1] + pad;
|
|
301
|
-
const y = +mMatch[2] + pad;
|
|
302
|
-
// Parse width from the L commands
|
|
303
|
-
const lMatches = [...d.matchAll(/[LQ]\s*([\d.]+)\s+([\d.]+)/g)];
|
|
304
|
-
let maxX = x, maxY = y;
|
|
305
|
-
for (const lm of lMatches) {
|
|
306
|
-
maxX = Math.max(maxX, +lm[1] + pad);
|
|
307
|
-
maxY = Math.max(maxY, +lm[2] + pad);
|
|
308
|
-
}
|
|
309
|
-
m.selfCalls.push({
|
|
310
|
-
idx: scIdx++,
|
|
311
|
-
label: text ? text.textContent.trim() : "",
|
|
312
|
-
x, y,
|
|
313
|
-
width: Math.round((maxX - x) * 10) / 10,
|
|
314
|
-
height: Math.round((maxY - y) * 10) / 10,
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// Fragments
|
|
319
|
-
doc.querySelectorAll("g.fragment, g[class*='fragment-']").forEach((g, i) => {
|
|
320
|
-
const rect = g.querySelector("rect.fragment-border");
|
|
321
|
-
if (!rect) return;
|
|
322
|
-
const kind = ([...g.classList].find(c => c.startsWith("fragment-")) || "").replace("fragment-", "");
|
|
323
|
-
m.fragments.push({
|
|
324
|
-
idx: i, kind,
|
|
325
|
-
x: +rect.getAttribute("x") + pad,
|
|
326
|
-
y: +rect.getAttribute("y") + pad,
|
|
327
|
-
width: +rect.getAttribute("width"),
|
|
328
|
-
height: +rect.getAttribute("height"),
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
// Returns
|
|
333
|
-
doc.querySelectorAll("g.return").forEach((g, i) => {
|
|
334
|
-
const line = g.querySelector("line.return-line");
|
|
335
|
-
const text = g.querySelector("text.return-label");
|
|
336
|
-
if (!line) return;
|
|
337
|
-
const x1 = +line.getAttribute("x1") + pad;
|
|
338
|
-
const x2 = +line.getAttribute("x2") + pad;
|
|
339
|
-
const y = +line.getAttribute("y1") + pad;
|
|
340
|
-
m.returns.push({
|
|
341
|
-
idx: i,
|
|
342
|
-
label: text ? text.textContent.trim() : "",
|
|
343
|
-
fromX: Math.min(x1, x2),
|
|
344
|
-
toX: Math.max(x1, x2),
|
|
345
|
-
y,
|
|
346
|
-
});
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
return m;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/** Align measurements: normalize both to first non-starter participant as origin */
|
|
353
|
-
function alignMeasurements(htmlM, svgM) {
|
|
354
|
-
const htmlAnchor = htmlM.participants.find(p => p.name !== "_STARTER_");
|
|
355
|
-
const svgAnchor = svgM.participants.find(p => p.name !== "_STARTER_");
|
|
356
|
-
if (!htmlAnchor || !svgAnchor) return { dx: 0, dy: 0 };
|
|
357
|
-
return {
|
|
358
|
-
dx: htmlAnchor.centerX - svgAnchor.centerX,
|
|
359
|
-
dy: htmlAnchor.topY - svgAnchor.topY,
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/** Compare matched elements and return rows for the table */
|
|
364
|
-
function buildComparisonRows(htmlM, svgM) {
|
|
365
|
-
const { dx, dy } = alignMeasurements(htmlM, svgM);
|
|
366
|
-
const rows = [];
|
|
367
|
-
|
|
368
|
-
function addSection(title) {
|
|
369
|
-
rows.push({ section: title });
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function addRow(name, prop, htmlVal, svgVal) {
|
|
373
|
-
const svgAligned = svgVal + (prop.includes("X") || prop === "width" ? dx : prop.includes("Y") || prop === "y" || prop === "height" ? dy : 0);
|
|
374
|
-
// For width/height, don't apply offset
|
|
375
|
-
const delta = prop === "width" || prop === "height"
|
|
376
|
-
? Math.round((htmlVal - svgVal) * 10) / 10
|
|
377
|
-
: Math.round((htmlVal - svgAligned) * 10) / 10;
|
|
378
|
-
rows.push({
|
|
379
|
-
name, prop,
|
|
380
|
-
html: Math.round(htmlVal * 10) / 10,
|
|
381
|
-
svg: Math.round(svgVal * 10) / 10,
|
|
382
|
-
delta,
|
|
383
|
-
absDelta: Math.abs(delta),
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// Diagram size
|
|
388
|
-
addSection("Diagram");
|
|
389
|
-
addRow("Diagram", "width", htmlM.diagram.width, svgM.diagram.width);
|
|
390
|
-
addRow("Diagram", "height", htmlM.diagram.height, svgM.diagram.height);
|
|
391
|
-
|
|
392
|
-
// Participants
|
|
393
|
-
addSection("Participants");
|
|
394
|
-
for (const hp of htmlM.participants) {
|
|
395
|
-
const sp = svgM.participants.find(p => p.name === hp.name);
|
|
396
|
-
if (!sp) {
|
|
397
|
-
rows.push({ name: hp.name, prop: "*", html: "present", svg: "MISSING", delta: "N/A", absDelta: 999 });
|
|
398
|
-
continue;
|
|
399
|
-
}
|
|
400
|
-
addRow(hp.name, "centerX", hp.centerX, sp.centerX);
|
|
401
|
-
addRow(hp.name, "topY", hp.topY, sp.topY);
|
|
402
|
-
addRow(hp.name, "width", hp.width, sp.width);
|
|
403
|
-
addRow(hp.name, "height", hp.height, sp.height);
|
|
404
|
-
}
|
|
405
|
-
// SVG-only participants
|
|
406
|
-
for (const sp of svgM.participants) {
|
|
407
|
-
if (!htmlM.participants.find(p => p.name === sp.name)) {
|
|
408
|
-
rows.push({ name: sp.name, prop: "*", html: "MISSING", svg: "present", delta: "N/A", absDelta: 999 });
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// Occurrences
|
|
413
|
-
if (htmlM.occurrences.length > 0 || svgM.occurrences.length > 0) {
|
|
414
|
-
addSection("Occurrences");
|
|
415
|
-
const maxLen = Math.max(htmlM.occurrences.length, svgM.occurrences.length);
|
|
416
|
-
for (let i = 0; i < maxLen; i++) {
|
|
417
|
-
const ho = htmlM.occurrences[i];
|
|
418
|
-
const so = svgM.occurrences[i];
|
|
419
|
-
const label = ho?.participant || so?.participant || `#${i}`;
|
|
420
|
-
if (ho && so) {
|
|
421
|
-
addRow(`${label} #${i}`, "x", ho.x, so.x);
|
|
422
|
-
addRow(`${label} #${i}`, "y", ho.y, so.y);
|
|
423
|
-
addRow(`${label} #${i}`, "width", ho.width, so.width);
|
|
424
|
-
addRow(`${label} #${i}`, "height", ho.height, so.height);
|
|
425
|
-
} else if (ho) {
|
|
426
|
-
rows.push({ name: `${label} #${i}`, prop: "*", html: "present", svg: "MISSING", delta: "N/A", absDelta: 999 });
|
|
427
|
-
} else {
|
|
428
|
-
rows.push({ name: `${label} #${i}`, prop: "*", html: "MISSING", svg: "present", delta: "N/A", absDelta: 999 });
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Messages
|
|
434
|
-
if (htmlM.messages.length > 0 || svgM.messages.length > 0) {
|
|
435
|
-
addSection("Messages");
|
|
436
|
-
const maxLen = Math.max(htmlM.messages.length, svgM.messages.length);
|
|
437
|
-
for (let i = 0; i < maxLen; i++) {
|
|
438
|
-
const hm = htmlM.messages[i];
|
|
439
|
-
const sm = svgM.messages[i];
|
|
440
|
-
const label = hm?.label || sm?.label || `msg#${i}`;
|
|
441
|
-
if (hm && sm) {
|
|
442
|
-
addRow(label, "fromX", hm.fromX, sm.fromX);
|
|
443
|
-
addRow(label, "toX", hm.toX, sm.toX);
|
|
444
|
-
addRow(label, "y", hm.y, sm.y);
|
|
445
|
-
} else if (hm) {
|
|
446
|
-
rows.push({ name: label, prop: "*", html: "present", svg: "MISSING", delta: "N/A", absDelta: 999 });
|
|
447
|
-
} else {
|
|
448
|
-
rows.push({ name: label, prop: "*", html: "MISSING", svg: "present", delta: "N/A", absDelta: 999 });
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Self-calls
|
|
454
|
-
if (htmlM.selfCalls.length > 0 || svgM.selfCalls.length > 0) {
|
|
455
|
-
addSection("Self-Calls");
|
|
456
|
-
const maxLen = Math.max(htmlM.selfCalls.length, svgM.selfCalls.length);
|
|
457
|
-
for (let i = 0; i < maxLen; i++) {
|
|
458
|
-
const hs = htmlM.selfCalls[i];
|
|
459
|
-
const ss = svgM.selfCalls[i];
|
|
460
|
-
const label = hs?.label || ss?.label || `self#${i}`;
|
|
461
|
-
if (hs && ss) {
|
|
462
|
-
addRow(label, "x", hs.x, ss.x);
|
|
463
|
-
addRow(label, "y", hs.y, ss.y);
|
|
464
|
-
addRow(label, "width", hs.width, ss.width);
|
|
465
|
-
addRow(label, "height", hs.height, ss.height);
|
|
466
|
-
} else if (hs) {
|
|
467
|
-
rows.push({ name: label, prop: "*", html: "present", svg: "MISSING", delta: "N/A", absDelta: 999 });
|
|
468
|
-
} else {
|
|
469
|
-
rows.push({ name: label, prop: "*", html: "MISSING", svg: "present", delta: "N/A", absDelta: 999 });
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Fragments
|
|
475
|
-
if (htmlM.fragments.length > 0 || svgM.fragments.length > 0) {
|
|
476
|
-
addSection("Fragments");
|
|
477
|
-
const maxLen = Math.max(htmlM.fragments.length, svgM.fragments.length);
|
|
478
|
-
for (let i = 0; i < maxLen; i++) {
|
|
479
|
-
const hf = htmlM.fragments[i];
|
|
480
|
-
const sf = svgM.fragments[i];
|
|
481
|
-
const label = `${hf?.kind || sf?.kind || "?"} #${i}`;
|
|
482
|
-
if (hf && sf) {
|
|
483
|
-
addRow(label, "x", hf.x, sf.x);
|
|
484
|
-
addRow(label, "y", hf.y, sf.y);
|
|
485
|
-
addRow(label, "width", hf.width, sf.width);
|
|
486
|
-
addRow(label, "height", hf.height, sf.height);
|
|
487
|
-
} else if (hf) {
|
|
488
|
-
rows.push({ name: label, prop: "*", html: "present", svg: "MISSING", delta: "N/A", absDelta: 999 });
|
|
489
|
-
} else {
|
|
490
|
-
rows.push({ name: label, prop: "*", html: "MISSING", svg: "present", delta: "N/A", absDelta: 999 });
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// Returns
|
|
496
|
-
if (htmlM.returns.length > 0 || svgM.returns.length > 0) {
|
|
497
|
-
addSection("Returns");
|
|
498
|
-
const maxLen = Math.max(htmlM.returns.length, svgM.returns.length);
|
|
499
|
-
for (let i = 0; i < maxLen; i++) {
|
|
500
|
-
const hr = htmlM.returns[i];
|
|
501
|
-
const sr = svgM.returns[i];
|
|
502
|
-
const label = hr?.label || sr?.label || `ret#${i}`;
|
|
503
|
-
if (hr && sr) {
|
|
504
|
-
addRow(label, "fromX", hr.fromX, sr.fromX);
|
|
505
|
-
addRow(label, "toX", hr.toX, sr.toX);
|
|
506
|
-
addRow(label, "y", hr.y, sr.y);
|
|
507
|
-
} else if (hr) {
|
|
508
|
-
rows.push({ name: label, prop: "*", html: "present", svg: "MISSING", delta: "N/A", absDelta: 999 });
|
|
509
|
-
} else {
|
|
510
|
-
rows.push({ name: label, prop: "*", html: "MISSING", svg: "present", delta: "N/A", absDelta: 999 });
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
return rows;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// ——— Report rendering ———
|
|
519
|
-
|
|
520
|
-
function renderTable(rows) {
|
|
521
|
-
let html = `<table class="measure-table">
|
|
522
|
-
<thead><tr>
|
|
523
|
-
<th>Element</th><th>Property</th><th>HTML</th><th>SVG</th><th style="text-align:right">Delta</th>
|
|
524
|
-
</tr></thead><tbody>`;
|
|
525
|
-
|
|
526
|
-
for (const r of rows) {
|
|
527
|
-
if (r.section) {
|
|
528
|
-
html += `<tr class="section-row"><td colspan="5">${r.section}</td></tr>`;
|
|
529
|
-
continue;
|
|
530
|
-
}
|
|
531
|
-
const cls = typeof r.absDelta === "number"
|
|
532
|
-
? r.absDelta <= 3 ? "ok" : r.absDelta <= 10 ? "warn" : "bad"
|
|
533
|
-
: "bad";
|
|
534
|
-
const deltaStr = typeof r.delta === "number" ? (r.delta > 0 ? `+${r.delta}` : `${r.delta}`) : r.delta;
|
|
535
|
-
html += `<tr>
|
|
536
|
-
<td class="el-name">${r.name}</td>
|
|
537
|
-
<td>${r.prop}</td>
|
|
538
|
-
<td>${r.html}</td>
|
|
539
|
-
<td>${r.svg}</td>
|
|
540
|
-
<td class="delta ${cls}" style="text-align:right">${deltaStr}</td>
|
|
541
|
-
</tr>`;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
html += `</tbody></table>`;
|
|
545
|
-
return html;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// ——— Main processing ———
|
|
549
|
-
|
|
550
|
-
async function processCase(caseName, code) {
|
|
551
|
-
// 1. SVG
|
|
552
|
-
const svgResult = renderToSvg(code);
|
|
553
|
-
const svgM = measureSvgElements(svgResult.svg);
|
|
554
|
-
|
|
555
|
-
// 2. HTML — render in workspace
|
|
556
|
-
const workspace = document.getElementById("workspace");
|
|
557
|
-
workspace.style.opacity = "1"; // must be visible for getBoundingClientRect
|
|
558
|
-
workspace.innerHTML = '<div id="ws-diagram"><pre class="zenuml" style="margin:0"></pre></div>';
|
|
559
|
-
const pre = workspace.querySelector("pre.zenuml");
|
|
560
|
-
pre.textContent = code;
|
|
561
|
-
const zu = new ZenUml(pre);
|
|
562
|
-
await zu.render(code, { theme: "theme-default" });
|
|
563
|
-
|
|
564
|
-
// Wait for layout
|
|
565
|
-
await new Promise(r => setTimeout(r, 100));
|
|
566
|
-
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
|
|
567
|
-
|
|
568
|
-
// Hide footer (SVG has no footer equivalent)
|
|
569
|
-
const footer = workspace.querySelector(".footer");
|
|
570
|
-
if (footer) footer.style.display = "none";
|
|
571
|
-
|
|
572
|
-
// Find the sequence diagram container
|
|
573
|
-
const seqDiagram = workspace.querySelector(".sequence-diagram") || workspace.querySelector(".seq-diagram") || workspace.querySelector("#ws-diagram");
|
|
574
|
-
|
|
575
|
-
// 3. Measure HTML
|
|
576
|
-
const htmlM = measureHtmlElements(seqDiagram);
|
|
577
|
-
|
|
578
|
-
// 4. Capture HTML screenshot
|
|
579
|
-
let htmlImageUrl = null;
|
|
580
|
-
try {
|
|
581
|
-
htmlImageUrl = await toPng(seqDiagram, { pixelRatio: 1 });
|
|
582
|
-
} catch (e) {
|
|
583
|
-
console.warn(`toPng failed for ${caseName}:`, e);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// 5. Cleanup
|
|
587
|
-
workspace.innerHTML = "";
|
|
588
|
-
workspace.style.opacity = "0";
|
|
589
|
-
|
|
590
|
-
return { svgString: svgResult.svg, svgM, htmlM, htmlImageUrl };
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
async function generateReport() {
|
|
594
|
-
const btn = document.getElementById("run-btn");
|
|
595
|
-
const progress = document.getElementById("progress");
|
|
596
|
-
const report = document.getElementById("report");
|
|
597
|
-
const summaryDiv = document.getElementById("summary");
|
|
598
|
-
btn.disabled = true;
|
|
599
|
-
report.innerHTML = "";
|
|
600
|
-
summaryDiv.innerHTML = "";
|
|
601
|
-
|
|
602
|
-
const caseNames = Object.keys(CASES);
|
|
603
|
-
const summaryRows = [];
|
|
604
|
-
|
|
605
|
-
for (let i = 0; i < caseNames.length; i++) {
|
|
606
|
-
const name = caseNames[i];
|
|
607
|
-
progress.textContent = `Processing ${i + 1}/${caseNames.length}: ${name}...`;
|
|
608
|
-
|
|
609
|
-
try {
|
|
610
|
-
const result = await processCase(name, CASES[name]);
|
|
611
|
-
const rows = buildComparisonRows(result.htmlM, result.svgM);
|
|
612
|
-
|
|
613
|
-
// Stats for this case
|
|
614
|
-
const dataRows = rows.filter(r => !r.section && typeof r.absDelta === "number");
|
|
615
|
-
const avgDelta = dataRows.length > 0
|
|
616
|
-
? Math.round((dataRows.reduce((s, r) => s + r.absDelta, 0) / dataRows.length) * 10) / 10
|
|
617
|
-
: 0;
|
|
618
|
-
const maxDelta = dataRows.length > 0
|
|
619
|
-
? Math.max(...dataRows.map(r => r.absDelta))
|
|
620
|
-
: 0;
|
|
621
|
-
const badCount = dataRows.filter(r => r.absDelta > 10).length;
|
|
622
|
-
const warnCount = dataRows.filter(r => r.absDelta > 3 && r.absDelta <= 10).length;
|
|
623
|
-
|
|
624
|
-
summaryRows.push({ name, avgDelta, maxDelta, badCount, warnCount, totalProps: dataRows.length });
|
|
625
|
-
|
|
626
|
-
// Build case card
|
|
627
|
-
const card = document.createElement("div");
|
|
628
|
-
card.className = "case-card";
|
|
629
|
-
card.id = `case-${name}`;
|
|
630
|
-
|
|
631
|
-
const badgeClass = maxDelta <= 3 ? "good" : maxDelta <= 10 ? "warn" : "bad";
|
|
632
|
-
const badgeText = `avg ${avgDelta}px, max ${maxDelta}px`;
|
|
633
|
-
|
|
634
|
-
card.innerHTML = `
|
|
635
|
-
<div class="case-header" onclick="this.nextElementSibling.classList.toggle('open')">
|
|
636
|
-
<h3>${name}</h3>
|
|
637
|
-
<span class="badge ${badgeClass}">${badgeText}</span>
|
|
638
|
-
<span style="margin-left:auto;color:#94a3b8;font-size:11px">${dataRows.length} properties measured</span>
|
|
639
|
-
</div>
|
|
640
|
-
<div class="case-body">
|
|
641
|
-
<div class="diagrams">
|
|
642
|
-
<div class="diagram-panel">
|
|
643
|
-
<h4>HTML Renderer</h4>
|
|
644
|
-
<div class="content">
|
|
645
|
-
${result.htmlImageUrl ? `<img src="${result.htmlImageUrl}" />` : "<em>Capture failed</em>"}
|
|
646
|
-
</div>
|
|
647
|
-
</div>
|
|
648
|
-
<div class="diagram-panel">
|
|
649
|
-
<h4>SVG Renderer</h4>
|
|
650
|
-
<div class="content">${result.svgString}</div>
|
|
651
|
-
</div>
|
|
652
|
-
</div>
|
|
653
|
-
<div style="padding:8px;max-height:500px;overflow:auto">
|
|
654
|
-
${renderTable(rows)}
|
|
655
|
-
</div>
|
|
656
|
-
</div>
|
|
657
|
-
`;
|
|
658
|
-
report.appendChild(card);
|
|
659
|
-
} catch (e) {
|
|
660
|
-
console.error(`Error processing ${name}:`, e);
|
|
661
|
-
const card = document.createElement("div");
|
|
662
|
-
card.className = "case-card";
|
|
663
|
-
card.innerHTML = `<div class="case-header"><h3>${name}</h3><span class="badge bad">ERROR: ${e.message}</span></div>`;
|
|
664
|
-
report.appendChild(card);
|
|
665
|
-
summaryRows.push({ name, avgDelta: -1, maxDelta: -1, badCount: -1, warnCount: -1, totalProps: 0 });
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// Summary table
|
|
670
|
-
const overallAvg = summaryRows.filter(r => r.avgDelta >= 0).length > 0
|
|
671
|
-
? Math.round(summaryRows.filter(r => r.avgDelta >= 0).reduce((s, r) => s + r.avgDelta, 0) / summaryRows.filter(r => r.avgDelta >= 0).length * 10) / 10
|
|
672
|
-
: 0;
|
|
673
|
-
const overallMax = Math.max(...summaryRows.filter(r => r.maxDelta >= 0).map(r => r.maxDelta), 0);
|
|
674
|
-
|
|
675
|
-
summaryDiv.innerHTML = `<div class="summary">
|
|
676
|
-
<div style="margin-bottom:8px"><strong>Overall:</strong> avg delta ${overallAvg}px, max delta ${overallMax}px across ${caseNames.length} cases</div>
|
|
677
|
-
<table>
|
|
678
|
-
<thead><tr>
|
|
679
|
-
<th>Case</th><th>Properties</th><th style="text-align:right">Avg Δ</th><th style="text-align:right">Max Δ</th><th style="text-align:right">Bad (>10px)</th><th style="text-align:right">Warn (>3px)</th>
|
|
680
|
-
</tr></thead>
|
|
681
|
-
<tbody>
|
|
682
|
-
${summaryRows.sort((a, b) => b.maxDelta - a.maxDelta).map(r => {
|
|
683
|
-
const cls = r.maxDelta <= 3 ? "ok" : r.maxDelta <= 10 ? "warn" : "bad";
|
|
684
|
-
return `<tr>
|
|
685
|
-
<td><a class="case-link" onclick="document.getElementById('case-${r.name}').querySelector('.case-body').classList.add('open'); document.getElementById('case-${r.name}').scrollIntoView({behavior:'smooth'})">${r.name}</a></td>
|
|
686
|
-
<td>${r.totalProps}</td>
|
|
687
|
-
<td class="delta ${cls}" style="text-align:right">${r.avgDelta >= 0 ? r.avgDelta : "ERR"}</td>
|
|
688
|
-
<td class="delta ${cls}" style="text-align:right">${r.maxDelta >= 0 ? r.maxDelta : "ERR"}</td>
|
|
689
|
-
<td style="text-align:right;color:${r.badCount > 0 ? '#dc2626' : '#16a34a'}">${r.badCount >= 0 ? r.badCount : "-"}</td>
|
|
690
|
-
<td style="text-align:right;color:${r.warnCount > 0 ? '#ca8a04' : '#16a34a'}">${r.warnCount >= 0 ? r.warnCount : "-"}</td>
|
|
691
|
-
</tr>`;
|
|
692
|
-
}).join("")}
|
|
693
|
-
</tbody>
|
|
694
|
-
</table>
|
|
695
|
-
</div>`;
|
|
696
|
-
|
|
697
|
-
progress.textContent = `Done! ${caseNames.length} cases processed.`;
|
|
698
|
-
btn.disabled = false;
|
|
699
|
-
btn.textContent = "Regenerate";
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
document.getElementById("run-btn").addEventListener("click", generateReport);
|
|
703
|
-
</script>
|
|
704
|
-
</body>
|
|
705
|
-
</html>
|