@veraxhq/verax 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -18
- package/bin/verax.js +7 -0
- package/package.json +15 -5
- package/src/cli/commands/baseline.js +104 -0
- package/src/cli/commands/default.js +323 -111
- package/src/cli/commands/doctor.js +36 -4
- package/src/cli/commands/ga.js +243 -0
- package/src/cli/commands/gates.js +95 -0
- package/src/cli/commands/inspect.js +131 -2
- package/src/cli/commands/release-check.js +213 -0
- package/src/cli/commands/run.js +498 -103
- package/src/cli/commands/security-check.js +211 -0
- package/src/cli/commands/truth.js +114 -0
- package/src/cli/entry.js +305 -68
- package/src/cli/util/angular-component-extractor.js +179 -0
- package/src/cli/util/angular-navigation-detector.js +141 -0
- package/src/cli/util/angular-network-detector.js +161 -0
- package/src/cli/util/angular-state-detector.js +162 -0
- package/src/cli/util/ast-interactive-detector.js +546 -0
- package/src/cli/util/ast-network-detector.js +603 -0
- package/src/cli/util/ast-usestate-detector.js +602 -0
- package/src/cli/util/bootstrap-guard.js +86 -0
- package/src/cli/util/detection-engine.js +4 -3
- package/src/cli/util/determinism-runner.js +123 -0
- package/src/cli/util/determinism-writer.js +129 -0
- package/src/cli/util/env-url.js +4 -0
- package/src/cli/util/events.js +76 -0
- package/src/cli/util/expectation-extractor.js +380 -74
- package/src/cli/util/findings-writer.js +126 -15
- package/src/cli/util/learn-writer.js +3 -1
- package/src/cli/util/observation-engine.js +69 -23
- package/src/cli/util/observe-writer.js +3 -1
- package/src/cli/util/paths.js +6 -14
- package/src/cli/util/project-discovery.js +23 -0
- package/src/cli/util/project-writer.js +3 -1
- package/src/cli/util/redact.js +2 -2
- package/src/cli/util/run-resolver.js +64 -0
- package/src/cli/util/runtime-budget.js +147 -0
- package/src/cli/util/source-requirement.js +55 -0
- package/src/cli/util/summary-writer.js +13 -1
- package/src/cli/util/svelte-navigation-detector.js +163 -0
- package/src/cli/util/svelte-network-detector.js +80 -0
- package/src/cli/util/svelte-sfc-extractor.js +147 -0
- package/src/cli/util/svelte-state-detector.js +243 -0
- package/src/cli/util/vue-navigation-detector.js +177 -0
- package/src/cli/util/vue-sfc-extractor.js +162 -0
- package/src/cli/util/vue-state-detector.js +215 -0
- package/src/types/global.d.ts +28 -0
- package/src/types/ts-ast.d.ts +24 -0
- package/src/verax/cli/doctor.js +2 -2
- package/src/verax/cli/finding-explainer.js +56 -3
- package/src/verax/cli/init.js +1 -1
- package/src/verax/cli/url-safety.js +12 -2
- package/src/verax/cli/wizard.js +13 -2
- package/src/verax/core/artifacts/registry.js +154 -0
- package/src/verax/core/artifacts/verifier.js +980 -0
- package/src/verax/core/baseline/baseline.enforcer.js +137 -0
- package/src/verax/core/baseline/baseline.snapshot.js +231 -0
- package/src/verax/core/budget-engine.js +1 -1
- package/src/verax/core/capabilities/gates.js +499 -0
- package/src/verax/core/capabilities/registry.js +475 -0
- package/src/verax/core/confidence/confidence-compute.js +137 -0
- package/src/verax/core/confidence/confidence-invariants.js +234 -0
- package/src/verax/core/confidence/confidence-report-writer.js +112 -0
- package/src/verax/core/confidence/confidence-weights.js +44 -0
- package/src/verax/core/confidence/confidence.defaults.js +65 -0
- package/src/verax/core/confidence/confidence.loader.js +79 -0
- package/src/verax/core/confidence/confidence.schema.js +94 -0
- package/src/verax/core/confidence-engine-refactor.js +484 -0
- package/src/verax/core/confidence-engine.js +486 -0
- package/src/verax/core/confidence-engine.js.backup +471 -0
- package/src/verax/core/contracts/index.js +29 -0
- package/src/verax/core/contracts/types.js +185 -0
- package/src/verax/core/contracts/validators.js +381 -0
- package/src/verax/core/decision-snapshot.js +31 -4
- package/src/verax/core/decisions/decision.trace.js +276 -0
- package/src/verax/core/determinism/contract-writer.js +89 -0
- package/src/verax/core/determinism/contract.js +139 -0
- package/src/verax/core/determinism/diff.js +364 -0
- package/src/verax/core/determinism/engine.js +221 -0
- package/src/verax/core/determinism/finding-identity.js +148 -0
- package/src/verax/core/determinism/normalize.js +438 -0
- package/src/verax/core/determinism/report-writer.js +92 -0
- package/src/verax/core/determinism/run-fingerprint.js +118 -0
- package/src/verax/core/determinism-model.js +35 -6
- package/src/verax/core/dynamic-route-intelligence.js +528 -0
- package/src/verax/core/evidence/evidence-capture-service.js +307 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
- package/src/verax/core/evidence-builder.js +487 -0
- package/src/verax/core/execution-mode-context.js +77 -0
- package/src/verax/core/execution-mode-detector.js +190 -0
- package/src/verax/core/failures/exit-codes.js +86 -0
- package/src/verax/core/failures/failure-summary.js +76 -0
- package/src/verax/core/failures/failure.factory.js +225 -0
- package/src/verax/core/failures/failure.ledger.js +132 -0
- package/src/verax/core/failures/failure.types.js +196 -0
- package/src/verax/core/failures/index.js +10 -0
- package/src/verax/core/ga/ga-report-writer.js +43 -0
- package/src/verax/core/ga/ga.artifact.js +49 -0
- package/src/verax/core/ga/ga.contract.js +434 -0
- package/src/verax/core/ga/ga.enforcer.js +86 -0
- package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
- package/src/verax/core/guardrails/policy.defaults.js +210 -0
- package/src/verax/core/guardrails/policy.loader.js +83 -0
- package/src/verax/core/guardrails/policy.schema.js +110 -0
- package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
- package/src/verax/core/guardrails-engine.js +505 -0
- package/src/verax/core/incremental-store.js +15 -7
- package/src/verax/core/observe/run-timeline.js +316 -0
- package/src/verax/core/perf/perf.contract.js +186 -0
- package/src/verax/core/perf/perf.display.js +65 -0
- package/src/verax/core/perf/perf.enforcer.js +91 -0
- package/src/verax/core/perf/perf.monitor.js +209 -0
- package/src/verax/core/perf/perf.report.js +198 -0
- package/src/verax/core/pipeline-tracker.js +238 -0
- package/src/verax/core/product-definition.js +127 -0
- package/src/verax/core/release/provenance.builder.js +271 -0
- package/src/verax/core/release/release-report-writer.js +40 -0
- package/src/verax/core/release/release.enforcer.js +159 -0
- package/src/verax/core/release/reproducibility.check.js +221 -0
- package/src/verax/core/release/sbom.builder.js +283 -0
- package/src/verax/core/replay-validator.js +4 -4
- package/src/verax/core/replay.js +1 -1
- package/src/verax/core/report/cross-index.js +192 -0
- package/src/verax/core/report/human-summary.js +222 -0
- package/src/verax/core/route-intelligence.js +419 -0
- package/src/verax/core/security/secrets.scan.js +326 -0
- package/src/verax/core/security/security-report.js +50 -0
- package/src/verax/core/security/security.enforcer.js +124 -0
- package/src/verax/core/security/supplychain.defaults.json +38 -0
- package/src/verax/core/security/supplychain.policy.js +326 -0
- package/src/verax/core/security/vuln.scan.js +265 -0
- package/src/verax/core/silence-impact.js +1 -1
- package/src/verax/core/silence-model.js +9 -7
- package/src/verax/core/truth/truth.certificate.js +250 -0
- package/src/verax/core/ui-feedback-intelligence.js +515 -0
- package/src/verax/detect/comparison.js +8 -3
- package/src/verax/detect/confidence-engine.js +645 -57
- package/src/verax/detect/confidence-helper.js +33 -0
- package/src/verax/detect/detection-engine.js +19 -2
- package/src/verax/detect/dynamic-route-findings.js +335 -0
- package/src/verax/detect/evidence-index.js +15 -65
- package/src/verax/detect/expectation-chain-detector.js +417 -0
- package/src/verax/detect/expectation-model.js +56 -3
- package/src/verax/detect/explanation-helpers.js +1 -1
- package/src/verax/detect/finding-detector.js +2 -2
- package/src/verax/detect/findings-writer.js +149 -20
- package/src/verax/detect/flow-detector.js +4 -4
- package/src/verax/detect/index.js +265 -15
- package/src/verax/detect/interactive-findings.js +3 -4
- package/src/verax/detect/journey-stall-detector.js +558 -0
- package/src/verax/detect/route-findings.js +218 -0
- package/src/verax/detect/signal-mapper.js +2 -2
- package/src/verax/detect/skip-classifier.js +4 -4
- package/src/verax/detect/ui-feedback-findings.js +207 -0
- package/src/verax/detect/verdict-engine.js +61 -9
- package/src/verax/detect/view-switch-correlator.js +242 -0
- package/src/verax/flow/flow-engine.js +3 -2
- package/src/verax/flow/flow-spec.js +1 -2
- package/src/verax/index.js +413 -33
- package/src/verax/intel/effect-detector.js +1 -1
- package/src/verax/intel/index.js +2 -2
- package/src/verax/intel/route-extractor.js +3 -3
- package/src/verax/intel/vue-navigation-extractor.js +81 -18
- package/src/verax/intel/vue-router-extractor.js +4 -2
- package/src/verax/learn/action-contract-extractor.js +684 -66
- package/src/verax/learn/ast-contract-extractor.js +53 -1
- package/src/verax/learn/index.js +36 -2
- package/src/verax/learn/manifest-writer.js +28 -14
- package/src/verax/learn/route-extractor.js +1 -1
- package/src/verax/learn/route-validator.js +12 -8
- package/src/verax/learn/state-extractor.js +1 -1
- package/src/verax/learn/static-extractor-navigation.js +1 -1
- package/src/verax/learn/static-extractor-validation.js +2 -2
- package/src/verax/learn/static-extractor.js +8 -7
- package/src/verax/learn/ts-contract-resolver.js +14 -12
- package/src/verax/observe/browser.js +22 -3
- package/src/verax/observe/console-sensor.js +2 -2
- package/src/verax/observe/expectation-executor.js +2 -1
- package/src/verax/observe/focus-sensor.js +1 -1
- package/src/verax/observe/human-driver.js +29 -10
- package/src/verax/observe/index.js +92 -844
- package/src/verax/observe/interaction-discovery.js +27 -15
- package/src/verax/observe/interaction-runner.js +31 -14
- package/src/verax/observe/loading-sensor.js +6 -0
- package/src/verax/observe/navigation-sensor.js +1 -1
- package/src/verax/observe/observe-context.js +205 -0
- package/src/verax/observe/observe-helpers.js +191 -0
- package/src/verax/observe/observe-runner.js +226 -0
- package/src/verax/observe/observers/budget-observer.js +185 -0
- package/src/verax/observe/observers/console-observer.js +102 -0
- package/src/verax/observe/observers/coverage-observer.js +107 -0
- package/src/verax/observe/observers/interaction-observer.js +471 -0
- package/src/verax/observe/observers/navigation-observer.js +132 -0
- package/src/verax/observe/observers/network-observer.js +87 -0
- package/src/verax/observe/observers/safety-observer.js +82 -0
- package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
- package/src/verax/observe/settle.js +1 -0
- package/src/verax/observe/state-sensor.js +8 -4
- package/src/verax/observe/state-ui-sensor.js +7 -1
- package/src/verax/observe/traces-writer.js +27 -16
- package/src/verax/observe/ui-feedback-detector.js +742 -0
- package/src/verax/observe/ui-signal-sensor.js +155 -2
- package/src/verax/scan-summary-writer.js +46 -9
- package/src/verax/shared/artifact-manager.js +9 -6
- package/src/verax/shared/budget-profiles.js +2 -2
- package/src/verax/shared/caching.js +1 -1
- package/src/verax/shared/config-loader.js +1 -2
- package/src/verax/shared/css-spinner-rules.js +204 -0
- package/src/verax/shared/dynamic-route-utils.js +12 -6
- package/src/verax/shared/retry-policy.js +1 -6
- package/src/verax/shared/root-artifacts.js +1 -1
- package/src/verax/shared/view-switch-rules.js +208 -0
- package/src/verax/shared/zip-artifacts.js +1 -0
- package/src/verax/validate/context-validator.js +1 -1
- package/src/verax/observe/index.js.backup +0 -1
- package/src/verax/validate/context-validator.js.bak +0 -0
|
@@ -2,14 +2,30 @@
|
|
|
2
2
|
* WAVE 3: UI Signal Sensor
|
|
3
3
|
* Detects user-visible feedback signals: loading states, dialogs, error messages
|
|
4
4
|
* Conservative: only count signals with accessibility semantics or explicit attributes
|
|
5
|
+
*
|
|
6
|
+
* CSS SPINNER DETECTION: Detects CSS-only loading indicators without semantic attributes
|
|
5
7
|
*/
|
|
6
8
|
|
|
9
|
+
import {
|
|
10
|
+
isBorderSpinnerPattern,
|
|
11
|
+
isRotationAnimation,
|
|
12
|
+
isPulseAnimation,
|
|
13
|
+
isSpinnerSize,
|
|
14
|
+
isDecorativeElement,
|
|
15
|
+
CSS_SPINNER_REASON_CODES
|
|
16
|
+
} from '../shared/css-spinner-rules.js';
|
|
17
|
+
|
|
7
18
|
export class UISignalSensor {
|
|
8
19
|
/**
|
|
9
20
|
* Snapshot current UI signals on the page.
|
|
10
|
-
* Returns: { hasLoadingIndicator, hasDialog, buttonStateChanged, errorSignals, explanation }
|
|
21
|
+
* Returns: { hasLoadingIndicator, hasDialog, buttonStateChanged, errorSignals, explanation, cssSpinners }
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} page - Playwright page object
|
|
24
|
+
* @param {number} interactionTime - Optional: timestamp of interaction (for timing window)
|
|
25
|
+
* @param {Object} beforeSnapshot - Optional: snapshot before interaction (for correlation)
|
|
26
|
+
* @returns {Promise<Object>} UI signals snapshot
|
|
11
27
|
*/
|
|
12
|
-
async snapshot(page) {
|
|
28
|
+
async snapshot(page, interactionTime = null, beforeSnapshot = null) {
|
|
13
29
|
const signals = await page.evaluate(() => {
|
|
14
30
|
const result = {
|
|
15
31
|
hasLoadingIndicator: false,
|
|
@@ -41,6 +57,7 @@ export class UISignalSensor {
|
|
|
41
57
|
const statusRegions = Array.from(document.querySelectorAll('[role="status"], [role="alert"]'));
|
|
42
58
|
const visibleStatusRegions = statusRegions.filter((el) => {
|
|
43
59
|
const style = window.getComputedStyle(el);
|
|
60
|
+
// @ts-expect-error - offsetParent exists on HTMLElement in browser context
|
|
44
61
|
return el.offsetParent !== null && style.visibility !== 'hidden' && style.display !== 'none' && style.opacity !== '0';
|
|
45
62
|
});
|
|
46
63
|
if (visibleStatusRegions.length > 0) {
|
|
@@ -57,6 +74,7 @@ export class UISignalSensor {
|
|
|
57
74
|
|
|
58
75
|
// Check for dialogs
|
|
59
76
|
const dialog = document.querySelector('[role="dialog"], [aria-modal="true"]');
|
|
77
|
+
// @ts-expect-error - offsetParent exists on HTMLElement in browser context
|
|
60
78
|
if (dialog && dialog.offsetParent !== null) {
|
|
61
79
|
// offsetParent is null if element is hidden
|
|
62
80
|
result.hasDialog = true;
|
|
@@ -83,6 +101,136 @@ export class UISignalSensor {
|
|
|
83
101
|
);
|
|
84
102
|
}
|
|
85
103
|
|
|
104
|
+
// CSS SPINNER DETECTION: Detect CSS-only loading indicators
|
|
105
|
+
result.cssSpinners = [];
|
|
106
|
+
result.cssSpinnerDetected = false;
|
|
107
|
+
|
|
108
|
+
const allElements = Array.from(document.querySelectorAll('*'));
|
|
109
|
+
const currentTime = Date.now();
|
|
110
|
+
const MAX_SPINNER_SIZE = 100;
|
|
111
|
+
const MIN_SPINNER_SIZE = 8;
|
|
112
|
+
const SPINNER_TIMING_WINDOW_MS = 2000;
|
|
113
|
+
|
|
114
|
+
// Helper functions (inlined for browser context)
|
|
115
|
+
const isBorderSpinnerPattern = (style) => {
|
|
116
|
+
const borderWidth = parseFloat(style.borderWidth) || 0;
|
|
117
|
+
const borderTopWidth = parseFloat(style.borderTopWidth) || 0;
|
|
118
|
+
if (borderWidth < 2 && borderTopWidth < 2) return false;
|
|
119
|
+
|
|
120
|
+
const borderColor = style.borderColor || '';
|
|
121
|
+
const borderTopColor = style.borderTopColor || '';
|
|
122
|
+
const hasDifferentTopColor = borderTopColor && borderColor && borderTopColor !== borderColor && borderTopColor !== 'rgba(0, 0, 0, 0)';
|
|
123
|
+
|
|
124
|
+
const borderRadius = style.borderRadius || '';
|
|
125
|
+
const isCircular = borderRadius.includes('50%') || borderRadius.includes('999') || parseFloat(borderRadius) > 20;
|
|
126
|
+
|
|
127
|
+
return hasDifferentTopColor && isCircular;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const isRotationAnimation = (style) => {
|
|
131
|
+
const animationName = (style.animationName || '').toLowerCase();
|
|
132
|
+
const animation = (style.animation || '').toLowerCase();
|
|
133
|
+
const transform = style.transform || '';
|
|
134
|
+
const animationDuration = style.animationDuration || '';
|
|
135
|
+
|
|
136
|
+
const spinnerKeywords = ['spin', 'rotate', 'loading', 'loader'];
|
|
137
|
+
const hasSpinnerKeyword = spinnerKeywords.some(k => animationName.includes(k) || animation.includes(k));
|
|
138
|
+
const hasRotation = transform.includes('rotate');
|
|
139
|
+
const isAnimated = animationDuration && animationDuration !== '0s' && !animationDuration.includes('none');
|
|
140
|
+
|
|
141
|
+
return (hasRotation || hasSpinnerKeyword) && isAnimated;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const isPulseAnimation = (style) => {
|
|
145
|
+
const animationName = (style.animationName || '').toLowerCase();
|
|
146
|
+
const animation = (style.animation || '').toLowerCase();
|
|
147
|
+
const animationDuration = style.animationDuration || '';
|
|
148
|
+
const opacity = parseFloat(style.opacity) || 1;
|
|
149
|
+
|
|
150
|
+
const pulseKeywords = ['pulse', 'fade', 'loading'];
|
|
151
|
+
const hasPulseKeyword = pulseKeywords.some(k => animationName.includes(k) || animation.includes(k));
|
|
152
|
+
const isAnimated = animationDuration && animationDuration !== '0s' && !animationDuration.includes('none');
|
|
153
|
+
const hasOpacityVariation = opacity < 1;
|
|
154
|
+
|
|
155
|
+
return hasPulseKeyword && isAnimated && hasOpacityVariation;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const isSpinnerSize = (width, height) => {
|
|
159
|
+
const maxDim = Math.max(width, height);
|
|
160
|
+
const minDim = Math.min(width, height);
|
|
161
|
+
if (maxDim > MAX_SPINNER_SIZE || minDim < MIN_SPINNER_SIZE) return false;
|
|
162
|
+
const aspectRatio = maxDim / (minDim || 1);
|
|
163
|
+
return aspectRatio <= 2.0;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
for (const el of allElements) {
|
|
167
|
+
const style = window.getComputedStyle(el);
|
|
168
|
+
// @ts-expect-error
|
|
169
|
+
if (el.offsetParent === null || style.visibility === 'hidden' || style.display === 'none' || style.opacity === '0') {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Skip if element has semantic loading attributes
|
|
174
|
+
if (el.hasAttribute('aria-busy') || el.hasAttribute('data-loading') || el.getAttribute('role') === 'progressbar') {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const width = el.offsetWidth || 0;
|
|
179
|
+
const height = el.offsetHeight || 0;
|
|
180
|
+
const elementId = el.id || `${el.tagName}-${el.className}`;
|
|
181
|
+
|
|
182
|
+
// Check size bounds
|
|
183
|
+
if (!isSpinnerSize(width, height)) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check for spinner patterns
|
|
188
|
+
let spinnerType = null;
|
|
189
|
+
let reasonCode = null;
|
|
190
|
+
|
|
191
|
+
if (isBorderSpinnerPattern(style)) {
|
|
192
|
+
spinnerType = 'border-spinner';
|
|
193
|
+
reasonCode = 'UI_CSS_SPINNER_DETECTED_BORDER';
|
|
194
|
+
} else if (isRotationAnimation(style)) {
|
|
195
|
+
spinnerType = 'rotation-spinner';
|
|
196
|
+
reasonCode = 'UI_CSS_SPINNER_DETECTED_ROTATION';
|
|
197
|
+
} else if (isPulseAnimation(style)) {
|
|
198
|
+
spinnerType = 'pulse-spinner';
|
|
199
|
+
reasonCode = 'UI_CSS_SPINNER_DETECTED_PULSE';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (spinnerType) {
|
|
203
|
+
result.cssSpinners.push({
|
|
204
|
+
type: spinnerType,
|
|
205
|
+
reasonCode,
|
|
206
|
+
elementId,
|
|
207
|
+
width,
|
|
208
|
+
height
|
|
209
|
+
});
|
|
210
|
+
result.cssSpinnerDetected = true;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Require 2+ independent signals for CONFIRMED CSS spinner feedback
|
|
215
|
+
if (result.cssSpinnerDetected) {
|
|
216
|
+
const hasDisabledButton = result.disabledElements.length > 0;
|
|
217
|
+
const hasPointerEventsDisabled = allElements.some(el => {
|
|
218
|
+
const style = window.getComputedStyle(el);
|
|
219
|
+
return style.pointerEvents === 'none';
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const corroboratingSignals = [hasDisabledButton, hasPointerEventsDisabled].filter(Boolean).length;
|
|
223
|
+
|
|
224
|
+
if (corroboratingSignals >= 1) {
|
|
225
|
+
result.hasLoadingIndicator = true;
|
|
226
|
+
result.explanation.push(`CSS spinner detected with ${corroboratingSignals} corroborating signal(s)`);
|
|
227
|
+
result.cssSpinnerReasonCode = 'UI_CSS_SPINNER_ACCEPTED_WITH_CORROBORATION';
|
|
228
|
+
} else {
|
|
229
|
+
result.explanation.push('CSS spinner detected but no corroborating signals (SUSPECTED)');
|
|
230
|
+
result.cssSpinnerReasonCode = 'UI_CSS_SPINNER_REJECTED_NO_CORROBORATION';
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
86
234
|
// VALIDATION INTELLIGENCE v1: Detect visible validation feedback
|
|
87
235
|
// Check for aria-invalid="true" with visible error text nearby
|
|
88
236
|
const invalidElements = Array.from(document.querySelectorAll('[aria-invalid="true"]'));
|
|
@@ -90,6 +238,7 @@ export class UISignalSensor {
|
|
|
90
238
|
|
|
91
239
|
for (const invalidEl of invalidElements) {
|
|
92
240
|
const style = window.getComputedStyle(invalidEl);
|
|
241
|
+
// @ts-expect-error - offsetParent exists on HTMLElement in browser context
|
|
93
242
|
const isVisible = invalidEl.offsetParent !== null &&
|
|
94
243
|
style.visibility !== 'hidden' &&
|
|
95
244
|
style.display !== 'none' &&
|
|
@@ -120,6 +269,7 @@ export class UISignalSensor {
|
|
|
120
269
|
const errorText = Array.from(parent.querySelectorAll('[role="alert"], .error, .invalid-feedback'))
|
|
121
270
|
.find(el => {
|
|
122
271
|
const elStyle = window.getComputedStyle(el);
|
|
272
|
+
// @ts-expect-error - offsetParent exists on HTMLElement in browser context
|
|
123
273
|
return el.offsetParent !== null &&
|
|
124
274
|
elStyle.visibility !== 'hidden' &&
|
|
125
275
|
elStyle.display !== 'none' &&
|
|
@@ -138,6 +288,7 @@ export class UISignalSensor {
|
|
|
138
288
|
const alertRegions = Array.from(document.querySelectorAll('[role="alert"], [role="status"]'));
|
|
139
289
|
const visibleAlertRegions = alertRegions.filter((el) => {
|
|
140
290
|
const style = window.getComputedStyle(el);
|
|
291
|
+
// @ts-expect-error - offsetParent exists on HTMLElement in browser context
|
|
141
292
|
const isVisible = el.offsetParent !== null &&
|
|
142
293
|
style.visibility !== 'hidden' &&
|
|
143
294
|
style.display !== 'none' &&
|
|
@@ -149,6 +300,7 @@ export class UISignalSensor {
|
|
|
149
300
|
const liveRegions = Array.from(document.querySelectorAll('[aria-live]'));
|
|
150
301
|
const visibleLiveRegions = liveRegions.filter((el) => {
|
|
151
302
|
const style = window.getComputedStyle(el);
|
|
303
|
+
// @ts-expect-error - offsetParent exists on HTMLElement in browser context
|
|
152
304
|
const isVisible = el.offsetParent !== null &&
|
|
153
305
|
style.visibility !== 'hidden' &&
|
|
154
306
|
style.display !== 'none' &&
|
|
@@ -176,6 +328,7 @@ export class UISignalSensor {
|
|
|
176
328
|
'[role="alert"], [class*="error"], [class*="danger"]'
|
|
177
329
|
);
|
|
178
330
|
if (errorMessages.length > 0) {
|
|
331
|
+
// @ts-expect-error - NodeListOf is iterable in browser context
|
|
179
332
|
for (const elem of errorMessages) {
|
|
180
333
|
const text = elem.textContent.trim().slice(0, 50);
|
|
181
334
|
if (text && (text.toLowerCase().includes('error') || text.toLowerCase().includes('fail'))) {
|
|
@@ -3,9 +3,15 @@ import { writeFileSync, mkdirSync, readFileSync, existsSync } from 'fs';
|
|
|
3
3
|
import { computeExpectationsSummary } from './shared/artifact-manager.js';
|
|
4
4
|
import { createImpactSummary } from './core/silence-impact.js';
|
|
5
5
|
import { computeDecisionSnapshot } from './core/decision-snapshot.js';
|
|
6
|
+
import { VERAX_PRODUCT_DEFINITION } from './core/product-definition.js';
|
|
7
|
+
import { ARTIFACT_REGISTRY, getArtifactVersions } from './core/artifacts/registry.js';
|
|
8
|
+
import { generateHumanSummary } from './core/report/human-summary.js';
|
|
6
9
|
|
|
7
|
-
export function writeScanSummary(projectDir, url, projectType, learnTruth, observeTruth, detectTruth, manifestPath, tracesPath, findingsPath, runDirOpt
|
|
8
|
-
|
|
10
|
+
export async function writeScanSummary(projectDir, url, projectType, learnTruth, observeTruth, detectTruth, manifestPath, tracesPath, findingsPath, runDirOpt, findingsArray = null) {
|
|
11
|
+
if (!runDirOpt) {
|
|
12
|
+
throw new Error('runDirOpt is required');
|
|
13
|
+
}
|
|
14
|
+
const scanDir = resolve(runDirOpt);
|
|
9
15
|
mkdirSync(scanDir, { recursive: true });
|
|
10
16
|
|
|
11
17
|
// Compute expectations summary from manifest
|
|
@@ -26,20 +32,30 @@ export function writeScanSummary(projectDir, url, projectType, learnTruth, obser
|
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
// PHASE 6: Compute determinism summary from decisions.json
|
|
35
|
+
// PHASE 21.2: Use HARD verdict from determinism contract
|
|
29
36
|
let determinismSummary = null;
|
|
30
37
|
if (runDirOpt && observeTruth?.runId) {
|
|
31
38
|
const decisionsPath = resolve(runDirOpt, 'decisions.json');
|
|
32
39
|
if (existsSync(decisionsPath)) {
|
|
33
40
|
try {
|
|
34
41
|
const decisions = JSON.parse(readFileSync(decisionsPath, 'utf-8'));
|
|
35
|
-
const { DecisionRecorder } =
|
|
42
|
+
const { DecisionRecorder } = await import('./core/determinism-model.js');
|
|
36
43
|
const recorder = DecisionRecorder.fromExport(decisions);
|
|
37
44
|
const summary = recorder.getSummary();
|
|
38
45
|
|
|
46
|
+
// PHASE 21.2: Compute HARD verdict from adaptive events
|
|
47
|
+
const { computeDeterminismVerdict } = await import('./core/determinism/contract.js');
|
|
48
|
+
const verdict = computeDeterminismVerdict(recorder);
|
|
49
|
+
|
|
39
50
|
determinismSummary = {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
verdict: verdict.verdict, // PHASE 21.2: HARD verdict (DETERMINISTIC or NON_DETERMINISTIC)
|
|
52
|
+
message: verdict.message,
|
|
53
|
+
reasons: verdict.reasons,
|
|
54
|
+
adaptiveEventsCount: verdict.adaptiveEvents.length,
|
|
55
|
+
// Legacy fields for backward compatibility
|
|
56
|
+
isDeterministic: verdict.verdict === 'DETERMINISTIC',
|
|
57
|
+
totalDecisions: summary.total,
|
|
58
|
+
decisionsByCategory: summary.byCategory,
|
|
43
59
|
decisionsPath: decisionsPath
|
|
44
60
|
};
|
|
45
61
|
} catch (error) {
|
|
@@ -55,19 +71,37 @@ export function writeScanSummary(projectDir, url, projectType, learnTruth, obser
|
|
|
55
71
|
decisionSnapshot = computeDecisionSnapshot(findingsArray, detectTruth, observeTruth, silences);
|
|
56
72
|
}
|
|
57
73
|
|
|
74
|
+
// PHASE 21.10: Generate human summary
|
|
75
|
+
let humanSummary = null;
|
|
76
|
+
if (observeTruth?.runId) {
|
|
77
|
+
try {
|
|
78
|
+
humanSummary = await generateHumanSummary(projectDir, observeTruth.runId);
|
|
79
|
+
} catch {
|
|
80
|
+
// Ignore errors generating human summary
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
58
84
|
const summary = {
|
|
59
85
|
version: 1,
|
|
86
|
+
contractVersion: 1, // PHASE 0: Track schema changes
|
|
60
87
|
scannedAt: new Date().toISOString(),
|
|
61
88
|
url: url,
|
|
62
89
|
projectType: projectType,
|
|
63
90
|
expectationsSummary: expectationsSummary,
|
|
91
|
+
// PHASE 0: Include Evidence Law statement
|
|
92
|
+
evidenceLaw: {
|
|
93
|
+
statement: VERAX_PRODUCT_DEFINITION.evidenceLaw.statement,
|
|
94
|
+
description: VERAX_PRODUCT_DEFINITION.evidenceLaw.definition,
|
|
95
|
+
enforcement: VERAX_PRODUCT_DEFINITION.evidenceLaw.enforcement
|
|
96
|
+
},
|
|
64
97
|
// PHASE 7: Decision snapshot first (most important for human decision-making)
|
|
65
98
|
decisionSnapshot: decisionSnapshot,
|
|
66
99
|
// PHASE 7: Misinterpretation guards (explicit warnings)
|
|
67
100
|
interpretationGuards: {
|
|
68
101
|
zeroFindings: 'Zero findings does NOT mean no problems. Check unverified count and confidence level.',
|
|
69
102
|
deterministicRun: 'Deterministic run does NOT mean correct site. Only means scan was reproducible.',
|
|
70
|
-
highSilenceImpact: 'High silence impact does NOT mean failures exist. Only means unknowns affect confidence.'
|
|
103
|
+
highSilenceImpact: 'High silence impact does NOT mean failures exist. Only means unknowns affect confidence.',
|
|
104
|
+
evidenceLaw: 'Not all findings are CONFIRMED. Some may be SUSPECTED (insufficient evidence). Only CONFIRMED findings are actionable.'
|
|
71
105
|
},
|
|
72
106
|
truth: {
|
|
73
107
|
learn: learnTruth,
|
|
@@ -85,14 +119,17 @@ export function writeScanSummary(projectDir, url, projectType, learnTruth, obser
|
|
|
85
119
|
} : null,
|
|
86
120
|
// PHASE 6: Add determinism summary
|
|
87
121
|
determinism: determinismSummary,
|
|
122
|
+
// PHASE 21.10: Add human summary
|
|
123
|
+
humanSummary: humanSummary,
|
|
88
124
|
paths: {
|
|
89
125
|
manifest: manifestPath,
|
|
90
126
|
traces: tracesPath,
|
|
91
127
|
findings: findingsPath
|
|
92
|
-
}
|
|
128
|
+
},
|
|
129
|
+
artifactVersions: getArtifactVersions()
|
|
93
130
|
};
|
|
94
131
|
|
|
95
|
-
const summaryPath = resolve(scanDir,
|
|
132
|
+
const summaryPath = resolve(scanDir, ARTIFACT_REGISTRY.scanSummary.filename);
|
|
96
133
|
writeFileSync(summaryPath, JSON.stringify(summary, null, 2) + '\n');
|
|
97
134
|
|
|
98
135
|
return {
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { existsSync, mkdirSync, writeFileSync, appendFileSync } from 'fs';
|
|
16
|
-
import { resolve
|
|
16
|
+
import { resolve } from 'path';
|
|
17
17
|
import { randomBytes } from 'crypto';
|
|
18
|
+
import { buildRunArtifactPaths } from '../core/artifacts/registry.js';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Generate a unique run ID.
|
|
@@ -33,17 +34,19 @@ export function generateRunId() {
|
|
|
33
34
|
export function initArtifactPaths(projectRoot, runId = null) {
|
|
34
35
|
const id = runId || generateRunId();
|
|
35
36
|
const runDir = resolve(projectRoot, '.verax', 'runs', id);
|
|
37
|
+
const registryPaths = buildRunArtifactPaths(runDir);
|
|
36
38
|
|
|
37
39
|
const paths = {
|
|
38
40
|
runId: id,
|
|
39
41
|
runDir,
|
|
40
|
-
summary:
|
|
41
|
-
findings:
|
|
42
|
+
summary: registryPaths.summaryJson,
|
|
43
|
+
findings: registryPaths.findingsJson,
|
|
42
44
|
expectations: resolve(runDir, 'expectations.json'),
|
|
43
|
-
traces:
|
|
44
|
-
evidence:
|
|
45
|
+
traces: registryPaths.tracesJsonl,
|
|
46
|
+
evidence: registryPaths.evidenceDir,
|
|
45
47
|
flows: resolve(runDir, 'flows'),
|
|
46
|
-
artifacts: resolve(projectRoot, '.verax', 'artifacts') // Legacy compat
|
|
48
|
+
artifacts: resolve(projectRoot, '.verax', 'artifacts'), // Legacy compat
|
|
49
|
+
registry: registryPaths.artifactVersions
|
|
47
50
|
};
|
|
48
51
|
|
|
49
52
|
// Create directories
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - EXHAUSTIVE: 300 seconds, maximum coverage (deep audit)
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { createScanBudget } from './scan-budget.js';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* QUICK profile: Fast feedback for development
|
|
@@ -107,7 +107,7 @@ export function getActiveBudgetProfile() {
|
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
109
|
* Create a scan budget with the active profile applied.
|
|
110
|
-
* @returns {
|
|
110
|
+
* @returns {Object} Complete scan budget with profile applied
|
|
111
111
|
*/
|
|
112
112
|
export function createScanBudgetWithProfile() {
|
|
113
113
|
const profile = getActiveBudgetProfile();
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS SPINNER DETECTION RULES
|
|
3
|
+
*
|
|
4
|
+
* Truth boundary for detecting CSS-only loading indicators (spinners)
|
|
5
|
+
* without semantic attributes (aria-busy, data-loading, role).
|
|
6
|
+
*
|
|
7
|
+
* Hard rules encoded as constants and predicates. No prose, only code.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Reason codes for CSS spinner detection decisions
|
|
12
|
+
*/
|
|
13
|
+
export const CSS_SPINNER_REASON_CODES = {
|
|
14
|
+
DETECTED_BORDER_SPINNER: 'UI_CSS_SPINNER_DETECTED_BORDER',
|
|
15
|
+
DETECTED_ROTATION_ANIMATION: 'UI_CSS_SPINNER_DETECTED_ROTATION',
|
|
16
|
+
DETECTED_PULSE_ANIMATION: 'UI_CSS_SPINNER_DETECTED_PULSE',
|
|
17
|
+
REJECTED_DECORATIVE: 'UI_CSS_SPINNER_REJECTED_DECORATIVE',
|
|
18
|
+
REJECTED_TOO_LARGE: 'UI_CSS_SPINNER_REJECTED_TOO_LARGE',
|
|
19
|
+
REJECTED_ALWAYS_PRESENT: 'UI_CSS_SPINNER_REJECTED_ALWAYS_PRESENT',
|
|
20
|
+
REJECTED_NO_CORROBORATION: 'UI_CSS_SPINNER_REJECTED_NO_CORROBORATION',
|
|
21
|
+
REJECTED_TIMING_WINDOW: 'UI_CSS_SPINNER_TIMED',
|
|
22
|
+
ACCEPTED_WITH_CORROBORATION: 'UI_CSS_SPINNER_ACCEPTED_WITH_CORROBORATION'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Maximum size for a spinner element (in pixels)
|
|
27
|
+
* Larger elements are likely decorative, not loading indicators
|
|
28
|
+
*/
|
|
29
|
+
export const MAX_SPINNER_SIZE = 100; // pixels (width or height)
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Minimum size for a spinner element (in pixels)
|
|
33
|
+
* Very small elements might be decorative dots
|
|
34
|
+
*/
|
|
35
|
+
export const MIN_SPINNER_SIZE = 8; // pixels
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Timing window for spinner appearance (milliseconds)
|
|
39
|
+
* Spinner must appear within this window after interaction to count as loading feedback
|
|
40
|
+
*/
|
|
41
|
+
export const SPINNER_TIMING_WINDOW_MS = 2000; // 2 seconds
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if an element has border-based spinner pattern
|
|
45
|
+
* Pattern: border + border-top (or border-left) with different color, creating circular spinner
|
|
46
|
+
*
|
|
47
|
+
* @param {Object} computedStyle - Computed style object
|
|
48
|
+
* @returns {boolean}
|
|
49
|
+
*/
|
|
50
|
+
export function isBorderSpinnerPattern(computedStyle) {
|
|
51
|
+
if (!computedStyle) return false;
|
|
52
|
+
|
|
53
|
+
const borderWidth = parseFloat(computedStyle.borderWidth) || 0;
|
|
54
|
+
const borderTopWidth = parseFloat(computedStyle.borderTopWidth) || 0;
|
|
55
|
+
const borderLeftWidth = parseFloat(computedStyle.borderLeftWidth) || 0;
|
|
56
|
+
|
|
57
|
+
// Must have border
|
|
58
|
+
if (borderWidth < 2 && borderTopWidth < 2 && borderLeftWidth < 2) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check for different border colors (spinner pattern)
|
|
63
|
+
const borderColor = computedStyle.borderColor || '';
|
|
64
|
+
const borderTopColor = computedStyle.borderTopColor || '';
|
|
65
|
+
const borderLeftColor = computedStyle.borderLeftColor || '';
|
|
66
|
+
|
|
67
|
+
// Border-top or border-left should have different color than main border
|
|
68
|
+
const hasDifferentTopColor = borderTopColor && borderColor && borderTopColor !== borderColor && borderTopColor !== 'rgba(0, 0, 0, 0)';
|
|
69
|
+
const hasDifferentLeftColor = borderLeftColor && borderColor && borderLeftColor !== borderColor && borderLeftColor !== 'rgba(0, 0, 0, 0)';
|
|
70
|
+
|
|
71
|
+
// Check for circular shape (border-radius >= 50%)
|
|
72
|
+
const borderRadius = computedStyle.borderRadius || '';
|
|
73
|
+
const isCircular = borderRadius.includes('50%') || borderRadius.includes('999') || parseFloat(borderRadius) > 20;
|
|
74
|
+
|
|
75
|
+
return (hasDifferentTopColor || hasDifferentLeftColor) && isCircular;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if an element has rotation animation
|
|
80
|
+
* Pattern: animation-name includes rotation, or transform: rotate() in keyframes
|
|
81
|
+
*
|
|
82
|
+
* @param {Object} computedStyle - Computed style object
|
|
83
|
+
* @param {Object} element - DOM element
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
export function isRotationAnimation(computedStyle, element) {
|
|
87
|
+
if (!computedStyle || !element) return false;
|
|
88
|
+
|
|
89
|
+
const animationName = computedStyle.animationName || '';
|
|
90
|
+
const animation = computedStyle.animation || '';
|
|
91
|
+
|
|
92
|
+
// Check animation name for spinner-related keywords (but don't rely only on names)
|
|
93
|
+
const spinnerKeywords = ['spin', 'rotate', 'loading', 'loader'];
|
|
94
|
+
const hasSpinnerKeyword = spinnerKeywords.some(keyword =>
|
|
95
|
+
animationName.toLowerCase().includes(keyword) || animation.toLowerCase().includes(keyword)
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Check for transform: rotate() in computed style (indicates rotation)
|
|
99
|
+
const transform = computedStyle.transform || '';
|
|
100
|
+
const hasRotation = transform.includes('rotate');
|
|
101
|
+
|
|
102
|
+
// Check for animation-duration (must be animated)
|
|
103
|
+
const animationDuration = computedStyle.animationDuration || '';
|
|
104
|
+
const isAnimated = animationDuration && animationDuration !== '0s' && !animationDuration.includes('none');
|
|
105
|
+
|
|
106
|
+
// Must have rotation AND be animated
|
|
107
|
+
return (hasRotation || hasSpinnerKeyword) && isAnimated;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check if an element has pulse animation (opacity or scale pulsing)
|
|
112
|
+
* Pattern: animation that changes opacity or scale repeatedly
|
|
113
|
+
*
|
|
114
|
+
* @param {Object} computedStyle - Computed style object
|
|
115
|
+
* @returns {boolean}
|
|
116
|
+
*/
|
|
117
|
+
export function isPulseAnimation(computedStyle) {
|
|
118
|
+
if (!computedStyle) return false;
|
|
119
|
+
|
|
120
|
+
const animationName = computedStyle.animationName || '';
|
|
121
|
+
const animation = computedStyle.animation || '';
|
|
122
|
+
|
|
123
|
+
// Check for pulse-related keywords
|
|
124
|
+
const pulseKeywords = ['pulse', 'fade', 'loading'];
|
|
125
|
+
const hasPulseKeyword = pulseKeywords.some(keyword =>
|
|
126
|
+
animationName.toLowerCase().includes(keyword) || animation.toLowerCase().includes(keyword)
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Check for animation-duration
|
|
130
|
+
const animationDuration = computedStyle.animationDuration || '';
|
|
131
|
+
const isAnimated = animationDuration && animationDuration !== '0s' && !animationDuration.includes('none');
|
|
132
|
+
|
|
133
|
+
// Check for opacity animation (common in pulse patterns)
|
|
134
|
+
const opacity = parseFloat(computedStyle.opacity) || 1;
|
|
135
|
+
const hasOpacityVariation = opacity < 1; // Partially transparent suggests pulsing
|
|
136
|
+
|
|
137
|
+
return hasPulseKeyword && isAnimated && hasOpacityVariation;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if element size is within spinner bounds
|
|
142
|
+
*
|
|
143
|
+
* @param {number} width - Element width in pixels
|
|
144
|
+
* @param {number} height - Element height in pixels
|
|
145
|
+
* @returns {boolean}
|
|
146
|
+
*/
|
|
147
|
+
export function isSpinnerSize(width, height) {
|
|
148
|
+
const maxDim = Math.max(width, height);
|
|
149
|
+
const minDim = Math.min(width, height);
|
|
150
|
+
|
|
151
|
+
// Must be within size bounds
|
|
152
|
+
if (maxDim > MAX_SPINNER_SIZE || minDim < MIN_SPINNER_SIZE) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Should be roughly square (spinners are usually circular/square)
|
|
157
|
+
const aspectRatio = maxDim / (minDim || 1);
|
|
158
|
+
return aspectRatio <= 2.0; // Allow some rectangular tolerance
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Check if spinner appeared within timing window
|
|
163
|
+
*
|
|
164
|
+
* @param {number} appearanceTime - Time when spinner appeared (ms since epoch)
|
|
165
|
+
* @param {number} interactionTime - Time when interaction occurred (ms since epoch)
|
|
166
|
+
* @returns {boolean}
|
|
167
|
+
*/
|
|
168
|
+
export function isWithinTimingWindow(appearanceTime, interactionTime) {
|
|
169
|
+
if (!appearanceTime || !interactionTime) return false;
|
|
170
|
+
const timeDiff = appearanceTime - interactionTime;
|
|
171
|
+
return timeDiff >= 0 && timeDiff <= SPINNER_TIMING_WINDOW_MS;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check if element is likely decorative (always present, large, not near interaction)
|
|
176
|
+
*
|
|
177
|
+
* @param {Object} element - DOM element
|
|
178
|
+
* @param {Object} computedStyle - Computed style object
|
|
179
|
+
* @param {boolean} wasPresentBefore - Whether element existed before interaction
|
|
180
|
+
* @returns {boolean}
|
|
181
|
+
*/
|
|
182
|
+
export function isDecorativeElement(element, computedStyle, wasPresentBefore) {
|
|
183
|
+
if (!element || !computedStyle) return false;
|
|
184
|
+
|
|
185
|
+
// If element was always present, likely decorative
|
|
186
|
+
if (wasPresentBefore) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check size
|
|
191
|
+
const width = element.offsetWidth || 0;
|
|
192
|
+
const height = element.offsetHeight || 0;
|
|
193
|
+
if (!isSpinnerSize(width, height)) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check if element is very large (likely decorative)
|
|
198
|
+
if (width > MAX_SPINNER_SIZE || height > MAX_SPINNER_SIZE) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
|
|
@@ -88,23 +88,27 @@ export function createExamplePath(originalPath) {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
let examplePath = originalPath;
|
|
91
|
-
let
|
|
91
|
+
let _isDynamic = false;
|
|
92
|
+
const parameters = new Set();
|
|
92
93
|
|
|
93
94
|
// Replace React/Vue :param
|
|
94
95
|
examplePath = examplePath.replace(/:(\w+)/g, (match, paramName) => {
|
|
95
|
-
|
|
96
|
+
_isDynamic = true;
|
|
97
|
+
parameters.add(paramName);
|
|
96
98
|
return getExampleValue(paramName);
|
|
97
99
|
});
|
|
98
100
|
|
|
99
101
|
// Replace Next.js [param]
|
|
100
102
|
examplePath = examplePath.replace(/\[(\w+)\]/g, (match, paramName) => {
|
|
101
|
-
|
|
103
|
+
_isDynamic = true;
|
|
104
|
+
parameters.add(paramName);
|
|
102
105
|
return getExampleValue(paramName);
|
|
103
106
|
});
|
|
104
107
|
|
|
105
108
|
// Replace template ${param}
|
|
106
109
|
examplePath = examplePath.replace(/\$\{(\w+)\}/g, (match, paramName) => {
|
|
107
|
-
|
|
110
|
+
_isDynamic = true;
|
|
111
|
+
parameters.add(paramName);
|
|
108
112
|
return getExampleValue(paramName);
|
|
109
113
|
});
|
|
110
114
|
|
|
@@ -112,7 +116,8 @@ export function createExamplePath(originalPath) {
|
|
|
112
116
|
examplePath,
|
|
113
117
|
originalPattern: originalPath,
|
|
114
118
|
isDynamic: true,
|
|
115
|
-
exampleExecution: true
|
|
119
|
+
exampleExecution: true,
|
|
120
|
+
parameters: Array.from(parameters)
|
|
116
121
|
};
|
|
117
122
|
}
|
|
118
123
|
|
|
@@ -210,7 +215,8 @@ export function normalizeTemplateLiteral(template) {
|
|
|
210
215
|
originalPattern: result.originalTarget,
|
|
211
216
|
examplePath: result.exampleTarget,
|
|
212
217
|
isDynamic: true,
|
|
213
|
-
exampleExecution: true
|
|
218
|
+
exampleExecution: true,
|
|
219
|
+
parameters: result.parameters
|
|
214
220
|
};
|
|
215
221
|
}
|
|
216
222
|
|