ui-design-workbench-cli 0.5.0__py3-none-any.whl
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.
- android_resource_resolver.py +370 -0
- android_xml_support.py +230 -0
- apple_resource_resolver.py +141 -0
- coverage_report.py +134 -0
- fidelity_adapter_api.py +89 -0
- fidelity_adapters.py +508 -0
- fidelity_core.py +241 -0
- fidelity_platform_adapters.py +902 -0
- ir_contracts.py +76 -0
- native_render_android.py +160 -0
- native_render_apple.py +165 -0
- native_render_contracts.py +148 -0
- native_render_registry.py +86 -0
- quality_common.py +217 -0
- render_preview.py +1797 -0
- scan_ui.py +1485 -0
- scoped_context.py +458 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/SKILL.md +52 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/adapter-api.md +20 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/agent-integrations.md +39 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/cache-protocol.md +61 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/design-modes.md +109 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/fidelity.md +53 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/ir-schema.md +412 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/native-rendering.md +40 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/platform-profiles.json +84 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/platform-standards.md +131 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/platforms.md +40 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/quality-automation.md +77 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/review-workflow.md +135 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/ui-guidance-mode.md +22 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/ui-policy-schema.md +83 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/ui-reviewer.md +175 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/validation.md +34 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/workbench-ui.md +108 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/native-render-state.schema.json +71 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/ui-agent-job.schema.json +36 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/ui-graph.schema.json +46 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/ui-ir.patch.schema.json +36 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/ui-ir.schema.json +296 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/uidw-config.schema.json +68 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/android_resource_resolver.py +370 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/android_xml_support.py +230 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/apple_resource_resolver.py +141 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/coverage_report.py +134 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/fidelity_adapter_api.py +89 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/fidelity_adapters.py +508 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/fidelity_core.py +241 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/fidelity_platform_adapters.py +902 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/generate_interaction_matrix.py +166 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/ir_contracts.py +76 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/merge_review_state.py +267 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/native_render_android.py +160 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/native_render_apple.py +165 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/native_render_contracts.py +148 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/native_render_registry.py +86 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/quality_common.py +217 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/render_preview.py +1797 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/scan_ui.py +1485 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/scoped_context.py +458 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/smoke_preview.js +978 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/uidw.py +2821 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/uidw_mcp.py +282 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/validate_platform_profiles.py +93 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/visual_regression.py +141 -0
- ui_design_workbench_cli-0.5.0.dist-info/METADATA +133 -0
- ui_design_workbench_cli-0.5.0.dist-info/RECORD +73 -0
- ui_design_workbench_cli-0.5.0.dist-info/WHEEL +5 -0
- ui_design_workbench_cli-0.5.0.dist-info/entry_points.txt +3 -0
- ui_design_workbench_cli-0.5.0.dist-info/top_level.txt +20 -0
- uidw.py +2821 -0
- uidw_mcp.py +282 -0
- validate_platform_profiles.py +93 -0
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* Run the standalone preview Diagnostics Runner in headless Chromium without app servers. */
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { pathToFileURL } = require('url');
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
|
|
10
|
+
function parseArgs(argv) {
|
|
11
|
+
const result = { input: null, output: null, screenshot: null, geometryOutput: null, captureView: null, captureScreen: null, captureLeftPanel: null, captureRightPanel: null, captureInspectorTab: null, captureReviewSection: null, viewportWidth: 1440, viewportHeight: 960, mode: 'projection', failOnFindings: false, captureOnly: false, timeoutMs: 60000 };
|
|
12
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
13
|
+
const value = argv[index];
|
|
14
|
+
if (value === '--output') result.output = argv[++index];
|
|
15
|
+
else if (value === '--screenshot') result.screenshot = argv[++index];
|
|
16
|
+
else if (value === '--geometry-output') result.geometryOutput = argv[++index];
|
|
17
|
+
else if (value === '--capture-view') result.captureView = argv[++index];
|
|
18
|
+
else if (value === '--capture-screen') result.captureScreen = argv[++index];
|
|
19
|
+
else if (value === '--capture-left-panel') result.captureLeftPanel = argv[++index];
|
|
20
|
+
else if (value === '--capture-right-panel') result.captureRightPanel = argv[++index];
|
|
21
|
+
else if (value === '--capture-inspector-tab') result.captureInspectorTab = argv[++index];
|
|
22
|
+
else if (value === '--capture-review-section') result.captureReviewSection = argv[++index];
|
|
23
|
+
else if (value === '--viewport-width') result.viewportWidth = Number(argv[++index]);
|
|
24
|
+
else if (value === '--viewport-height') result.viewportHeight = Number(argv[++index]);
|
|
25
|
+
else if (value === '--mode') result.mode = argv[++index];
|
|
26
|
+
else if (value === '--fail-on-findings') result.failOnFindings = true;
|
|
27
|
+
else if (value === '--capture-only') result.captureOnly = true;
|
|
28
|
+
else if (value === '--timeout-ms') result.timeoutMs = Number(argv[++index]);
|
|
29
|
+
else if (!result.input) result.input = value;
|
|
30
|
+
else throw new Error(`Unexpected argument: ${value}`);
|
|
31
|
+
}
|
|
32
|
+
if (!result.input) throw new Error('Usage: smoke_preview.js <ui-preview.html> [--mode projection|review] [--output diagnostics.json] [--screenshot preview.png] [--geometry-output geometry.json] [--capture-only] [--capture-view overview|prototype|single|states|compare] [--capture-screen ID] [--capture-left-panel open|closed] [--capture-right-panel open|closed] [--capture-inspector-tab inspect|review|comments] [--capture-review-section summary|problems|changes] [--viewport-width 1440] [--viewport-height 960] [--fail-on-findings]');
|
|
33
|
+
if (!['projection', 'review'].includes(result.mode)) throw new Error('--mode must be projection or review');
|
|
34
|
+
if (result.captureView && !['overview', 'prototype', 'single', 'states', 'compare'].includes(result.captureView)) throw new Error('--capture-view must be overview, prototype, single, states, or compare');
|
|
35
|
+
if (result.captureLeftPanel && !['open', 'closed'].includes(result.captureLeftPanel)) throw new Error('--capture-left-panel must be open or closed');
|
|
36
|
+
if (result.captureRightPanel && !['open', 'closed'].includes(result.captureRightPanel)) throw new Error('--capture-right-panel must be open or closed');
|
|
37
|
+
if (result.captureInspectorTab && !['inspect', 'review', 'comments'].includes(result.captureInspectorTab)) throw new Error('--capture-inspector-tab must be inspect, review, or comments');
|
|
38
|
+
if (result.captureReviewSection && !['summary', 'problems', 'changes'].includes(result.captureReviewSection)) throw new Error('--capture-review-section must be summary, problems, or changes');
|
|
39
|
+
if (!Number.isFinite(result.viewportWidth) || result.viewportWidth < 320 || !Number.isFinite(result.viewportHeight) || result.viewportHeight < 320) throw new Error('Viewport width and height must be at least 320');
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function chromeCandidates() {
|
|
44
|
+
const values = [process.env.CHROME_PATH];
|
|
45
|
+
if (process.platform === 'win32') {
|
|
46
|
+
values.push(
|
|
47
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
48
|
+
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
49
|
+
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
50
|
+
);
|
|
51
|
+
} else if (process.platform === 'darwin') {
|
|
52
|
+
values.push('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge');
|
|
53
|
+
} else {
|
|
54
|
+
values.push('/usr/bin/google-chrome', '/usr/bin/chromium', '/usr/bin/chromium-browser', '/usr/bin/microsoft-edge');
|
|
55
|
+
}
|
|
56
|
+
return values.filter(Boolean);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function findChrome() {
|
|
60
|
+
const executable = chromeCandidates().find(candidate => fs.existsSync(candidate));
|
|
61
|
+
if (!executable) throw new Error('Chromium browser not found. Set CHROME_PATH to Chrome, Edge, or Chromium.');
|
|
62
|
+
return executable;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const delay = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
66
|
+
|
|
67
|
+
async function waitForFile(file, timeoutMs) {
|
|
68
|
+
const deadline = Date.now() + timeoutMs;
|
|
69
|
+
while (Date.now() < deadline) {
|
|
70
|
+
if (fs.existsSync(file) && fs.statSync(file).size > 0) return;
|
|
71
|
+
await delay(100);
|
|
72
|
+
}
|
|
73
|
+
throw new Error(`Timed out waiting for ${file}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function connectCdp(webSocketUrl) {
|
|
77
|
+
const socket = new WebSocket(webSocketUrl);
|
|
78
|
+
await new Promise((resolve, reject) => {
|
|
79
|
+
socket.addEventListener('open', resolve, { once: true });
|
|
80
|
+
socket.addEventListener('error', () => reject(new Error('Cannot connect to Chromium DevTools')), { once: true });
|
|
81
|
+
});
|
|
82
|
+
let nextId = 1;
|
|
83
|
+
const pending = new Map();
|
|
84
|
+
socket.addEventListener('message', event => {
|
|
85
|
+
const payload = JSON.parse(String(event.data));
|
|
86
|
+
if (!payload.id || !pending.has(payload.id)) return;
|
|
87
|
+
const { resolve, reject } = pending.get(payload.id);
|
|
88
|
+
pending.delete(payload.id);
|
|
89
|
+
if (payload.error) reject(new Error(payload.error.message));
|
|
90
|
+
else resolve(payload.result);
|
|
91
|
+
});
|
|
92
|
+
const call = (method, params = {}) => new Promise((resolve, reject) => {
|
|
93
|
+
const id = nextId++;
|
|
94
|
+
pending.set(id, { resolve, reject });
|
|
95
|
+
socket.send(JSON.stringify({ id, method, params }));
|
|
96
|
+
});
|
|
97
|
+
return { socket, call };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function main() {
|
|
101
|
+
const args = parseArgs(process.argv.slice(2));
|
|
102
|
+
const input = path.resolve(args.input);
|
|
103
|
+
if (!fs.existsSync(input)) throw new Error(`Preview not found: ${input}`);
|
|
104
|
+
const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'ui-design-workbench-smoke-'));
|
|
105
|
+
const activePort = path.join(profile, 'DevToolsActivePort');
|
|
106
|
+
const url = pathToFileURL(input);
|
|
107
|
+
url.searchParams.set('diagnostics', 'run');
|
|
108
|
+
url.searchParams.set('diagnostic-mode', args.mode);
|
|
109
|
+
url.searchParams.set('lang', 'ru');
|
|
110
|
+
const browser = spawn(findChrome(), [
|
|
111
|
+
'--headless=new',
|
|
112
|
+
'--disable-gpu',
|
|
113
|
+
'--no-first-run',
|
|
114
|
+
'--disable-background-networking',
|
|
115
|
+
'--remote-debugging-port=0',
|
|
116
|
+
`--user-data-dir=${profile}`,
|
|
117
|
+
url.href,
|
|
118
|
+
], { stdio: 'ignore', windowsHide: true });
|
|
119
|
+
let socket;
|
|
120
|
+
let cdp;
|
|
121
|
+
try {
|
|
122
|
+
await waitForFile(activePort, Math.min(args.timeoutMs, 15000));
|
|
123
|
+
const [port] = fs.readFileSync(activePort, 'utf8').trim().split(/\r?\n/);
|
|
124
|
+
let targets = [];
|
|
125
|
+
const targetDeadline = Date.now() + 15000;
|
|
126
|
+
while (Date.now() < targetDeadline) {
|
|
127
|
+
targets = await fetch(`http://127.0.0.1:${port}/json/list`).then(response => response.json());
|
|
128
|
+
if (targets.some(target => target.type === 'page' && target.url.startsWith('file:'))) break;
|
|
129
|
+
await delay(100);
|
|
130
|
+
}
|
|
131
|
+
const target = targets.find(item => item.type === 'page' && item.url.startsWith('file:'));
|
|
132
|
+
if (!target) throw new Error('Preview page was not created by Chromium');
|
|
133
|
+
cdp = await connectCdp(target.webSocketDebuggerUrl);
|
|
134
|
+
socket = cdp.socket;
|
|
135
|
+
await cdp.call('Runtime.enable');
|
|
136
|
+
await cdp.call('Emulation.setDeviceMetricsOverride', { width: args.viewportWidth, height: args.viewportHeight, deviceScaleFactor: 1, mobile: false });
|
|
137
|
+
const deadline = Date.now() + args.timeoutMs;
|
|
138
|
+
let status = 'idle';
|
|
139
|
+
while (Date.now() < deadline) {
|
|
140
|
+
const response = await cdp.call('Runtime.evaluate', { expression: "document.documentElement.dataset.diagnosticsStatus || 'idle'", returnByValue: true });
|
|
141
|
+
status = response.result?.value || 'idle';
|
|
142
|
+
if (status === 'complete') break;
|
|
143
|
+
await delay(200);
|
|
144
|
+
}
|
|
145
|
+
if (status !== 'complete') throw new Error(`Diagnostics did not finish within ${args.timeoutMs}ms (status=${status})`);
|
|
146
|
+
const response = await cdp.call('Runtime.evaluate', { expression: "document.getElementById('runtime-diagnostics-report').textContent", returnByValue: true });
|
|
147
|
+
const report = JSON.parse(response.result?.value || 'null');
|
|
148
|
+
if (!report || report.status !== 'complete') throw new Error('Diagnostics report is missing or incomplete');
|
|
149
|
+
const initialVisualStateResponse = await cdp.call('Runtime.evaluate', {
|
|
150
|
+
expression: `({
|
|
151
|
+
view: state.view,
|
|
152
|
+
screen: state.screen,
|
|
153
|
+
activeVersion: state.activeVersion,
|
|
154
|
+
compareBaseVersion: state.compareBaseVersion,
|
|
155
|
+
compareTargetVersion: state.compareTargetVersion,
|
|
156
|
+
versionIds: versions.map(version => version.id),
|
|
157
|
+
versionDecision: state.versionDecision,
|
|
158
|
+
findingDecisions: JSON.parse(JSON.stringify(state.findingDecisions)),
|
|
159
|
+
findingFilter: state.findingFilter,
|
|
160
|
+
findingSource: state.findingSource,
|
|
161
|
+
findingScreen: state.findingScreen,
|
|
162
|
+
findingFocus: JSON.parse(JSON.stringify(state.findingFocus)),
|
|
163
|
+
runtimeFindings: JSON.parse(JSON.stringify(state.runtimeFindings)),
|
|
164
|
+
reviewSection: state.reviewSection,
|
|
165
|
+
reviewScope: state.reviewScope,
|
|
166
|
+
reviewRuns: JSON.parse(JSON.stringify(state.reviewRuns)),
|
|
167
|
+
importedReview: JSON.parse(JSON.stringify(state.importedReview)),
|
|
168
|
+
sidebarOpen: state.sidebarOpen,
|
|
169
|
+
inspectorOpen: state.inspectorOpen,
|
|
170
|
+
inspectorTab: state.inspectorTab,
|
|
171
|
+
zoomMode: state.zoomMode,
|
|
172
|
+
zoom: state.zoom,
|
|
173
|
+
selectedNodeId: state.selectedNodeId,
|
|
174
|
+
selectedScreenId: state.selectedScreenId,
|
|
175
|
+
focusedFindingId: state.focusedFindingId,
|
|
176
|
+
openFindingId: state.openFindingId,
|
|
177
|
+
showFindings: state.showFindings,
|
|
178
|
+
showBefore: state.showBefore,
|
|
179
|
+
showAfter: state.showAfter,
|
|
180
|
+
layerReturnView: state.layerReturnView,
|
|
181
|
+
diagnosticTargetIds: JSON.parse(JSON.stringify(state.diagnosticTargetIds)),
|
|
182
|
+
stageScroll: JSON.parse(JSON.stringify(state.stageScroll)),
|
|
183
|
+
screenScrolls: JSON.parse(JSON.stringify(state.screenScrolls)),
|
|
184
|
+
})`,
|
|
185
|
+
returnByValue: true,
|
|
186
|
+
});
|
|
187
|
+
const initialVisualState = initialVisualStateResponse.result?.value || {};
|
|
188
|
+
const workflowResponse = args.captureOnly ? { result: { value: {} } } : await cdp.call('Runtime.evaluate', {
|
|
189
|
+
expression: `(async () => {
|
|
190
|
+
const totalBefore = Number(document.querySelector('.finding-total')?.textContent || 0);
|
|
191
|
+
const auditLink = document.querySelector('[data-audit-findings]');
|
|
192
|
+
if (auditLink) auditLink.click();
|
|
193
|
+
const linkedVisible = !document.querySelector('.finding-context')?.hidden;
|
|
194
|
+
document.querySelector('.finding-context button')?.click();
|
|
195
|
+
const diagnosticAction = [...document.querySelectorAll('[data-accept-diagnostic]')]
|
|
196
|
+
.find(button => button.closest('.diagnostic-card')?.dataset.result !== 'pass');
|
|
197
|
+
diagnosticAction?.click();
|
|
198
|
+
const diagnosticFindingCreated = Boolean(diagnosticAction);
|
|
199
|
+
const staleRuntimeId = 'runtime-stale-smoke';
|
|
200
|
+
state.runtimeFindings.push({
|
|
201
|
+
id: staleRuntimeId, title: 'Stale runtime finding', category: 'runtime-diagnostic', severity: 'medium', confidence: 'high',
|
|
202
|
+
screenId: screens[0].id, nodeId: null, observation: 'Old engine result.', impact: 'Noise.', recommendation: 'Recheck.',
|
|
203
|
+
evidence: [{ type: 'heuristic', ref: 'runtime-diagnostics.old', note: 'Synthetic stale result.' }], effort: 'small', runtimeDiagnosticId: 'old-check'
|
|
204
|
+
});
|
|
205
|
+
state.findingDecisions[staleRuntimeId] = 'accepted';
|
|
206
|
+
let diagnosticDownloads = 0;
|
|
207
|
+
const originalAnchorClick = HTMLAnchorElement.prototype.click;
|
|
208
|
+
HTMLAnchorElement.prototype.click = function smokeDownloadGuard() {
|
|
209
|
+
if (this.download) diagnosticDownloads += 1;
|
|
210
|
+
return originalAnchorClick.call(this);
|
|
211
|
+
};
|
|
212
|
+
try {
|
|
213
|
+
await runAutomatedReview();
|
|
214
|
+
} finally {
|
|
215
|
+
HTMLAnchorElement.prototype.click = originalAnchorClick;
|
|
216
|
+
}
|
|
217
|
+
const diagnosticsDoNotDownload = diagnosticDownloads === 0;
|
|
218
|
+
const staleRuntimePruned = !state.runtimeFindings.some(item => item.id === staleRuntimeId)
|
|
219
|
+
&& !Object.prototype.hasOwnProperty.call(state.findingDecisions, staleRuntimeId);
|
|
220
|
+
const decisionsBeforeBulk = JSON.parse(JSON.stringify(state.findingDecisions));
|
|
221
|
+
if (document.querySelector('.review-next-action')?.dataset.action === 'problems') document.querySelector('.review-next-action')?.click();
|
|
222
|
+
if (document.querySelector('.review-next-action')?.dataset.action === 'select-all') document.querySelector('.review-next-action')?.click();
|
|
223
|
+
const bulkSelected = selectedFindings().length;
|
|
224
|
+
const bulkSelectionPathWorks = bulkSelected === findings.filter(item => !findingResolved(item)).length
|
|
225
|
+
&& state.reviewSection === 'problems'
|
|
226
|
+
&& document.querySelector('.review-next-action')?.dataset.action === 'changes'
|
|
227
|
+
&& document.querySelector('.review-next-action')?.textContent?.includes(String(bulkSelected));
|
|
228
|
+
state.findingDecisions = decisionsBeforeBulk;
|
|
229
|
+
renderFindings();
|
|
230
|
+
const isProductFinding = item => item.result !== 'pass' && item.metrics?.scope !== 'workbench';
|
|
231
|
+
const actionableChecks = (state.diagnostics?.checks || []).filter(isProductFinding).length;
|
|
232
|
+
const actionableGroups = groupDiagnosticChecks((state.diagnostics?.checks || []).filter(isProductFinding)).length;
|
|
233
|
+
const totalAfter = Number(document.querySelector('.finding-total')?.textContent || 0);
|
|
234
|
+
const autoReviewFindings = state.runtimeFindings.length;
|
|
235
|
+
const selected = selectedFindings().length;
|
|
236
|
+
const selectionNextStep = selected === 0
|
|
237
|
+
|| (state.reviewSection === 'problems'
|
|
238
|
+
&& document.querySelector('.review-next-action')?.dataset.action === 'changes'
|
|
239
|
+
&& document.querySelector('.review-next-action')?.textContent?.includes(String(selected)));
|
|
240
|
+
const request = selected && typeof fixRequestPayload === 'function' ? fixRequestPayload() : null;
|
|
241
|
+
const expertRequest = typeof expertReviewRequestPayload === 'function' ? expertReviewRequestPayload() : null;
|
|
242
|
+
const expertHandoff = window.__uiPreviewDiagnostics?.agentHandoff?.('expert');
|
|
243
|
+
const proposalHandoff = window.__uiPreviewDiagnostics?.agentHandoff?.('proposal');
|
|
244
|
+
state.agentHandoff = {
|
|
245
|
+
kind: 'proposal', status: 'prepared', createdAt: new Date().toISOString(),
|
|
246
|
+
acceptedFindingIds: proposalHandoff?.context?.acceptedFindingIds || [],
|
|
247
|
+
screenIds: proposalHandoff?.context?.screenIds || [], artifactDir: proposalHandoff?.path || ''
|
|
248
|
+
};
|
|
249
|
+
renderAgentHandoff();
|
|
250
|
+
const handoffPanelWorks = document.querySelector('.agent-handoff-panel')?.hidden === false
|
|
251
|
+
&& Boolean(document.querySelector('.copy-agent-task'))
|
|
252
|
+
&& Boolean(document.querySelector('.refresh-preview'))
|
|
253
|
+
&& document.querySelector('.agent-handoff-meta')?.textContent?.includes('выбранном AI-агенте');
|
|
254
|
+
state.agentHandoff = null;
|
|
255
|
+
renderAgentHandoff();
|
|
256
|
+
const runtimeActionable = Boolean(request?.findings?.some(item => item.runtimeDiagnosticId));
|
|
257
|
+
const contextProposalAction = ['proposal', 'compare-selected', 'approve', 'source'].includes(document.querySelector('.review-next-action')?.dataset.action);
|
|
258
|
+
const smokeFinding = {
|
|
259
|
+
id: 'smoke-imported-finding', title: 'Smoke imported finding', category: 'smoke', severity: 'low', confidence: 'high',
|
|
260
|
+
screenId: screens[0].id, nodeId: nodeIdsForScreen(screens[0])[0] || null,
|
|
261
|
+
observation: 'Imported during smoke.', impact: 'None.', recommendation: 'None.',
|
|
262
|
+
evidence: [{ type: 'source', ref: 'smoke', note: 'Synthetic smoke fixture.' }], effort: 'small', proposalVersionId: 'smoke-imported-proposal'
|
|
263
|
+
};
|
|
264
|
+
importExpertReviewData({
|
|
265
|
+
type: 'ui-design-workbench-expert-review-result', requestRevision: reviewRevision, project: ir.project?.name,
|
|
266
|
+
summary: 'Smoke import', findings: [smokeFinding],
|
|
267
|
+
versions: [{ id: 'smoke-imported-proposal', label: 'Smoke proposal', kind: 'proposal', parent: baselineVersion, findingIds: [smokeFinding.id], resolvedFindingIds: [smokeFinding.id], nodeOverrides: {} }]
|
|
268
|
+
});
|
|
269
|
+
const importWorks = state.importedReview?.addedVersions === 1
|
|
270
|
+
&& findings.some(item => item.id === smokeFinding.id)
|
|
271
|
+
&& versionById['smoke-imported-proposal'];
|
|
272
|
+
state.compareBaseVersion = versions[1]?.id || baselineVersion;
|
|
273
|
+
state.compareTargetVersion = 'smoke-imported-proposal';
|
|
274
|
+
state.findingFocus = [];
|
|
275
|
+
state.findingFilter = 'all';
|
|
276
|
+
state.findingSource = 'all';
|
|
277
|
+
state.findingScreen = 'all';
|
|
278
|
+
state.showFindings = true;
|
|
279
|
+
const smokeAnnotation = {
|
|
280
|
+
id: 'smoke-canvas-comment', screenId: screens[0].id, nodeId: null,
|
|
281
|
+
versionId: 'smoke-imported-proposal', text: 'Smoke user comment.', priority: 'medium', status: 'new'
|
|
282
|
+
};
|
|
283
|
+
state.annotations.push(smokeAnnotation);
|
|
284
|
+
renderQueue();
|
|
285
|
+
renderCompareVersionOptions();
|
|
286
|
+
setScreen(screens[0].id, 'compare');
|
|
287
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
288
|
+
const compareVariantsWork = document.querySelectorAll('.compare-panel').length === 2
|
|
289
|
+
&& getComputedStyle(document.querySelector('.header-version-picker')).display === 'none'
|
|
290
|
+
&& document.querySelectorAll('.compare-version-select').length === 2
|
|
291
|
+
&& getComputedStyle(document.querySelector('.compare-workspace-controls')).display === 'flex';
|
|
292
|
+
const compareSelectorsMatchCanvas = document.querySelector('.compare-base-select')?.value === state.compareBaseVersion
|
|
293
|
+
&& document.querySelector('.compare-target-select')?.value === state.compareTargetVersion
|
|
294
|
+
&& Boolean(document.querySelector('.device[data-version-id="' + CSS.escape(state.compareBaseVersion) + '"]'))
|
|
295
|
+
&& Boolean(document.querySelector('.device[data-version-id="' + CSS.escape(state.compareTargetVersion) + '"]'));
|
|
296
|
+
const smokeMarker = document.querySelector('[data-open-finding="' + CSS.escape(smokeFinding.id) + '"]');
|
|
297
|
+
const smokeCommentMarker = document.querySelector('[data-open-annotation="' + CSS.escape(smokeAnnotation.id) + '"]');
|
|
298
|
+
const baseDeviceRect = document.querySelector('.device[data-version-id="' + CSS.escape(state.compareBaseVersion) + '"]')?.getBoundingClientRect();
|
|
299
|
+
const targetDeviceRect = document.querySelector('.device[data-version-id="' + CSS.escape(state.compareTargetVersion) + '"]')?.getBoundingClientRect();
|
|
300
|
+
const findingMarkerRect = smokeMarker?.getBoundingClientRect();
|
|
301
|
+
const commentMarkerRect = smokeCommentMarker?.getBoundingClientRect();
|
|
302
|
+
const markersFollowComparedViews = Boolean(baseDeviceRect && targetDeviceRect && findingMarkerRect && commentMarkerRect
|
|
303
|
+
&& findingMarkerRect.left + findingMarkerRect.width / 2 < baseDeviceRect.left
|
|
304
|
+
&& commentMarkerRect.left + commentMarkerRect.width / 2 > targetDeviceRect.right);
|
|
305
|
+
const userCommentHasDistinctColor = Boolean(smokeCommentMarker
|
|
306
|
+
&& getComputedStyle(smokeCommentMarker).backgroundColor === 'rgb(124, 58, 237)');
|
|
307
|
+
smokeCommentMarker?.click();
|
|
308
|
+
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
309
|
+
const commentPopoverOpens = Boolean(document.querySelector('[data-annotation-popover="' + CSS.escape(smokeAnnotation.id) + '"]'));
|
|
310
|
+
document.querySelector('[data-collapse-annotation="' + CSS.escape(smokeAnnotation.id) + '"]')?.click();
|
|
311
|
+
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
312
|
+
const commentPopoverCollapses = !document.querySelector('[data-annotation-popover="' + CSS.escape(smokeAnnotation.id) + '"]')
|
|
313
|
+
&& Boolean(document.querySelector('[data-open-annotation="' + CSS.escape(smokeAnnotation.id) + '"]'));
|
|
314
|
+
const smokeListNumber = document.querySelector('.finding-card[data-finding-id="' + CSS.escape(smokeFinding.id) + '"] .finding-list-index')?.textContent?.trim();
|
|
315
|
+
const markerNumberMatches = Boolean(smokeMarker && smokeMarker.textContent.trim() === smokeListNumber);
|
|
316
|
+
smokeMarker?.click();
|
|
317
|
+
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
318
|
+
const markerPopoverOpens = Boolean(document.querySelector('[data-finding-popover="' + CSS.escape(smokeFinding.id) + '"]'));
|
|
319
|
+
document.querySelector('[data-collapse-finding="' + CSS.escape(smokeFinding.id) + '"]')?.click();
|
|
320
|
+
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
321
|
+
const markerPopoverCollapses = !document.querySelector('[data-finding-popover="' + CSS.escape(smokeFinding.id) + '"]')
|
|
322
|
+
&& Boolean(document.querySelector('[data-open-finding="' + CSS.escape(smokeFinding.id) + '"]'));
|
|
323
|
+
const findingsToggle = document.querySelector('[data-canvas-layer="findings"]');
|
|
324
|
+
findingsToggle?.click();
|
|
325
|
+
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
326
|
+
const markersHide = document.querySelectorAll('.finding-pin,.finding-pin-card').length === 0
|
|
327
|
+
&& findingsToggle?.getAttribute('aria-pressed') === 'false'
|
|
328
|
+
&& Boolean(document.querySelector('[data-open-annotation="' + CSS.escape(smokeAnnotation.id) + '"]'));
|
|
329
|
+
findingsToggle?.click();
|
|
330
|
+
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
331
|
+
const markersRestore = Boolean(document.querySelector('[data-open-finding="' + CSS.escape(smokeFinding.id) + '"]'))
|
|
332
|
+
&& findingsToggle?.getAttribute('aria-pressed') === 'true';
|
|
333
|
+
state.activeVersion = 'smoke-imported-proposal';
|
|
334
|
+
versionSelect.value = state.activeVersion;
|
|
335
|
+
const navigationSource = screens.find(screen => nodeIdsForScreen(screen).some(nodeId => nodes[nodeId]?.action?.type === 'navigate' && nodes[nodeId]?.action?.target !== screen.id));
|
|
336
|
+
let prototypeNavigationWorks = true;
|
|
337
|
+
let prototypeTreePreservesMode = true;
|
|
338
|
+
let prototypeForcesInteraction = true;
|
|
339
|
+
if (navigationSource) {
|
|
340
|
+
const navigationNodeId = nodeIdsForScreen(navigationSource).find(nodeId => nodes[nodeId]?.action?.type === 'navigate' && nodes[nodeId]?.action?.target !== navigationSource.id);
|
|
341
|
+
const navigationTarget = nodes[navigationNodeId]?.action?.target;
|
|
342
|
+
state.interaction = 'inspect';
|
|
343
|
+
setScreen(navigationSource.id, 'single');
|
|
344
|
+
setView('prototype');
|
|
345
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
346
|
+
prototypeForcesInteraction = state.interaction === 'interact';
|
|
347
|
+
document.querySelector('.stage [data-node-id="' + CSS.escape(navigationNodeId) + '"]')?.click();
|
|
348
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
349
|
+
prototypeNavigationWorks = state.view === 'prototype' && state.screen === navigationTarget;
|
|
350
|
+
const treeTarget = screens.find(screen => screen.id !== state.screen)?.id;
|
|
351
|
+
if (treeTarget) {
|
|
352
|
+
document.querySelector('.screen-link[data-screen="' + CSS.escape(treeTarget) + '"]')?.click();
|
|
353
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
354
|
+
prototypeTreePreservesMode = state.view === 'prototype' && state.screen === treeTarget;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const scenarioScreen = screens[0];
|
|
359
|
+
const scenarioNodeId = scenarioScreen ? nodeIdsForScreen(scenarioScreen).find(nodeId => nodes[nodeId]?.type === 'text') : null;
|
|
360
|
+
let screenScenarioControlsWork = true;
|
|
361
|
+
let stateGalleryWorks = true;
|
|
362
|
+
if (scenarioScreen && scenarioNodeId) {
|
|
363
|
+
const originalScenarios = scenarioScreen.scenarios;
|
|
364
|
+
scenarioScreen.scenarios = [...(Array.isArray(originalScenarios) ? originalScenarios : []), { id: 'smoke-fixture', label: 'Smoke fixture', nodeOverrides: { [scenarioNodeId]: { text: 'Scenario fixture visible' } } }];
|
|
365
|
+
setScreen(scenarioScreen.id, 'single');
|
|
366
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
367
|
+
const scenarioSelect = document.querySelector('.screen-state-select[data-screen-scenario]');
|
|
368
|
+
const initialControlWidth = scenarioSelect?.getBoundingClientRect().width || 0;
|
|
369
|
+
if (scenarioSelect) {
|
|
370
|
+
scenarioSelect.value = 'smoke-fixture';
|
|
371
|
+
scenarioSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
|
372
|
+
}
|
|
373
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
374
|
+
const updatedScenarioSelect = document.querySelector('.screen-state-select[data-screen-scenario]');
|
|
375
|
+
screenScenarioControlsWork = Boolean(scenarioSelect)
|
|
376
|
+
&& state.screenScenarioIds[scenarioScreen.id] === 'smoke-fixture'
|
|
377
|
+
&& document.querySelector('.stage [data-node-id="' + CSS.escape(scenarioNodeId) + '"]')?.textContent === 'Scenario fixture visible'
|
|
378
|
+
&& updatedScenarioSelect?.value === 'smoke-fixture'
|
|
379
|
+
&& updatedScenarioSelect?.closest('.screen-control-group')?.querySelector('.screen-state-label')
|
|
380
|
+
&& Math.abs((updatedScenarioSelect?.getBoundingClientRect().width || 0) - initialControlWidth) < 1
|
|
381
|
+
&& document.activeElement === updatedScenarioSelect;
|
|
382
|
+
setView('states');
|
|
383
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
384
|
+
stateGalleryWorks = state.view === 'states'
|
|
385
|
+
&& document.querySelectorAll('.state-gallery .state-panel').length === screenScenarios(scenarioScreen).length
|
|
386
|
+
&& document.querySelectorAll('.state-gallery .device').length === screenScenarios(scenarioScreen).length
|
|
387
|
+
&& Boolean(document.querySelector('.screen-state-select[data-gallery-scenario]'))
|
|
388
|
+
&& (themes.length < 2 || Boolean(document.querySelector('.screen-state-select[data-gallery-theme]')))
|
|
389
|
+
&& document.querySelector('.variant-gallery-toolbar')?.style.zoom === ''
|
|
390
|
+
&& Number.parseFloat(document.querySelector('.variant-canvas')?.style.zoom || '0') > 0;
|
|
391
|
+
const matrixButton = document.querySelector('[data-variant-axis="matrix"]');
|
|
392
|
+
if (matrixButton) {
|
|
393
|
+
matrixButton.click();
|
|
394
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
395
|
+
stateGalleryWorks = stateGalleryWorks
|
|
396
|
+
&& state.variantAxis === 'matrix'
|
|
397
|
+
&& Boolean(document.querySelector('.screen-state-select[data-gallery-scenario]'))
|
|
398
|
+
&& Boolean(document.querySelector('.screen-state-select[data-gallery-theme]'))
|
|
399
|
+
&& document.querySelectorAll('.state-gallery .device').length === themes.length * screenScenarios(scenarioScreen).length;
|
|
400
|
+
}
|
|
401
|
+
if (originalScenarios === undefined) delete scenarioScreen.scenarios; else scenarioScreen.scenarios = originalScenarios;
|
|
402
|
+
delete state.screenScenarioIds[scenarioScreen.id];
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
setScreen(screens[0].id, 'single');
|
|
406
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
407
|
+
const resolvedMarkerHidden = !document.querySelector('[data-open-finding="' + CSS.escape(smokeFinding.id) + '"]');
|
|
408
|
+
const proposalMarkersHidden = document.querySelectorAll('[data-open-finding],[data-finding-popover]').length === 0;
|
|
409
|
+
state.activeVersion = baselineVersion;
|
|
410
|
+
versionSelect.value = state.activeVersion;
|
|
411
|
+
renderView();
|
|
412
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
413
|
+
const baselineMarkerRestored = Boolean(document.querySelector('[data-open-finding="' + CSS.escape(smokeFinding.id) + '"]'));
|
|
414
|
+
setView('compare');
|
|
415
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
416
|
+
const resolvedMarkersStayOnBaseline = document.querySelectorAll('[data-open-finding="' + CSS.escape(smokeFinding.id) + '"]').length === 1;
|
|
417
|
+
const compareFindingMarkersBaselineOnly = [...document.querySelectorAll('[data-open-finding],[data-finding-popover]')]
|
|
418
|
+
.every(marker => marker.dataset.markerDevice?.includes(':' + state.compareBaseVersion + ':'));
|
|
419
|
+
const versionArchitectureWorks = resolvedMarkerHidden && proposalMarkersHidden && baselineMarkerRestored
|
|
420
|
+
&& resolvedMarkersStayOnBaseline && compareFindingMarkersBaselineOnly;
|
|
421
|
+
state.activeVersion = 'smoke-imported-proposal';
|
|
422
|
+
versionSelect.value = state.activeVersion;
|
|
423
|
+
setReviewSection('changes');
|
|
424
|
+
renderFindings();
|
|
425
|
+
const addressedCard = document.querySelector('.finding-card[data-finding-id="' + CSS.escape(smokeFinding.id) + '"]');
|
|
426
|
+
const addressedIsNotVerified = findingAddressedInProposal(smokeFinding)
|
|
427
|
+
&& !findingResolved(smokeFinding)
|
|
428
|
+
&& addressedCard?.querySelector('.finding-card-status')?.textContent?.trim() === 'Учтено в новом макете'
|
|
429
|
+
&& document.querySelector('.version-application-status')?.textContent?.trim() === 'Ещё не применён к проекту'
|
|
430
|
+
&& !document.querySelector('[data-review-section="changes"]')?.classList.contains('complete');
|
|
431
|
+
state.compareBaseVersion = baselineVersion;
|
|
432
|
+
state.compareTargetVersion = 'smoke-imported-proposal';
|
|
433
|
+
setView('compare');
|
|
434
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
435
|
+
const markerBeforeZoom = document.querySelector('[data-open-finding="' + CSS.escape(smokeFinding.id) + '"]')?.getBoundingClientRect();
|
|
436
|
+
const deviceBeforeZoom = document.querySelector('.device[data-version-id="' + CSS.escape(state.compareBaseVersion) + '"]')?.getBoundingClientRect();
|
|
437
|
+
const anchorBeforeZoom = document.querySelector('.device[data-version-id="' + CSS.escape(state.compareBaseVersion) + '"] .device-content')?.getBoundingClientRect();
|
|
438
|
+
const markerOffsetBeforeZoom = markerBeforeZoom && deviceBeforeZoom && anchorBeforeZoom ? (() => {
|
|
439
|
+
const center = markerBeforeZoom.left + markerBeforeZoom.width / 2;
|
|
440
|
+
const side = center < deviceBeforeZoom.left ? 'left' : 'right';
|
|
441
|
+
const centerY = markerBeforeZoom.top + markerBeforeZoom.height / 2;
|
|
442
|
+
return { side, x: center - (side === 'left' ? deviceBeforeZoom.left : deviceBeforeZoom.right), y: centerY - (anchorBeforeZoom.top + Math.min(12, anchorBeforeZoom.height / 2)), withinDeviceY: centerY >= deviceBeforeZoom.top - 1 && centerY <= deviceBeforeZoom.bottom + 1 };
|
|
443
|
+
})() : null;
|
|
444
|
+
setZoom(.8);
|
|
445
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
446
|
+
const zoomedMarker = document.querySelector('[data-open-finding="' + CSS.escape(smokeFinding.id) + '"]');
|
|
447
|
+
const zoomedDevice = document.querySelector('.device[data-version-id="' + CSS.escape(state.compareBaseVersion) + '"]');
|
|
448
|
+
const zoomedAnchor = zoomedDevice?.querySelector('.device-content');
|
|
449
|
+
const markerRect = zoomedMarker?.getBoundingClientRect();
|
|
450
|
+
const deviceRect = zoomedDevice?.getBoundingClientRect();
|
|
451
|
+
const anchorRect = zoomedAnchor?.getBoundingClientRect();
|
|
452
|
+
const markerOffsetAfterZoom = markerRect && deviceRect && anchorRect ? (() => {
|
|
453
|
+
const center = markerRect.left + markerRect.width / 2;
|
|
454
|
+
const side = center < deviceRect.left ? 'left' : 'right';
|
|
455
|
+
const centerY = markerRect.top + markerRect.height / 2;
|
|
456
|
+
return { side, x: center - (side === 'left' ? deviceRect.left : deviceRect.right), y: centerY - (anchorRect.top + Math.min(12, anchorRect.height / 2)), withinDeviceY: centerY >= deviceRect.top - 1 && centerY <= deviceRect.bottom + 1 };
|
|
457
|
+
})() : null;
|
|
458
|
+
const markerTracksZoom = Boolean(markerOffsetBeforeZoom && markerOffsetAfterZoom
|
|
459
|
+
&& markerOffsetBeforeZoom.side === markerOffsetAfterZoom.side
|
|
460
|
+
&& Math.abs(markerOffsetBeforeZoom.x) <= 70 && Math.abs(markerOffsetAfterZoom.x) <= 70
|
|
461
|
+
&& markerOffsetBeforeZoom.withinDeviceY && markerOffsetAfterZoom.withinDeviceY);
|
|
462
|
+
const markerTrackMetrics = { before: markerOffsetBeforeZoom, after: markerOffsetAfterZoom };
|
|
463
|
+
const markerGroups = new Map();
|
|
464
|
+
for (const marker of document.querySelectorAll('.finding-pin[data-marker-device],.annotation-pin[data-marker-device]')) {
|
|
465
|
+
const group = markerGroups.get(marker.dataset.markerDevice) || [];
|
|
466
|
+
group.push(marker.getBoundingClientRect());
|
|
467
|
+
markerGroups.set(marker.dataset.markerDevice, group);
|
|
468
|
+
}
|
|
469
|
+
let markerOverlaps = 0;
|
|
470
|
+
for (const rects of markerGroups.values()) for (let first = 0; first < rects.length; first += 1) {
|
|
471
|
+
for (let second = first + 1; second < rects.length; second += 1) {
|
|
472
|
+
const x = Math.max(0, Math.min(rects[first].right, rects[second].right) - Math.max(rects[first].left, rects[second].left));
|
|
473
|
+
const y = Math.max(0, Math.min(rects[first].bottom, rects[second].bottom) - Math.max(rects[first].top, rects[second].top));
|
|
474
|
+
if (x * y > 1) markerOverlaps += 1;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const markerLayoutStable = markerGroups.size > 0 && markerOverlaps === 0;
|
|
478
|
+
document.querySelectorAll('[data-collapse-finding]').forEach(button => button.click());
|
|
479
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
480
|
+
const linkedMarker = document.querySelector('.finding-pin-leader + [data-open-finding="' + CSS.escape(smokeFinding.id) + '"]')
|
|
481
|
+
|| document.querySelector('.finding-pin-leader + .finding-pin');
|
|
482
|
+
const leadersHiddenByDefault = [...document.querySelectorAll('.finding-pin-leader')]
|
|
483
|
+
.every(leader => leader.nextElementSibling?.matches(':hover') || Number(getComputedStyle(leader).opacity) === 0);
|
|
484
|
+
linkedMarker?.click();
|
|
485
|
+
await new Promise(resolve => setTimeout(resolve, 180));
|
|
486
|
+
const openLeader = document.querySelector('.finding-pin-leader + [data-finding-popover="' + CSS.escape(smokeFinding.id) + '"]')
|
|
487
|
+
|| document.querySelector('.finding-pin-leader + .finding-pin-card');
|
|
488
|
+
const leaderRevealsForOpenCard = !linkedMarker || Boolean(openLeader && Number(getComputedStyle(openLeader).opacity) === 1);
|
|
489
|
+
document.querySelector('[data-collapse-finding]')?.click();
|
|
490
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
491
|
+
const markerLeaderWorks = leadersHiddenByDefault && leaderRevealsForOpenCard;
|
|
492
|
+
const markerLeaderMetrics = { leadersHiddenByDefault, leaderRevealsForOpenCard, linkedMarker: Boolean(linkedMarker), openLeader: Boolean(openLeader), openLeaderOpacity: openLeader ? getComputedStyle(openLeader).opacity : null };
|
|
493
|
+
const railCommandsWork = ['inspect', 'review', 'comments'].every(tab => Boolean(document.querySelector('.workbench-rail [data-inspector-tab="' + tab + '"]')))
|
|
494
|
+
&& document.querySelectorAll('.workbench-rail .rail-menu').length === 2
|
|
495
|
+
&& Boolean(document.querySelector('.workbench-rail .rail-menu.language-menu .locale-current'))
|
|
496
|
+
&& Boolean(document.querySelector('.workbench-rail .rail-menu .export-feedback'))
|
|
497
|
+
&& document.querySelectorAll('.canvas-tools [data-interaction]').length === 1
|
|
498
|
+
&& Boolean(document.querySelector('.canvas-tools [data-interaction="interact"]'))
|
|
499
|
+
&& !document.querySelector('.canvas-tools [data-canvas-layer]');
|
|
500
|
+
const fileRailMenu = document.querySelector('.workbench-rail .rail-menu:not(.language-menu)');
|
|
501
|
+
const languageRailMenu = document.querySelector('.workbench-rail .rail-menu.language-menu');
|
|
502
|
+
const fileRailSummaryRect = fileRailMenu?.querySelector('summary')?.getBoundingClientRect();
|
|
503
|
+
const fileRailIconRect = fileRailMenu?.querySelector('summary .icon')?.getBoundingClientRect();
|
|
504
|
+
const railFileGeometryWorks = Boolean(fileRailMenu && fileRailSummaryRect && fileRailIconRect
|
|
505
|
+
&& document.querySelector('.workbench-rail')?.firstElementChild === fileRailMenu
|
|
506
|
+
&& Math.abs((fileRailSummaryRect.left + fileRailSummaryRect.right - fileRailIconRect.left - fileRailIconRect.right) / 2) <= .5
|
|
507
|
+
&& Math.abs((fileRailSummaryRect.top + fileRailSummaryRect.bottom - fileRailIconRect.top - fileRailIconRect.bottom) / 2) <= .5);
|
|
508
|
+
fileRailMenu?.querySelector('summary')?.click();
|
|
509
|
+
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
510
|
+
const fileRailMenuOpens = fileRailMenu?.open === true;
|
|
511
|
+
languageRailMenu?.querySelector('summary')?.click();
|
|
512
|
+
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
513
|
+
const railMenusAreExclusive = languageRailMenu?.open === true && fileRailMenu?.open === false;
|
|
514
|
+
document.body.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, button: 0 }));
|
|
515
|
+
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
516
|
+
const railMenusDismiss = languageRailMenu?.open === false && fileRailMenu?.open === false;
|
|
517
|
+
const railMenusWork = fileRailMenuOpens && railMenusAreExclusive && railMenusDismiss;
|
|
518
|
+
state.annotations = state.annotations.filter(item => item.id !== smokeAnnotation.id);
|
|
519
|
+
state.openAnnotationId = null;
|
|
520
|
+
renderQueue();
|
|
521
|
+
renderFindingMarkers();
|
|
522
|
+
setWorkbenchLocale('en', { remember: false, updateLocation: false });
|
|
523
|
+
const localeMetrics = { englishCanvas: document.querySelector('.canvas-tools')?.getAttribute('aria-label'), englishVersionTitle: document.querySelector('.header-version-picker')?.getAttribute('title'), englishCompare: document.querySelector('.mode-button[data-view="compare"] .button-label')?.textContent?.trim() };
|
|
524
|
+
const englishLocaleWorks = document.documentElement.lang === 'en'
|
|
525
|
+
&& document.querySelector('.canvas-tools')?.getAttribute('aria-label') === 'Canvas tools'
|
|
526
|
+
&& document.querySelector('.header-version-picker')?.getAttribute('title') === 'Currently displayed mockup'
|
|
527
|
+
&& document.querySelector('.mode-button[data-view="compare"] .button-label')?.textContent?.trim() === 'Compare';
|
|
528
|
+
setWorkbenchLocale('ru', { remember: false, updateLocation: false });
|
|
529
|
+
Object.assign(localeMetrics, { russianCanvas: document.querySelector('.canvas-tools')?.getAttribute('aria-label'), russianVersionTitle: document.querySelector('.header-version-picker')?.getAttribute('title'), russianCompare: document.querySelector('.mode-button[data-view="compare"] .button-label')?.textContent?.trim() });
|
|
530
|
+
const russianLocaleRestores = document.documentElement.lang === 'ru'
|
|
531
|
+
&& document.querySelector('.canvas-tools')?.getAttribute('aria-label') === 'Инструменты холста'
|
|
532
|
+
&& document.querySelector('.header-version-picker')?.getAttribute('title') === 'Какой макет сейчас показан'
|
|
533
|
+
&& document.querySelector('.mode-button[data-view="compare"] .button-label')?.textContent?.trim() === 'Сравнить';
|
|
534
|
+
for (const id of Object.keys(state.findingDecisions)) state.findingDecisions[id] = 'pending';
|
|
535
|
+
state.findingDecisions[smokeFinding.id] = 'accepted';
|
|
536
|
+
renderFindings();
|
|
537
|
+
const sourceBeforeApproval = document.querySelector('.review-next-action')?.dataset.action !== 'source';
|
|
538
|
+
approveActiveVersion();
|
|
539
|
+
setReviewSection('summary');
|
|
540
|
+
const sourceHiddenOnSummary = document.querySelector('.review-next-action')?.dataset.action !== 'source';
|
|
541
|
+
setReviewSection('problems');
|
|
542
|
+
const sourceHiddenOnProblems = document.querySelector('.review-next-action')?.dataset.action !== 'source';
|
|
543
|
+
setReviewSection('changes');
|
|
544
|
+
const sourceAfterApproval = document.querySelector('.review-next-action')?.dataset.action === 'source';
|
|
545
|
+
const applyOnlyOnFixStep = sourceHiddenOnSummary && sourceHiddenOnProblems && sourceAfterApproval;
|
|
546
|
+
const sourceRequest = typeof sourceRequestPayload === 'function' ? sourceRequestPayload() : null;
|
|
547
|
+
const implementationHandoff = window.__uiPreviewDiagnostics?.agentHandoff?.('implement');
|
|
548
|
+
const directFixEntryPoint = Boolean(document.querySelector('.fix-all-source'));
|
|
549
|
+
const revisionVisible = Boolean(document.querySelector('.revision-badge')?.textContent?.trim());
|
|
550
|
+
const declaredScenarioCases = screens.flatMap(screen => screenScenarios(screen).slice(1).map(scenario => [screen.id, scenario.id]));
|
|
551
|
+
const screenScenarioDiagnosticsWork = declaredScenarioCases.every(([screenId, scenarioId]) => (state.diagnostics?.checks || []).some(check => check.scenarioId === 'state-matrix' && check.screenId === screenId && check.metrics?.screenScenarioId === scenarioId));
|
|
552
|
+
|
|
553
|
+
setScreen(screens[0].id, 'single');
|
|
554
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
555
|
+
const firstNode = document.querySelector('.stage .ui-node[data-node-id]');
|
|
556
|
+
const selectedNodeId = firstNode?.dataset.nodeId || null;
|
|
557
|
+
if (firstNode) selectNode(firstNode);
|
|
558
|
+
setZoom(.9);
|
|
559
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
560
|
+
const selectionPreserved = !selectedNodeId || document.querySelector('[data-node-id="' + CSS.escape(selectedNodeId) + '"]')?.dataset.selected === 'true';
|
|
561
|
+
|
|
562
|
+
setView('overview');
|
|
563
|
+
setZoom(2);
|
|
564
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
565
|
+
const panStage = document.querySelector('.stage');
|
|
566
|
+
panStage.scrollLeft = 0;
|
|
567
|
+
panStage.scrollTop = 0;
|
|
568
|
+
panStage.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, pointerId: 77, button: 1, buttons: 4, clientX: 420, clientY: 360 }));
|
|
569
|
+
panStage.dispatchEvent(new PointerEvent('pointermove', { bubbles: true, pointerId: 77, button: -1, buttons: 4, clientX: 350, clientY: 310 }));
|
|
570
|
+
const middlePanWorks = panStage.scrollLeft >= 60
|
|
571
|
+
&& (panStage.scrollHeight <= panStage.clientHeight + 1 || panStage.scrollTop >= 40)
|
|
572
|
+
&& panStage.classList.contains('canvas-panning');
|
|
573
|
+
const middlePanMetrics = { left: panStage.scrollLeft, top: panStage.scrollTop, scrollWidth: panStage.scrollWidth, scrollHeight: panStage.scrollHeight, clientWidth: panStage.clientWidth, clientHeight: panStage.clientHeight, active: panStage.classList.contains('canvas-panning') };
|
|
574
|
+
panStage.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, pointerId: 77, button: 1, buttons: 0, clientX: 350, clientY: 310 }));
|
|
575
|
+
const middlePanReleases = !panStage.classList.contains('canvas-panning');
|
|
576
|
+
setZoom(1);
|
|
577
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
578
|
+
const stage = document.querySelector('.stage');
|
|
579
|
+
const requestedScroll = Math.min(40, Math.max(0, stage.scrollWidth - stage.clientWidth));
|
|
580
|
+
stage.scrollLeft = requestedScroll;
|
|
581
|
+
renderView();
|
|
582
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
583
|
+
const renderedStage = document.querySelector('.stage');
|
|
584
|
+
const expectedScroll = Math.min(requestedScroll, Math.max(0, renderedStage.scrollWidth - renderedStage.clientWidth));
|
|
585
|
+
const scrollPreserved = Math.abs(renderedStage.scrollLeft - expectedScroll) <= 1;
|
|
586
|
+
|
|
587
|
+
setInspectorTab('inspect');
|
|
588
|
+
const inspectTabWorks = document.querySelector('[data-inspector-pane="inspect"]')?.hidden === false
|
|
589
|
+
&& [...document.querySelectorAll('[data-inspector-pane]')].filter(pane => !pane.hidden).length === 1;
|
|
590
|
+
setInspectorTab('comments');
|
|
591
|
+
const commentsTabWorks = document.querySelector('[data-inspector-pane="comments"]')?.hidden === false
|
|
592
|
+
&& [...document.querySelectorAll('[data-inspector-pane]')].filter(pane => !pane.hidden).length === 1;
|
|
593
|
+
setInspectorTab('review');
|
|
594
|
+
const reviewTabWorks = document.querySelector('[data-inspector-pane="review"]')?.hidden === false
|
|
595
|
+
&& [...document.querySelectorAll('[data-inspector-pane]')].filter(pane => !pane.hidden).length === 1;
|
|
596
|
+
|
|
597
|
+
const search = document.querySelector('.screen-search');
|
|
598
|
+
const searchProbe = [...document.querySelectorAll('.screen-link')].at(-1)?.textContent?.trim() || '';
|
|
599
|
+
if (search) {
|
|
600
|
+
search.value = searchProbe;
|
|
601
|
+
search.dispatchEvent(new Event('input', { bubbles: true }));
|
|
602
|
+
}
|
|
603
|
+
const treeSearchWorks = [...document.querySelectorAll('.screen-link:not([hidden])')]
|
|
604
|
+
.some(button => button.textContent.trim() === searchProbe);
|
|
605
|
+
if (search) {
|
|
606
|
+
search.value = '';
|
|
607
|
+
search.dispatchEvent(new Event('input', { bubbles: true }));
|
|
608
|
+
}
|
|
609
|
+
const evidenceEdges = (ir.navigationGraph?.edges || []).filter(edge => edge.kind === 'open-logical-view');
|
|
610
|
+
const nestedTreeWorks = evidenceEdges.every(edge => (treePaths[edge.to]?.length || 0) > (treePaths[edge.from]?.length || 0));
|
|
611
|
+
|
|
612
|
+
let compactPanelsExclusive = true;
|
|
613
|
+
if (innerWidth <= 980) {
|
|
614
|
+
state.sidebarOpen = true;
|
|
615
|
+
state.inspectorOpen = false;
|
|
616
|
+
applyPanelState();
|
|
617
|
+
document.querySelector('.inspector-toggle')?.click();
|
|
618
|
+
const rightOpenedAlone = state.inspectorOpen && !state.sidebarOpen;
|
|
619
|
+
document.querySelector('.sidebar-toggle')?.click();
|
|
620
|
+
const leftOpenedAlone = state.sidebarOpen && !state.inspectorOpen;
|
|
621
|
+
compactPanelsExclusive = rightOpenedAlone && leftOpenedAlone;
|
|
622
|
+
setInspectorTab('review');
|
|
623
|
+
}
|
|
624
|
+
const reviewBackup = feedbackPayload();
|
|
625
|
+
const expectedAnnotationCount = state.annotations.length;
|
|
626
|
+
const expectedDecisionCount = Object.keys(state.findingDecisions).length;
|
|
627
|
+
state.annotations = [];
|
|
628
|
+
state.findingDecisions = {};
|
|
629
|
+
restoreReviewStateData(reviewBackup);
|
|
630
|
+
const reviewBackupWorks = reviewBackup.type === 'ui-design-workbench-review-state'
|
|
631
|
+
&& reviewBackup.diagnosticsEngineVersion === diagnosticsEngineVersion
|
|
632
|
+
&& state.annotations.length === expectedAnnotationCount
|
|
633
|
+
&& Object.keys(state.findingDecisions).length === expectedDecisionCount;
|
|
634
|
+
return {
|
|
635
|
+
totalBefore,
|
|
636
|
+
totalAfter,
|
|
637
|
+
actionableChecks,
|
|
638
|
+
actionableGroups,
|
|
639
|
+
diagnosticFindingCreated,
|
|
640
|
+
diagnosticsDoNotDownload,
|
|
641
|
+
staleRuntimePruned,
|
|
642
|
+
autoReviewFindings,
|
|
643
|
+
autoReviewButton: Boolean(document.querySelector('.review-next-action')),
|
|
644
|
+
agentReviewButtonEnabled: document.querySelector('.open-agent-review')?.disabled === false,
|
|
645
|
+
expertReviewButtonEnabled: document.querySelector('.export-review-request')?.disabled === false,
|
|
646
|
+
expertRequestValid: expertRequest?.type === 'ui-design-workbench-expert-review-request'
|
|
647
|
+
&& expertRequest?.diagnosticsSummary?.status === 'complete'
|
|
648
|
+
&& expertRequest?.activeVersion === baselineVersion
|
|
649
|
+
&& expertRequest?.reviewVersionId === baselineVersion
|
|
650
|
+
&& expertRequest?.screenIds?.length === screens.length
|
|
651
|
+
&& expertRequest?.contextFile?.endsWith('ui-agent-context.json')
|
|
652
|
+
&& expertRequest?.patchFile?.endsWith('ui-ir.patch.json')
|
|
653
|
+
&& expertRequest?.requiredChatReport?.includes('plain numbered list')
|
|
654
|
+
&& expertRequest?.requiredChatReport?.includes('Do not duplicate it as plain text in HTML')
|
|
655
|
+
&& expertRequest?.expectedResult?.type === 'ui-design-workbench-ir-patch',
|
|
656
|
+
expertHandoffValid: expertHandoff?.supported === true
|
|
657
|
+
&& expertHandoff?.provider === 'generic'
|
|
658
|
+
&& expertHandoff?.url === null
|
|
659
|
+
&& expertHandoff?.path === previewContext.artifactDir
|
|
660
|
+
&& expertHandoff?.context?.activeVersion === baselineVersion
|
|
661
|
+
&& expertHandoff?.context?.reviewVersionId === baselineVersion
|
|
662
|
+
&& expertHandoff?.prompt?.includes('ui-design-workbench')
|
|
663
|
+
&& expertHandoff?.prompt?.includes('Не изменяй исходный проект'),
|
|
664
|
+
proposalHandoffValid: selected === 0 || (proposalHandoff?.supported === true
|
|
665
|
+
&& proposalHandoff?.context?.acceptedFindingIds?.length === selected
|
|
666
|
+
&& proposalHandoff?.context?.sourceChangeAllowed === false),
|
|
667
|
+
implementationHandoffValid: implementationHandoff?.supported === true
|
|
668
|
+
&& implementationHandoff?.context?.sourceChangeAllowed === true
|
|
669
|
+
&& Boolean(implementationHandoff?.context?.projectRoot)
|
|
670
|
+
&& implementationHandoff?.context?.contextFile?.endsWith('ui-agent-context.json')
|
|
671
|
+
&& implementationHandoff?.prompt?.includes('contextFile')
|
|
672
|
+
&& implementationHandoff?.prompt?.includes('не запускай новое полное ревью'),
|
|
673
|
+
directFixEntryPoint,
|
|
674
|
+
handoffPanelWorks,
|
|
675
|
+
contextProposalAction,
|
|
676
|
+
importWorks: Boolean(importWorks),
|
|
677
|
+
compareVariantsWork,
|
|
678
|
+
compareSelectorsMatchCanvas,
|
|
679
|
+
layerControlsWork: document.querySelectorAll('[data-canvas-layer]').length === 1
|
|
680
|
+
&& document.querySelectorAll('.mode-button[data-view]').length === 5
|
|
681
|
+
&& document.querySelectorAll('.compare-version-select').length === 2,
|
|
682
|
+
prototypeForcesInteraction,
|
|
683
|
+
prototypeNavigationWorks,
|
|
684
|
+
prototypeTreePreservesMode,
|
|
685
|
+
screenScenarioControlsWork,
|
|
686
|
+
stateGalleryWorks,
|
|
687
|
+
screenScenarioDiagnosticsWork,
|
|
688
|
+
markerNumberMatches,
|
|
689
|
+
markerPopoverOpens,
|
|
690
|
+
markerPopoverCollapses,
|
|
691
|
+
commentPopoverOpens,
|
|
692
|
+
commentPopoverCollapses,
|
|
693
|
+
userCommentHasDistinctColor,
|
|
694
|
+
markersFollowComparedViews,
|
|
695
|
+
railCommandsWork,
|
|
696
|
+
railMenusWork,
|
|
697
|
+
railFileGeometryWorks,
|
|
698
|
+
markersToggleWork: markersHide && markersRestore,
|
|
699
|
+
versionArchitectureWorks,
|
|
700
|
+
addressedIsNotVerified,
|
|
701
|
+
markerTracksZoom,
|
|
702
|
+
markerTrackMetrics,
|
|
703
|
+
markerLayoutStable,
|
|
704
|
+
markerOverlaps,
|
|
705
|
+
markerLeaderWorks,
|
|
706
|
+
markerLeaderMetrics,
|
|
707
|
+
middleMousePanWorks: middlePanWorks && middlePanReleases,
|
|
708
|
+
middlePanMetrics,
|
|
709
|
+
localeSwitchWorks: englishLocaleWorks && russianLocaleRestores,
|
|
710
|
+
localeMetrics,
|
|
711
|
+
reviewSectionsWork: ['summary', 'problems', 'changes'].every(section => Boolean(document.querySelector('[data-review-section="' + section + '"]'))),
|
|
712
|
+
simplifiedReviewFlow: document.querySelectorAll('.review-phase').length === 0
|
|
713
|
+
&& !document.querySelector('.review-context-status')?.textContent?.includes('Шаг 2 из 3'),
|
|
714
|
+
reviewBackupWorks,
|
|
715
|
+
reviewBackupControls: Boolean(document.querySelector('.export-feedback') && document.querySelector('.import-feedback') && document.querySelector('.import-feedback-file')),
|
|
716
|
+
auditLinks: document.querySelectorAll('[data-audit-findings]').length,
|
|
717
|
+
linkedVisible,
|
|
718
|
+
selected,
|
|
719
|
+
selectionNextStep,
|
|
720
|
+
bulkSelectionPathWorks,
|
|
721
|
+
requestFindings: request?.acceptedFindingIds?.length || 0,
|
|
722
|
+
runtimeActionable,
|
|
723
|
+
reviewEntryPoint: Boolean(document.querySelector('[data-inspector-tab="review"]')),
|
|
724
|
+
sourceBeforeApproval,
|
|
725
|
+
sourceAfterApproval,
|
|
726
|
+
applyOnlyOnFixStep,
|
|
727
|
+
sourceApprovalGuard: sourceRequest?.sourceChangeAllowed === true
|
|
728
|
+
&& Boolean(sourceRequest?.projectRoot)
|
|
729
|
+
&& sourceRequest?.contextFile?.endsWith('ui-agent-context.json')
|
|
730
|
+
&& Array.isArray(sourceRequest?.requiredChatReport)
|
|
731
|
+
&& sourceRequest?.requestedAction?.includes('never repeat the full AI review automatically'),
|
|
732
|
+
revisionVisible,
|
|
733
|
+
selectionPreserved,
|
|
734
|
+
scrollPreserved,
|
|
735
|
+
inspectTabWorks,
|
|
736
|
+
commentsTabWorks,
|
|
737
|
+
reviewTabWorks,
|
|
738
|
+
treeSearchWorks,
|
|
739
|
+
nestedTreeWorks,
|
|
740
|
+
compactPanelsExclusive,
|
|
741
|
+
};
|
|
742
|
+
})()`,
|
|
743
|
+
returnByValue: true,
|
|
744
|
+
awaitPromise: true,
|
|
745
|
+
});
|
|
746
|
+
if (!args.captureOnly && workflowResponse.exceptionDetails) {
|
|
747
|
+
throw new Error(`Review workflow evaluation failed: ${workflowResponse.exceptionDetails.exception?.description || workflowResponse.exceptionDetails.text || 'unknown error'}`);
|
|
748
|
+
}
|
|
749
|
+
const workflow = workflowResponse.result?.value || {};
|
|
750
|
+
if (!args.captureOnly && !workflow.reviewEntryPoint) throw new Error('Review workflow entry point is missing');
|
|
751
|
+
const expectedRuntimeFindings = workflow.actionableGroups;
|
|
752
|
+
const actionableWorkflowFailed = workflow.diagnosticFindingCreated
|
|
753
|
+
? (!workflow.contextProposalAction || workflow.selected !== 1 || workflow.requestFindings !== 1 || !workflow.runtimeActionable)
|
|
754
|
+
: (workflow.selected !== 0 || workflow.requestFindings !== 0 || workflow.runtimeActionable);
|
|
755
|
+
if (!args.captureOnly && workflow.totalBefore > 0 && (
|
|
756
|
+
!workflow.auditLinks || !workflow.linkedVisible || workflow.totalAfter !== workflow.totalBefore + expectedRuntimeFindings
|
|
757
|
+
|| !workflow.diagnosticsDoNotDownload || !workflow.staleRuntimePruned
|
|
758
|
+
|| workflow.autoReviewFindings !== expectedRuntimeFindings || !workflow.autoReviewButton
|
|
759
|
+
|| !workflow.agentReviewButtonEnabled || !workflow.expertReviewButtonEnabled || !workflow.expertRequestValid
|
|
760
|
+
|| !workflow.expertHandoffValid || !workflow.proposalHandoffValid || !workflow.implementationHandoffValid || !workflow.directFixEntryPoint
|
|
761
|
+
|| !workflow.handoffPanelWorks
|
|
762
|
+
|| actionableWorkflowFailed || !workflow.importWorks || !workflow.compareVariantsWork || !workflow.compareSelectorsMatchCanvas || !workflow.reviewSectionsWork
|
|
763
|
+
|| !workflow.simplifiedReviewFlow || !workflow.reviewBackupWorks || !workflow.reviewBackupControls
|
|
764
|
+
|| !workflow.layerControlsWork || !workflow.prototypeForcesInteraction || !workflow.prototypeNavigationWorks || !workflow.prototypeTreePreservesMode
|
|
765
|
+
|| !workflow.screenScenarioControlsWork || !workflow.stateGalleryWorks || !workflow.screenScenarioDiagnosticsWork || !workflow.markerNumberMatches || !workflow.markerPopoverOpens
|
|
766
|
+
|| !workflow.markerPopoverCollapses || !workflow.commentPopoverOpens || !workflow.commentPopoverCollapses
|
|
767
|
+
|| !workflow.userCommentHasDistinctColor || !workflow.markersFollowComparedViews || !workflow.railCommandsWork || !workflow.railMenusWork || !workflow.railFileGeometryWorks
|
|
768
|
+
|| !workflow.markersToggleWork || !workflow.versionArchitectureWorks || !workflow.addressedIsNotVerified
|
|
769
|
+
|| !workflow.markerTracksZoom || !workflow.markerLayoutStable || !workflow.markerLeaderWorks || !workflow.middleMousePanWorks || !workflow.localeSwitchWorks
|
|
770
|
+
|| !workflow.selectionNextStep || !workflow.bulkSelectionPathWorks || !workflow.sourceBeforeApproval || !workflow.sourceAfterApproval || !workflow.applyOnlyOnFixStep
|
|
771
|
+
|| !workflow.sourceApprovalGuard || !workflow.revisionVisible
|
|
772
|
+
|| !workflow.selectionPreserved || !workflow.scrollPreserved
|
|
773
|
+
|| !workflow.inspectTabWorks || !workflow.commentsTabWorks || !workflow.reviewTabWorks
|
|
774
|
+
|| !workflow.treeSearchWorks || !workflow.nestedTreeWorks || !workflow.compactPanelsExclusive
|
|
775
|
+
)) {
|
|
776
|
+
throw new Error(`Review workflow smoke failed: ${JSON.stringify(workflow)}`);
|
|
777
|
+
}
|
|
778
|
+
const fixedProfiles = (report.profiles || []).filter(item => item.viewport && item.viewport !== 'current');
|
|
779
|
+
const measuredProfiles = new Set((report.checks || []).map(item => item.metrics?.profileId).filter(Boolean));
|
|
780
|
+
if (fixedProfiles.length < 2 || fixedProfiles.some(item => !measuredProfiles.has(item.id))) {
|
|
781
|
+
throw new Error(`Viewport profile smoke failed: configured=${fixedProfiles.map(item => item.id)} measured=${[...measuredProfiles]}`);
|
|
782
|
+
}
|
|
783
|
+
const captureState = { ...initialVisualState };
|
|
784
|
+
captureState.findingFocus = [];
|
|
785
|
+
captureState.findingFilter = 'all';
|
|
786
|
+
captureState.findingSource = 'all';
|
|
787
|
+
captureState.findingScreen = 'all';
|
|
788
|
+
if (args.captureView) captureState.view = args.captureView;
|
|
789
|
+
if (args.captureScreen) captureState.screen = args.captureScreen;
|
|
790
|
+
if (args.captureLeftPanel) captureState.sidebarOpen = args.captureLeftPanel === 'open';
|
|
791
|
+
if (args.captureRightPanel) captureState.inspectorOpen = args.captureRightPanel === 'open';
|
|
792
|
+
if (args.captureInspectorTab) captureState.inspectorTab = args.captureInspectorTab;
|
|
793
|
+
if (args.captureReviewSection) captureState.reviewSection = args.captureReviewSection;
|
|
794
|
+
if (args.viewportWidth <= 980 && captureState.sidebarOpen && captureState.inspectorOpen) captureState.sidebarOpen = false;
|
|
795
|
+
await cdp.call('Runtime.evaluate', {
|
|
796
|
+
expression: `(() => {
|
|
797
|
+
const snapshot = ${JSON.stringify(captureState)};
|
|
798
|
+
const originalVersionIds = new Set(snapshot.versionIds || []);
|
|
799
|
+
for (let index = versions.length - 1; index >= 0; index -= 1) {
|
|
800
|
+
if (originalVersionIds.has(versions[index].id)) continue;
|
|
801
|
+
delete versionById[versions[index].id];
|
|
802
|
+
versions.splice(index, 1);
|
|
803
|
+
}
|
|
804
|
+
document.querySelectorAll('.version-select option,.compare-version-select option').forEach(option => {
|
|
805
|
+
if (!originalVersionIds.has(option.value)) option.remove();
|
|
806
|
+
});
|
|
807
|
+
delete snapshot.versionIds;
|
|
808
|
+
if (!screens.some(item => item.id === snapshot.screen)) snapshot.screen = screens[0]?.id;
|
|
809
|
+
Object.assign(state, snapshot);
|
|
810
|
+
versionSelect.value = state.activeVersion;
|
|
811
|
+
writeLocation(false);
|
|
812
|
+
applyPanelState();
|
|
813
|
+
syncInteractionButtons();
|
|
814
|
+
renderFindings();
|
|
815
|
+
renderQueue();
|
|
816
|
+
renderFixQueue();
|
|
817
|
+
renderReviewSections();
|
|
818
|
+
renderCoverage();
|
|
819
|
+
renderReviewHistory();
|
|
820
|
+
renderImportStatus();
|
|
821
|
+
renderAgentHandoff();
|
|
822
|
+
renderView();
|
|
823
|
+
document.querySelector('[data-inspector-pane="review"]')?.scrollTo({ top: 0, behavior: 'instant' });
|
|
824
|
+
const toast = document.querySelector('.workbench-toast');
|
|
825
|
+
if (toast) { toast.classList.remove('visible'); toast.textContent = ''; }
|
|
826
|
+
})()`,
|
|
827
|
+
returnByValue: true,
|
|
828
|
+
});
|
|
829
|
+
await delay(100);
|
|
830
|
+
if (args.screenshot) {
|
|
831
|
+
await cdp.call('Page.enable');
|
|
832
|
+
const shot = await cdp.call('Page.captureScreenshot', { format: 'png', fromSurface: true, captureBeyondViewport: false });
|
|
833
|
+
fs.writeFileSync(path.resolve(args.screenshot), Buffer.from(shot.data, 'base64'));
|
|
834
|
+
}
|
|
835
|
+
if (args.geometryOutput) {
|
|
836
|
+
const geometryResponse = await cdp.call('Runtime.evaluate', {
|
|
837
|
+
expression: `(() => {
|
|
838
|
+
const rect = element => {
|
|
839
|
+
if (!element) return null;
|
|
840
|
+
const value = element.getBoundingClientRect();
|
|
841
|
+
const style = getComputedStyle(element);
|
|
842
|
+
return {
|
|
843
|
+
x: Math.round(value.x * 100) / 100,
|
|
844
|
+
y: Math.round(value.y * 100) / 100,
|
|
845
|
+
width: Math.round(value.width * 100) / 100,
|
|
846
|
+
height: Math.round(value.height * 100) / 100,
|
|
847
|
+
visible: style.display !== 'none' && style.visibility !== 'hidden' && value.width > 0 && value.height > 0,
|
|
848
|
+
overflowX: style.overflowX,
|
|
849
|
+
overflowY: style.overflowY,
|
|
850
|
+
};
|
|
851
|
+
};
|
|
852
|
+
const selectors = {
|
|
853
|
+
rail: '.workbench-rail',
|
|
854
|
+
leftPanel: '.sidebar',
|
|
855
|
+
documentBar: '.workbench-header',
|
|
856
|
+
stage: '.stage',
|
|
857
|
+
canvasTools: '.canvas-tools',
|
|
858
|
+
zoomControls: '.floating-zoom',
|
|
859
|
+
rightPanel: '.inspector',
|
|
860
|
+
activeInspectorPane: '[data-inspector-pane]:not([hidden])',
|
|
861
|
+
reviewFooter: '.review-context-footer',
|
|
862
|
+
};
|
|
863
|
+
const elements = Object.fromEntries(Object.entries(selectors).map(([key, selector]) => [key, rect(document.querySelector(selector))]));
|
|
864
|
+
document.querySelectorAll('[data-screen-card]').forEach((element, index) => { elements['screenCard:' + (element.dataset.screenCard || index)] = rect(element); });
|
|
865
|
+
const cards = Object.entries(elements).filter(([key, value]) => key.startsWith('screenCard:') && value?.visible);
|
|
866
|
+
const cardOverlaps = [];
|
|
867
|
+
for (let first = 0; first < cards.length; first += 1) for (let second = first + 1; second < cards.length; second += 1) {
|
|
868
|
+
const [aKey, a] = cards[first], [bKey, b] = cards[second];
|
|
869
|
+
if (a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y) cardOverlaps.push([aKey, bKey]);
|
|
870
|
+
}
|
|
871
|
+
const controlPairs = [['canvasTools', 'zoomControls'], ['canvasTools', 'rightPanel'], ['zoomControls', 'rightPanel'], ['canvasTools', 'leftPanel'], ['zoomControls', 'leftPanel']];
|
|
872
|
+
const controlOverlaps = controlPairs.filter(([aKey, bKey]) => {
|
|
873
|
+
const a = elements[aKey], b = elements[bKey];
|
|
874
|
+
return a?.visible && b?.visible && a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
|
|
875
|
+
});
|
|
876
|
+
return { version: 2, viewport: { width: innerWidth, height: innerHeight }, view: state.view, screen: state.screen, zoom: state.computedZoom, sidebarOpen: state.sidebarOpen, inspectorOpen: state.inspectorOpen, inspectorTab: state.inspectorTab, elements, overlaps: [...cardOverlaps, ...controlOverlaps], cardOverlaps, controlOverlaps };
|
|
877
|
+
})()`,
|
|
878
|
+
returnByValue: true,
|
|
879
|
+
});
|
|
880
|
+
fs.writeFileSync(path.resolve(args.geometryOutput), JSON.stringify(geometryResponse.result?.value || {}, null, 2) + '\n');
|
|
881
|
+
}
|
|
882
|
+
if (!args.captureOnly) {
|
|
883
|
+
await cdp.call('Page.enable');
|
|
884
|
+
const storageKeyResponse = await cdp.call('Runtime.evaluate', { expression: '({storageKey,reviewBaseRevision})', returnByValue: true });
|
|
885
|
+
const reviewStorageKey = storageKeyResponse.result?.value?.storageKey;
|
|
886
|
+
const baseRevision = storageKeyResponse.result?.value?.reviewBaseRevision;
|
|
887
|
+
if (!reviewStorageKey) throw new Error('Review storage key is unavailable');
|
|
888
|
+
const legacyShellInjection = await cdp.call('Page.addScriptToEvaluateOnNewDocument', {
|
|
889
|
+
source: `localStorage.setItem(${JSON.stringify(reviewStorageKey)}, JSON.stringify({revision:${JSON.stringify(`${baseRevision}-legacy-shell`)},diagnosticsEngineVersion:1,diagnostics:{engineVersion:1,mode:'review',checks:[],summary:{}},runtimeFindings:[{id:'legacy-runtime',category:'runtime-diagnostic',screenId:'legacy',runtimeDiagnosticId:'legacy-check'}],reviewRuns:[{id:'legacy-shell-run',engineVersion:1}],findingDecisions:{'legacy-runtime':'accepted'}}));`,
|
|
890
|
+
});
|
|
891
|
+
await cdp.call('Page.navigate', { url: pathToFileURL(input).href });
|
|
892
|
+
const legacyMigrationDeadline = Date.now() + 15000;
|
|
893
|
+
let legacyShellStateRestored = false;
|
|
894
|
+
while (Date.now() < legacyMigrationDeadline) {
|
|
895
|
+
const migrationResponse = await cdp.call('Runtime.evaluate', {
|
|
896
|
+
expression: `(() => {
|
|
897
|
+
if (document.readyState !== 'complete') return false;
|
|
898
|
+
const stored = JSON.parse(localStorage.getItem(storageKey) || '{}');
|
|
899
|
+
return document.querySelector('.revision-notice')?.hidden === true
|
|
900
|
+
&& state.reviewRuns.some(item => item.id === 'legacy-shell-run')
|
|
901
|
+
&& state.diagnostics === null
|
|
902
|
+
&& !state.runtimeFindings.some(item => item.id === 'legacy-runtime')
|
|
903
|
+
&& !Object.prototype.hasOwnProperty.call(state.findingDecisions, 'legacy-runtime')
|
|
904
|
+
&& stored.targetRevision === reviewTargetRevision
|
|
905
|
+
&& stored.workbenchSchema === workbenchSchema;
|
|
906
|
+
})()`,
|
|
907
|
+
returnByValue: true,
|
|
908
|
+
});
|
|
909
|
+
legacyShellStateRestored = migrationResponse.result?.value === true;
|
|
910
|
+
if (legacyShellStateRestored) break;
|
|
911
|
+
await delay(100);
|
|
912
|
+
}
|
|
913
|
+
await cdp.call('Page.removeScriptToEvaluateOnNewDocument', { identifier: legacyShellInjection.identifier });
|
|
914
|
+
if (!legacyShellStateRestored) throw new Error('Renderer-only review state migration failed');
|
|
915
|
+
const staleInjection = await cdp.call('Page.addScriptToEvaluateOnNewDocument', {
|
|
916
|
+
source: `localStorage.setItem(${JSON.stringify(reviewStorageKey)}, JSON.stringify({revision:'stale-smoke',annotations:[],findingDecisions:{}}));`,
|
|
917
|
+
});
|
|
918
|
+
await cdp.call('Page.navigate', { url: pathToFileURL(input).href });
|
|
919
|
+
const revisionDeadline = Date.now() + 15000;
|
|
920
|
+
let staleNoticeVisible = false;
|
|
921
|
+
while (Date.now() < revisionDeadline) {
|
|
922
|
+
const staleResponse = await cdp.call('Runtime.evaluate', {
|
|
923
|
+
expression: "document.readyState === 'complete' && document.querySelector('.revision-notice')?.hidden === false",
|
|
924
|
+
returnByValue: true,
|
|
925
|
+
});
|
|
926
|
+
staleNoticeVisible = staleResponse.result?.value === true;
|
|
927
|
+
if (staleNoticeVisible) break;
|
|
928
|
+
await delay(100);
|
|
929
|
+
}
|
|
930
|
+
await cdp.call('Page.removeScriptToEvaluateOnNewDocument', { identifier: staleInjection.identifier });
|
|
931
|
+
if (!staleNoticeVisible) throw new Error('Stale revision notice did not appear');
|
|
932
|
+
const stalePreservedResponse = await cdp.call('Runtime.evaluate', {
|
|
933
|
+
expression: `(() => { persist(); return JSON.parse(localStorage.getItem(storageKey) || '{}').revision === 'stale-smoke'; })()`,
|
|
934
|
+
returnByValue: true,
|
|
935
|
+
});
|
|
936
|
+
if (stalePreservedResponse.result?.value !== true) throw new Error('Stale review snapshot was overwritten before a migration decision');
|
|
937
|
+
const resetResponse = await cdp.call('Runtime.evaluate', {
|
|
938
|
+
expression: `(() => {
|
|
939
|
+
document.querySelector('.discard-review-state')?.click();
|
|
940
|
+
const stored = JSON.parse(localStorage.getItem(storageKey) || '{}');
|
|
941
|
+
return document.querySelector('.revision-notice')?.hidden === true && stored.revision !== 'stale-smoke';
|
|
942
|
+
})()`,
|
|
943
|
+
returnByValue: true,
|
|
944
|
+
});
|
|
945
|
+
if (resetResponse.result?.value !== true) throw new Error('Stale revision reset failed');
|
|
946
|
+
}
|
|
947
|
+
if (args.output) fs.writeFileSync(path.resolve(args.output), `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
948
|
+
const summary = report.summary || {};
|
|
949
|
+
console.log(`${args.mode === 'review' ? 'Review' : 'Projection'} diagnostics complete: product-warning=${summary.productWarning ?? summary.warning ?? 0} product-fail=${summary.productFail ?? summary.fail ?? 0} tool-warning=${summary.toolWarning || 0} tool-fail=${summary.toolFail || 0} checks=${report.checks?.length || 0}`);
|
|
950
|
+
if (args.mode === 'review' && !args.captureOnly) console.log(`Review workflow: findings=${workflow.totalAfter || 0} auditLinks=${workflow.auditLinks || 0} selected=${workflow.selected || 0} requestFindings=${workflow.requestFindings || 0} profiles=${fixedProfiles.length}`);
|
|
951
|
+
if (args.failOnFindings && (summary.productFail ?? summary.fail ?? 0) > 0) process.exitCode = 1;
|
|
952
|
+
} finally {
|
|
953
|
+
try { await Promise.race([cdp?.call('Browser.close'), delay(1500)]); } catch (_) {}
|
|
954
|
+
try { socket?.close(); } catch (_) {}
|
|
955
|
+
if (browser.exitCode === null) await Promise.race([
|
|
956
|
+
new Promise(resolve => browser.once('exit', resolve)),
|
|
957
|
+
delay(5000),
|
|
958
|
+
]);
|
|
959
|
+
if (browser.exitCode === null) {
|
|
960
|
+
try { browser.kill('SIGKILL'); } catch (_) {}
|
|
961
|
+
await Promise.race([
|
|
962
|
+
new Promise(resolve => browser.once('exit', resolve)),
|
|
963
|
+
delay(3000),
|
|
964
|
+
]);
|
|
965
|
+
}
|
|
966
|
+
let cleanupError = null;
|
|
967
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
968
|
+
try { fs.rmSync(profile, { recursive: true, force: true }); cleanupError = null; break; }
|
|
969
|
+
catch (error) { cleanupError = error; await delay(200 * (attempt + 1)); }
|
|
970
|
+
}
|
|
971
|
+
if (cleanupError) console.warn(`Temporary browser profile could not be removed: ${cleanupError.message}`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
main().catch(error => {
|
|
976
|
+
console.error(error.message);
|
|
977
|
+
process.exitCode = 2;
|
|
978
|
+
});
|