playwright-intelligence-engine 0.0.0-stage → 2.0.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/LICENSE +191 -0
- package/README.md +229 -3
- package/bin/pw-intel.js +37 -0
- package/dist/authoring/author.d.ts +83 -0
- package/dist/authoring/author.d.ts.map +1 -0
- package/dist/authoring/generation-contract.d.ts +90 -0
- package/dist/authoring/generation-contract.d.ts.map +1 -0
- package/dist/authoring/hybrid-author.d.ts +6 -0
- package/dist/authoring/hybrid-author.d.ts.map +1 -0
- package/dist/authoring/index.d.ts +7 -0
- package/dist/authoring/index.d.ts.map +1 -0
- package/dist/authoring/llm-author.d.ts +28 -0
- package/dist/authoring/llm-author.d.ts.map +1 -0
- package/dist/authoring/naming/index.d.ts +4 -0
- package/dist/authoring/naming/index.d.ts.map +1 -0
- package/dist/authoring/naming/name-plan.d.ts +45 -0
- package/dist/authoring/naming/name-plan.d.ts.map +1 -0
- package/dist/authoring/naming/name-planner.d.ts +25 -0
- package/dist/authoring/naming/name-planner.d.ts.map +1 -0
- package/dist/authoring/naming/naming-linter.d.ts +20 -0
- package/dist/authoring/naming/naming-linter.d.ts.map +1 -0
- package/dist/authoring/template-author.d.ts +6 -0
- package/dist/authoring/template-author.d.ts.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/collectors/collector-interface.d.ts +7 -0
- package/dist/collectors/collector-interface.d.ts.map +1 -0
- package/dist/collectors/environment-collector.d.ts +12 -0
- package/dist/collectors/environment-collector.d.ts.map +1 -0
- package/dist/collectors/error-collector.d.ts +18 -0
- package/dist/collectors/error-collector.d.ts.map +1 -0
- package/dist/collectors/git-diff-collector.d.ts +20 -0
- package/dist/collectors/git-diff-collector.d.ts.map +1 -0
- package/dist/collectors/live-browser-collector.d.ts +45 -0
- package/dist/collectors/live-browser-collector.d.ts.map +1 -0
- package/dist/collectors/network-collector.d.ts +28 -0
- package/dist/collectors/network-collector.d.ts.map +1 -0
- package/dist/collectors/static-ast-collector.d.ts +32 -0
- package/dist/collectors/static-ast-collector.d.ts.map +1 -0
- package/dist/collectors/trace-collector.d.ts +12 -0
- package/dist/collectors/trace-collector.d.ts.map +1 -0
- package/dist/config/project-config.d.ts +33 -0
- package/dist/config/project-config.d.ts.map +1 -0
- package/dist/core/errors/engine-errors.d.ts +34 -0
- package/dist/core/errors/engine-errors.d.ts.map +1 -0
- package/dist/core/types/diagnostic-context.d.ts +151 -0
- package/dist/core/types/diagnostic-context.d.ts.map +1 -0
- package/dist/core/types/repair-evidence.d.ts +56 -0
- package/dist/core/types/repair-evidence.d.ts.map +1 -0
- package/dist/core/version.d.ts +13 -0
- package/dist/core/version.d.ts.map +1 -0
- package/dist/diagnostics/diagnostic-contract.d.ts +52 -0
- package/dist/diagnostics/diagnostic-contract.d.ts.map +1 -0
- package/dist/diagnostics/diagnostic-report.d.ts +11 -0
- package/dist/diagnostics/diagnostic-report.d.ts.map +1 -0
- package/dist/diagnostics/evidence-correlator.d.ts +22 -0
- package/dist/diagnostics/evidence-correlator.d.ts.map +1 -0
- package/dist/diagnostics/external-failure-evidence.d.ts +18 -0
- package/dist/diagnostics/external-failure-evidence.d.ts.map +1 -0
- package/dist/diagnostics/failure-category.d.ts +4 -0
- package/dist/diagnostics/failure-category.d.ts.map +1 -0
- package/dist/diagnostics/failure-classifier.d.ts +58 -0
- package/dist/diagnostics/failure-classifier.d.ts.map +1 -0
- package/dist/evaluation/evaluation-scorecard.d.ts +72 -0
- package/dist/evaluation/evaluation-scorecard.d.ts.map +1 -0
- package/dist/evaluation/repository-inspector.d.ts +21 -0
- package/dist/evaluation/repository-inspector.d.ts.map +1 -0
- package/dist/evaluation/repository-runner.d.ts +24 -0
- package/dist/evaluation/repository-runner.d.ts.map +1 -0
- package/dist/evidence/evidence-model.d.ts +88 -0
- package/dist/evidence/evidence-model.d.ts.map +1 -0
- package/dist/evidence/evidence-normalizer.d.ts +19 -0
- package/dist/evidence/evidence-normalizer.d.ts.map +1 -0
- package/dist/evidence/evidence-redactor.d.ts +37 -0
- package/dist/evidence/evidence-redactor.d.ts.map +1 -0
- package/dist/framework/config-adapter.d.ts +28 -0
- package/dist/framework/config-adapter.d.ts.map +1 -0
- package/dist/gate/index.d.ts +21 -0
- package/dist/gate/index.d.ts.map +1 -0
- package/dist/gate/oracle.d.ts +26 -0
- package/dist/gate/oracle.d.ts.map +1 -0
- package/dist/gate/quality.d.ts +27 -0
- package/dist/gate/quality.d.ts.map +1 -0
- package/dist/gate/safety.d.ts +28 -0
- package/dist/gate/safety.d.ts.map +1 -0
- package/dist/generator/architecture-generator.d.ts +55 -0
- package/dist/generator/architecture-generator.d.ts.map +1 -0
- package/dist/generator/assertion-quality-gate.d.ts +18 -0
- package/dist/generator/assertion-quality-gate.d.ts.map +1 -0
- package/dist/generator/candidate-writer.d.ts +34 -0
- package/dist/generator/candidate-writer.d.ts.map +1 -0
- package/dist/generator/test-generator.d.ts +26 -0
- package/dist/generator/test-generator.d.ts.map +1 -0
- package/dist/governance/audit-log.d.ts +23 -0
- package/dist/governance/audit-log.d.ts.map +1 -0
- package/dist/governance/governance-policy.d.ts +24 -0
- package/dist/governance/governance-policy.d.ts.map +1 -0
- package/dist/governance/team-dashboard.d.ts +51 -0
- package/dist/governance/team-dashboard.d.ts.map +1 -0
- package/dist/governance/workflow-audit.d.ts +13 -0
- package/dist/governance/workflow-audit.d.ts.map +1 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20318 -0
- package/dist/integrations/ci-summary.d.ts +14 -0
- package/dist/integrations/ci-summary.d.ts.map +1 -0
- package/dist/integrations/playwright-report-adapter.d.ts +22 -0
- package/dist/integrations/playwright-report-adapter.d.ts.map +1 -0
- package/dist/integrations/quality-gate.d.ts +35 -0
- package/dist/integrations/quality-gate.d.ts.map +1 -0
- package/dist/integrations/sarif-report.d.ts +48 -0
- package/dist/integrations/sarif-report.d.ts.map +1 -0
- package/dist/intelligence/autonomous-repair-workflow.d.ts +31 -0
- package/dist/intelligence/autonomous-repair-workflow.d.ts.map +1 -0
- package/dist/intelligence/business-intent.d.ts +58 -0
- package/dist/intelligence/business-intent.d.ts.map +1 -0
- package/dist/intelligence/evidence-bundle.d.ts +364 -0
- package/dist/intelligence/evidence-bundle.d.ts.map +1 -0
- package/dist/intelligence/failure-clustering.d.ts +38 -0
- package/dist/intelligence/failure-clustering.d.ts.map +1 -0
- package/dist/intelligence/generation-agent.d.ts +120 -0
- package/dist/intelligence/generation-agent.d.ts.map +1 -0
- package/dist/intelligence/generation-verifier.d.ts +151 -0
- package/dist/intelligence/generation-verifier.d.ts.map +1 -0
- package/dist/intelligence/llm-proposal.d.ts +377 -0
- package/dist/intelligence/llm-proposal.d.ts.map +1 -0
- package/dist/intelligence/locator-disambiguation-engine.d.ts +38 -0
- package/dist/intelligence/locator-disambiguation-engine.d.ts.map +1 -0
- package/dist/intelligence/model-contract.d.ts +86 -0
- package/dist/intelligence/model-contract.d.ts.map +1 -0
- package/dist/intelligence/model-provider.d.ts +75 -0
- package/dist/intelligence/model-provider.d.ts.map +1 -0
- package/dist/intelligence/project-intelligence.d.ts +22 -0
- package/dist/intelligence/project-intelligence.d.ts.map +1 -0
- package/dist/intelligence/repair-agent.d.ts +46 -0
- package/dist/intelligence/repair-agent.d.ts.map +1 -0
- package/dist/intelligence/repository-profile.d.ts +131 -0
- package/dist/intelligence/repository-profile.d.ts.map +1 -0
- package/dist/intelligence/suite-intelligence.d.ts +59 -0
- package/dist/intelligence/suite-intelligence.d.ts.map +1 -0
- package/dist/intelligence/test-intelligence.d.ts +44 -0
- package/dist/intelligence/test-intelligence.d.ts.map +1 -0
- package/dist/intelligence/trace-proposal-workflow.d.ts +30 -0
- package/dist/intelligence/trace-proposal-workflow.d.ts.map +1 -0
- package/dist/intelligence/verified-llm-repair-workflow.d.ts +21 -0
- package/dist/intelligence/verified-llm-repair-workflow.d.ts.map +1 -0
- package/dist/intelligence/workflow-planner.d.ts +278 -0
- package/dist/intelligence/workflow-planner.d.ts.map +1 -0
- package/dist/mcp/server.d.ts +32 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/memory/stability-ledger.d.ts +46 -0
- package/dist/memory/stability-ledger.d.ts.map +1 -0
- package/dist/perception/app-model.d.ts +55 -0
- package/dist/perception/app-model.d.ts.map +1 -0
- package/dist/perception/crawler.d.ts +18 -0
- package/dist/perception/crawler.d.ts.map +1 -0
- package/dist/perception/index.d.ts +3 -0
- package/dist/perception/index.d.ts.map +1 -0
- package/dist/repair/approval-service.d.ts +139 -0
- package/dist/repair/approval-service.d.ts.map +1 -0
- package/dist/repair/assertion-hasher.d.ts +62 -0
- package/dist/repair/assertion-hasher.d.ts.map +1 -0
- package/dist/repair/ast-patcher.d.ts +36 -0
- package/dist/repair/ast-patcher.d.ts.map +1 -0
- package/dist/repair/browser-operation-executor.d.ts +27 -0
- package/dist/repair/browser-operation-executor.d.ts.map +1 -0
- package/dist/repair/diff-allowlist.d.ts +74 -0
- package/dist/repair/diff-allowlist.d.ts.map +1 -0
- package/dist/repair/intent-validator.d.ts +157 -0
- package/dist/repair/intent-validator.d.ts.map +1 -0
- package/dist/repair/repair-operations.d.ts +40 -0
- package/dist/repair/repair-operations.d.ts.map +1 -0
- package/dist/repair/repair-planner.d.ts +98 -0
- package/dist/repair/repair-planner.d.ts.map +1 -0
- package/dist/repair/repair-workflow.d.ts +42 -0
- package/dist/repair/repair-workflow.d.ts.map +1 -0
- package/dist/repair/safe-operation-compiler.d.ts +15 -0
- package/dist/repair/safe-operation-compiler.d.ts.map +1 -0
- package/dist/repair/source-mapper.d.ts +19 -0
- package/dist/repair/source-mapper.d.ts.map +1 -0
- package/dist/repair/statement-patcher.d.ts +75 -0
- package/dist/repair/statement-patcher.d.ts.map +1 -0
- package/dist/repair/verification-gates.d.ts +25 -0
- package/dist/repair/verification-gates.d.ts.map +1 -0
- package/dist/repair/workflow-plan.d.ts +35 -0
- package/dist/repair/workflow-plan.d.ts.map +1 -0
- package/dist/repair/workspace-promoter.d.ts +43 -0
- package/dist/repair/workspace-promoter.d.ts.map +1 -0
- package/dist/reproduce/diagnostic-rerunner.d.ts +40 -0
- package/dist/reproduce/diagnostic-rerunner.d.ts.map +1 -0
- package/dist/reproduce/direct-browser-investigator.d.ts +41 -0
- package/dist/reproduce/direct-browser-investigator.d.ts.map +1 -0
- package/dist/reproduce/interactive-workflow-runner.d.ts +43 -0
- package/dist/reproduce/interactive-workflow-runner.d.ts.map +1 -0
- package/dist/sandbox/dependency-strategy.d.ts +45 -0
- package/dist/sandbox/dependency-strategy.d.ts.map +1 -0
- package/dist/sandbox/execution-runner.d.ts +81 -0
- package/dist/sandbox/execution-runner.d.ts.map +1 -0
- package/dist/sandbox/worktree-manager.d.ts +51 -0
- package/dist/sandbox/worktree-manager.d.ts.map +1 -0
- package/dist/selector/abstention-policy.d.ts +34 -0
- package/dist/selector/abstention-policy.d.ts.map +1 -0
- package/dist/selector/candidate-extractor.d.ts +71 -0
- package/dist/selector/candidate-extractor.d.ts.map +1 -0
- package/dist/selector/live-locator-verifier.d.ts +28 -0
- package/dist/selector/live-locator-verifier.d.ts.map +1 -0
- package/dist/selector/repair-candidate-ranker.d.ts +10 -0
- package/dist/selector/repair-candidate-ranker.d.ts.map +1 -0
- package/dist/selector/selector-scorer.d.ts +43 -0
- package/dist/selector/selector-scorer.d.ts.map +1 -0
- package/dist/trace/adapters/adapter-registry.d.ts +18 -0
- package/dist/trace/adapters/adapter-registry.d.ts.map +1 -0
- package/dist/trace/adapters/trace-adapter.d.ts +21 -0
- package/dist/trace/adapters/trace-adapter.d.ts.map +1 -0
- package/dist/trace/adapters/unknown-adapter.d.ts +10 -0
- package/dist/trace/adapters/unknown-adapter.d.ts.map +1 -0
- package/dist/trace/adapters/v10-adapter.d.ts +10 -0
- package/dist/trace/adapters/v10-adapter.d.ts.map +1 -0
- package/dist/trace/adapters/v4-adapter.d.ts +11 -0
- package/dist/trace/adapters/v4-adapter.d.ts.map +1 -0
- package/dist/trace/adapters/v8-adapter.d.ts +10 -0
- package/dist/trace/adapters/v8-adapter.d.ts.map +1 -0
- package/dist/trace/adapters/v9-adapter.d.ts +10 -0
- package/dist/trace/adapters/v9-adapter.d.ts.map +1 -0
- package/dist/trace/archive-reader.d.ts +44 -0
- package/dist/trace/archive-reader.d.ts.map +1 -0
- package/dist/trace/snapshot-resolver.d.ts +55 -0
- package/dist/trace/snapshot-resolver.d.ts.map +1 -0
- package/dist/trace/trace-parser.d.ts +50 -0
- package/dist/trace/trace-parser.d.ts.map +1 -0
- package/dist/trace/version-detector.d.ts +20 -0
- package/dist/trace/version-detector.d.ts.map +1 -0
- package/package.json +76 -6
- package/skills/README.md +20 -0
- package/skills/pie-failure-investigation/SKILL.md +16 -0
- package/skills/pie-quality-engineer/SKILL.md +66 -0
- package/skills/pie-release-readiness/SKILL.md +22 -0
- package/skills/pie-repair-review/SKILL.md +21 -0
- package/skills/pie-trace-to-repair/SKILL.md +35 -0
- package/src/authoring/author.ts +81 -0
- package/src/authoring/generation-contract.ts +283 -0
- package/src/authoring/hybrid-author.ts +115 -0
- package/src/authoring/index.ts +7 -0
- package/src/authoring/llm-author.ts +282 -0
- package/src/authoring/naming/index.ts +4 -0
- package/src/authoring/naming/name-plan.ts +28 -0
- package/src/authoring/naming/name-planner.ts +339 -0
- package/src/authoring/naming/naming-linter.ts +292 -0
- package/src/authoring/template-author.ts +39 -0
- package/src/cli/index.ts +1172 -0
- package/src/collectors/collector-interface.ts +8 -0
- package/src/collectors/environment-collector.ts +103 -0
- package/src/collectors/error-collector.ts +177 -0
- package/src/collectors/git-diff-collector.ts +190 -0
- package/src/collectors/live-browser-collector.ts +412 -0
- package/src/collectors/network-collector.ts +90 -0
- package/src/collectors/static-ast-collector.ts +226 -0
- package/src/collectors/trace-collector.ts +66 -0
- package/src/config/project-config.ts +114 -0
- package/src/core/errors/engine-errors.ts +90 -0
- package/src/core/types/diagnostic-context.ts +94 -0
- package/src/core/types/repair-evidence.ts +45 -0
- package/src/core/version.ts +25 -0
- package/src/diagnostics/diagnostic-contract.ts +79 -0
- package/src/diagnostics/diagnostic-report.ts +204 -0
- package/src/diagnostics/evidence-correlator.ts +117 -0
- package/src/diagnostics/external-failure-evidence.ts +72 -0
- package/src/diagnostics/failure-category.ts +40 -0
- package/src/diagnostics/failure-classifier.ts +701 -0
- package/src/evaluation/evaluation-scorecard.ts +198 -0
- package/src/evaluation/repository-inspector.ts +143 -0
- package/src/evaluation/repository-runner.ts +197 -0
- package/src/evidence/evidence-model.ts +76 -0
- package/src/evidence/evidence-normalizer.ts +322 -0
- package/src/evidence/evidence-redactor.ts +218 -0
- package/src/framework/config-adapter.ts +131 -0
- package/src/gate/index.ts +43 -0
- package/src/gate/oracle.ts +69 -0
- package/src/gate/quality.ts +101 -0
- package/src/gate/safety.ts +94 -0
- package/src/generator/architecture-generator.ts +582 -0
- package/src/generator/assertion-quality-gate.ts +88 -0
- package/src/generator/candidate-writer.ts +288 -0
- package/src/generator/test-generator.ts +52 -0
- package/src/governance/audit-log.ts +115 -0
- package/src/governance/governance-policy.ts +72 -0
- package/src/governance/team-dashboard.ts +189 -0
- package/src/governance/workflow-audit.ts +72 -0
- package/src/index.ts +107 -0
- package/src/integrations/ci-summary.ts +37 -0
- package/src/integrations/playwright-report-adapter.ts +173 -0
- package/src/integrations/quality-gate.ts +95 -0
- package/src/integrations/sarif-report.ts +110 -0
- package/src/intelligence/autonomous-repair-workflow.ts +139 -0
- package/src/intelligence/business-intent.ts +163 -0
- package/src/intelligence/evidence-bundle.ts +300 -0
- package/src/intelligence/failure-clustering.ts +134 -0
- package/src/intelligence/generation-agent.ts +561 -0
- package/src/intelligence/generation-verifier.ts +1115 -0
- package/src/intelligence/llm-proposal.ts +519 -0
- package/src/intelligence/locator-disambiguation-engine.ts +331 -0
- package/src/intelligence/model-contract.ts +205 -0
- package/src/intelligence/model-provider.ts +348 -0
- package/src/intelligence/project-intelligence.ts +140 -0
- package/src/intelligence/repair-agent.ts +297 -0
- package/src/intelligence/repository-profile.ts +796 -0
- package/src/intelligence/suite-intelligence.ts +305 -0
- package/src/intelligence/test-intelligence.ts +136 -0
- package/src/intelligence/trace-proposal-workflow.ts +318 -0
- package/src/intelligence/verified-llm-repair-workflow.ts +48 -0
- package/src/intelligence/workflow-planner.ts +966 -0
- package/src/mcp/server.ts +970 -0
- package/src/memory/stability-ledger.ts +243 -0
- package/src/perception/app-model.ts +32 -0
- package/src/perception/crawler.ts +263 -0
- package/src/perception/index.ts +3 -0
- package/src/repair/approval-service.ts +541 -0
- package/src/repair/assertion-hasher.ts +386 -0
- package/src/repair/ast-patcher.ts +223 -0
- package/src/repair/browser-operation-executor.ts +263 -0
- package/src/repair/diff-allowlist.ts +612 -0
- package/src/repair/intent-validator.ts +761 -0
- package/src/repair/repair-operations.ts +65 -0
- package/src/repair/repair-planner.ts +467 -0
- package/src/repair/repair-workflow.ts +241 -0
- package/src/repair/safe-operation-compiler.ts +68 -0
- package/src/repair/source-mapper.ts +156 -0
- package/src/repair/statement-patcher.ts +544 -0
- package/src/repair/verification-gates.ts +46 -0
- package/src/repair/workflow-plan.ts +59 -0
- package/src/repair/workspace-promoter.ts +313 -0
- package/src/reproduce/diagnostic-rerunner.ts +287 -0
- package/src/reproduce/direct-browser-investigator.ts +208 -0
- package/src/reproduce/interactive-workflow-runner.ts +828 -0
- package/src/sandbox/dependency-strategy.ts +215 -0
- package/src/sandbox/execution-runner.ts +408 -0
- package/src/sandbox/worktree-manager.ts +287 -0
- package/src/selector/abstention-policy.ts +175 -0
- package/src/selector/candidate-extractor.ts +343 -0
- package/src/selector/live-locator-verifier.ts +139 -0
- package/src/selector/repair-candidate-ranker.ts +92 -0
- package/src/selector/selector-scorer.ts +344 -0
- package/src/trace/adapters/adapter-registry.ts +49 -0
- package/src/trace/adapters/trace-adapter.ts +28 -0
- package/src/trace/adapters/unknown-adapter.ts +50 -0
- package/src/trace/adapters/v10-adapter.ts +39 -0
- package/src/trace/adapters/v4-adapter.ts +31 -0
- package/src/trace/adapters/v8-adapter.ts +42 -0
- package/src/trace/adapters/v9-adapter.ts +39 -0
- package/src/trace/archive-reader.ts +246 -0
- package/src/trace/snapshot-resolver.ts +308 -0
- package/src/trace/trace-parser.ts +467 -0
- package/src/trace/version-detector.ts +81 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import AdmZip from 'adm-zip';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { ZipSlipSecurityError, TraceCorruptedError } from '../core/errors/engine-errors.js';
|
|
5
|
+
|
|
6
|
+
export interface UnpackOptions {
|
|
7
|
+
maxTotalSize?: number; // Default 500 MB
|
|
8
|
+
maxEntries?: number; // Default 10,000 entries
|
|
9
|
+
targetDir?: string; // Directory to extract to
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ExtractedTraceArchive {
|
|
13
|
+
archivePath: string;
|
|
14
|
+
extractDir: string;
|
|
15
|
+
entryNames: string[];
|
|
16
|
+
hasTraceEventFile: boolean;
|
|
17
|
+
hasNetworkEventFile: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TraceEntryDiscovery {
|
|
21
|
+
/** Best stream for backward-compatible single-stream consumers. */
|
|
22
|
+
traceEntryName?: string | undefined;
|
|
23
|
+
networkEntryName?: string | undefined;
|
|
24
|
+
/** All streams, ordered with explicit failure streams first. */
|
|
25
|
+
traceEntryNames?: string[] | undefined;
|
|
26
|
+
networkEntryNames?: string[] | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class ArchiveReader {
|
|
30
|
+
private static readonly DEFAULT_MAX_SIZE = 100 * 1024 * 1024; // 100 MB max uncompressed
|
|
31
|
+
private static readonly DEFAULT_MAX_ENTRIES = 10000;
|
|
32
|
+
private static readonly MAX_COMPRESSION_RATIO = 10; // 10:1 ratio protection
|
|
33
|
+
private static readonly MAX_SINGLE_ENTRY_SIZE = 50 * 1024 * 1024; // 50 MB max single entry
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Automatically discovers the primary trace and network JSONL entry names inside a Playwright trace archive.
|
|
37
|
+
* Matches stream and worker IDs between .trace and .network files to prevent cross-worker mismatch.
|
|
38
|
+
*/
|
|
39
|
+
public static findTraceFileEntries(zipPath: string): TraceEntryDiscovery {
|
|
40
|
+
try {
|
|
41
|
+
const zip = new AdmZip(zipPath);
|
|
42
|
+
const entries = zip.getEntries();
|
|
43
|
+
|
|
44
|
+
// A Playwright archive can contain several streams. The largest stream is
|
|
45
|
+
// often a browser/page stream, while the runner's test.trace contains the
|
|
46
|
+
// actual failed pw:api action and error. Prefer the stream with explicit
|
|
47
|
+
// failed-action evidence, then fall back to the largest stream for older
|
|
48
|
+
// or minimal archives.
|
|
49
|
+
const traceCandidates = entries
|
|
50
|
+
.filter((e) => e.entryName.endsWith('.trace'))
|
|
51
|
+
.map((entry) => {
|
|
52
|
+
const text = entry.getData().toString('utf8');
|
|
53
|
+
const lines = text.split(/\r?\n/);
|
|
54
|
+
let failureScore = 0;
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
if (!line.includes('"type":"')) continue;
|
|
57
|
+
if (line.includes('"type":"after"') && line.includes('"error"')) failureScore += 100;
|
|
58
|
+
if (line.includes('"type":"error"') && (line.includes('"message"') || line.includes('"error"'))) failureScore += 80;
|
|
59
|
+
if (line.includes('"type":"log"')) failureScore += 1;
|
|
60
|
+
}
|
|
61
|
+
if (path.basename(entry.entryName).toLowerCase() === 'test.trace' && failureScore > 0) {
|
|
62
|
+
failureScore += 10;
|
|
63
|
+
}
|
|
64
|
+
return { entry, failureScore };
|
|
65
|
+
})
|
|
66
|
+
.sort((a, b) => b.failureScore - a.failureScore || b.entry.header.size - a.entry.header.size);
|
|
67
|
+
|
|
68
|
+
const orderedTraceNames = traceCandidates.map(({ entry }) => entry.entryName);
|
|
69
|
+
const bestTrace = orderedTraceNames[0];
|
|
70
|
+
if (!bestTrace) {
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Correlate network entry with matching stream / worker prefix if possible
|
|
75
|
+
const traceBase = path.basename(bestTrace, '.trace');
|
|
76
|
+
const networkCandidates = entries.filter((e) => e.entryName.endsWith('.network'));
|
|
77
|
+
const networkEntryNames = networkCandidates.map((entry) => entry.entryName);
|
|
78
|
+
|
|
79
|
+
let bestNetwork: string | undefined;
|
|
80
|
+
// 1. Exact base match (e.g. 0.trace -> 0.network, trace.trace -> trace.network)
|
|
81
|
+
const exactMatch = networkCandidates.find((e) => path.basename(e.entryName, '.network') === traceBase);
|
|
82
|
+
if (exactMatch) {
|
|
83
|
+
bestNetwork = exactMatch.entryName;
|
|
84
|
+
} else {
|
|
85
|
+
// 2. Prefix match (e.g. 0-trace.trace -> 0-network.network or 0.network)
|
|
86
|
+
const workerPrefix = traceBase.split('-')[0] ?? '';
|
|
87
|
+
const prefixMatch = networkCandidates.find((e) => path.basename(e.entryName, '.network').startsWith(workerPrefix));
|
|
88
|
+
bestNetwork = prefixMatch ? prefixMatch.entryName : networkCandidates.sort((a, b) => b.header.size - a.header.size)[0]?.entryName;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
traceEntryName: bestTrace,
|
|
93
|
+
networkEntryName: bestNetwork,
|
|
94
|
+
traceEntryNames: orderedTraceNames,
|
|
95
|
+
networkEntryNames,
|
|
96
|
+
};
|
|
97
|
+
} catch {
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Reads multiple archive entries and joins them as one logical JSONL stream. */
|
|
103
|
+
public static async readEntriesAsText(zipPath: string, entryNames: string[]): Promise<string> {
|
|
104
|
+
const uniqueNames = [...new Set(entryNames.filter(Boolean))];
|
|
105
|
+
const texts = await Promise.all(uniqueNames.map((entryName) => ArchiveReader.readEntryAsText(zipPath, entryName)));
|
|
106
|
+
return texts.join('\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Reads a single entry as a UTF-8 string directly from a .zip file in memory
|
|
111
|
+
* with strict per-entry size limits.
|
|
112
|
+
*/
|
|
113
|
+
public static async readEntryAsText(zipPath: string, entryName: string): Promise<string> {
|
|
114
|
+
try {
|
|
115
|
+
const zip = new AdmZip(zipPath);
|
|
116
|
+
const entry = zip.getEntry(entryName);
|
|
117
|
+
if (!entry) {
|
|
118
|
+
throw new Error(`Entry '${entryName}' not found in archive '${zipPath}'`);
|
|
119
|
+
}
|
|
120
|
+
if (entry.header.size > ArchiveReader.MAX_SINGLE_ENTRY_SIZE) {
|
|
121
|
+
throw new TraceCorruptedError(
|
|
122
|
+
zipPath,
|
|
123
|
+
`Entry '${entryName}' size ${entry.header.size} bytes exceeds single entry limit of ${ArchiveReader.MAX_SINGLE_ENTRY_SIZE} bytes`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return entry.getData().toString('utf8');
|
|
127
|
+
} catch (err: any) {
|
|
128
|
+
if (err instanceof ZipSlipSecurityError) throw err;
|
|
129
|
+
throw new TraceCorruptedError(zipPath, err.message);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Safely unpacks a Playwright trace.zip archive to an isolated destination directory
|
|
135
|
+
* with strict Zip-Slip and Zip-Bomb protections.
|
|
136
|
+
*/
|
|
137
|
+
public static async unpackSafely(
|
|
138
|
+
zipPath: string,
|
|
139
|
+
destinationDir: string,
|
|
140
|
+
options?: UnpackOptions
|
|
141
|
+
): Promise<ExtractedTraceArchive> {
|
|
142
|
+
const maxSize = options?.maxTotalSize ?? ArchiveReader.DEFAULT_MAX_SIZE;
|
|
143
|
+
const maxEntries = options?.maxEntries ?? ArchiveReader.DEFAULT_MAX_ENTRIES;
|
|
144
|
+
|
|
145
|
+
const resolvedDest = path.resolve(destinationDir);
|
|
146
|
+
await fs.mkdir(resolvedDest, { recursive: true });
|
|
147
|
+
|
|
148
|
+
let zip: AdmZip;
|
|
149
|
+
try {
|
|
150
|
+
zip = new AdmZip(zipPath);
|
|
151
|
+
} catch (err: any) {
|
|
152
|
+
throw new TraceCorruptedError(zipPath, `Failed to initialize zip reader: ${err.message}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const entries = zip.getEntries();
|
|
156
|
+
if (entries.length > maxEntries) {
|
|
157
|
+
throw new TraceCorruptedError(
|
|
158
|
+
zipPath,
|
|
159
|
+
`Zip-Bomb suspected: archive contains ${entries.length} entries (max allowed: ${maxEntries})`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let totalSize = 0;
|
|
164
|
+
const entryNames: string[] = [];
|
|
165
|
+
|
|
166
|
+
// Phase 1: Validate all entries BEFORE writing anything to disk (All-or-nothing check)
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
totalSize += entry.header.size;
|
|
169
|
+
if (totalSize > maxSize) {
|
|
170
|
+
throw new TraceCorruptedError(
|
|
171
|
+
zipPath,
|
|
172
|
+
`Zip-Bomb suspected: uncompressed size exceeded limit of ${maxSize} bytes`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Check for Zip-Bomb compression ratio anomaly (e.g. 100:1 ratio)
|
|
177
|
+
if (entry.header.compressedSize > 0 && entry.header.size > 512 * 1024) {
|
|
178
|
+
const ratio = entry.header.size / entry.header.compressedSize;
|
|
179
|
+
if (ratio > ArchiveReader.MAX_COMPRESSION_RATIO) {
|
|
180
|
+
throw new TraceCorruptedError(
|
|
181
|
+
zipPath,
|
|
182
|
+
`Zip-Bomb suspected: compression ratio ${ratio.toFixed(1)}:1 exceeds limit of ${ArchiveReader.MAX_COMPRESSION_RATIO}:1 for entry '${entry.entryName}'`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check for Zip-Slip path traversal vulnerability
|
|
188
|
+
const entryName = entry.entryName;
|
|
189
|
+
// Cross-platform check: reject absolute paths, drive letters, UNC paths, and root-relative paths
|
|
190
|
+
if (
|
|
191
|
+
path.isAbsolute(entryName) ||
|
|
192
|
+
path.win32.isAbsolute(entryName) ||
|
|
193
|
+
path.posix.isAbsolute(entryName) ||
|
|
194
|
+
/^[a-zA-Z]:[/\\]/.test(entryName) ||
|
|
195
|
+
entryName.startsWith('/') ||
|
|
196
|
+
entryName.startsWith('\\')
|
|
197
|
+
) {
|
|
198
|
+
throw new ZipSlipSecurityError(entry.entryName);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Ensure target path is strictly within destination directory on both Windows and POSIX
|
|
202
|
+
const normalizedEntry = entryName.replace(/\\/g, '/');
|
|
203
|
+
const targetFilePath = path.resolve(resolvedDest, normalizedEntry);
|
|
204
|
+
const relative = path.relative(resolvedDest, targetFilePath);
|
|
205
|
+
if (
|
|
206
|
+
relative.startsWith('..') ||
|
|
207
|
+
path.isAbsolute(relative) ||
|
|
208
|
+
path.win32.isAbsolute(relative) ||
|
|
209
|
+
path.posix.isAbsolute(relative)
|
|
210
|
+
) {
|
|
211
|
+
throw new ZipSlipSecurityError(entry.entryName);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
entryNames.push(entry.entryName);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Phase 2: Safe extraction
|
|
218
|
+
for (const entry of entries) {
|
|
219
|
+
const normalizedEntry = entry.entryName.replace(/\\/g, '/');
|
|
220
|
+
const targetFilePath = path.resolve(resolvedDest, normalizedEntry);
|
|
221
|
+
if (entry.isDirectory) {
|
|
222
|
+
await fs.mkdir(targetFilePath, { recursive: true });
|
|
223
|
+
} else {
|
|
224
|
+
await fs.mkdir(path.dirname(targetFilePath), { recursive: true });
|
|
225
|
+
const content = entry.getData();
|
|
226
|
+
await fs.writeFile(targetFilePath, content);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const hasTraceEventFile = entryNames.some(
|
|
231
|
+
(name) => name === 'trace.trace' || name.endsWith('.trace')
|
|
232
|
+
);
|
|
233
|
+
const hasNetworkEventFile = entryNames.some(
|
|
234
|
+
(name) => name === 'trace.network' || name.endsWith('.network')
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
archivePath: zipPath,
|
|
239
|
+
extractDir: resolvedDest,
|
|
240
|
+
entryNames,
|
|
241
|
+
hasTraceEventFile,
|
|
242
|
+
hasNetworkEventFile,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
export type NodeSnapshot =
|
|
2
|
+
| string
|
|
3
|
+
| number
|
|
4
|
+
| [string, Record<string, string>, ...NodeSnapshot[]];
|
|
5
|
+
|
|
6
|
+
export interface ResolvedElement {
|
|
7
|
+
tag: string;
|
|
8
|
+
attributes: Record<string, string>;
|
|
9
|
+
text: string;
|
|
10
|
+
children: ResolvedElement[];
|
|
11
|
+
role?: string | undefined;
|
|
12
|
+
accessibleName?: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type SnapshotCompleteness = 'COMPLETE' | 'PARTIAL' | 'UNAVAILABLE';
|
|
16
|
+
|
|
17
|
+
export interface ResolvedSnapshot {
|
|
18
|
+
frameId: string;
|
|
19
|
+
callId?: string | undefined;
|
|
20
|
+
phase: 'before' | 'action' | 'after';
|
|
21
|
+
completeness: SnapshotCompleteness;
|
|
22
|
+
unresolvedReferences: string[];
|
|
23
|
+
dom: ResolvedElement[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface FrameSnapshotRecord {
|
|
27
|
+
frameId: string;
|
|
28
|
+
pageId?: string | undefined;
|
|
29
|
+
callId?: string | undefined;
|
|
30
|
+
snapshotName?: string | undefined;
|
|
31
|
+
phase?: 'before' | 'action' | 'after' | undefined;
|
|
32
|
+
node?: NodeSnapshot | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class SnapshotResolver {
|
|
36
|
+
/**
|
|
37
|
+
* Reconstructs an in-memory resolved element tree from Playwright's array-based NodeSnapshot,
|
|
38
|
+
* tracking provenance, snapshot phase, and evidence completeness.
|
|
39
|
+
*/
|
|
40
|
+
public static resolveSnapshot(record: FrameSnapshotRecord): ResolvedSnapshot {
|
|
41
|
+
const frameId = record.frameId || 'main-frame';
|
|
42
|
+
const phase = record.phase ?? 'before';
|
|
43
|
+
const unresolvedReferences: string[] = [];
|
|
44
|
+
|
|
45
|
+
if (!record.node) {
|
|
46
|
+
return {
|
|
47
|
+
frameId,
|
|
48
|
+
callId: record.callId,
|
|
49
|
+
phase,
|
|
50
|
+
completeness: 'UNAVAILABLE',
|
|
51
|
+
unresolvedReferences: ['Missing root node in snapshot record'],
|
|
52
|
+
dom: [],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const rootElement = SnapshotResolver.resolveNode(record.node, unresolvedReferences);
|
|
57
|
+
const completeness: SnapshotCompleteness =
|
|
58
|
+
unresolvedReferences.length > 0 ? 'PARTIAL' : rootElement ? 'COMPLETE' : 'UNAVAILABLE';
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
frameId,
|
|
62
|
+
callId: record.callId,
|
|
63
|
+
phase,
|
|
64
|
+
completeness,
|
|
65
|
+
unresolvedReferences,
|
|
66
|
+
dom: rootElement ? [rootElement] : [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Reconstructs an in-memory resolved element tree from Playwright's array-based NodeSnapshot.
|
|
72
|
+
*/
|
|
73
|
+
public static resolveNode(node: NodeSnapshot, unresolvedRefs?: string[]): ResolvedElement | null {
|
|
74
|
+
if (typeof node === 'string' || typeof node === 'number') {
|
|
75
|
+
if (typeof node === 'string' && node.startsWith('ref:')) {
|
|
76
|
+
unresolvedRefs?.push(node);
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!Array.isArray(node) || node.length < 2) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const tag = (node[0] as string).toLowerCase();
|
|
86
|
+
const attributes = (node[1] as Record<string, string>) ?? {};
|
|
87
|
+
const rawChildren = node.slice(2) as NodeSnapshot[];
|
|
88
|
+
|
|
89
|
+
const children: ResolvedElement[] = [];
|
|
90
|
+
let textAccumulator = '';
|
|
91
|
+
|
|
92
|
+
for (const child of rawChildren) {
|
|
93
|
+
if (typeof child === 'string') {
|
|
94
|
+
if (child.startsWith('ref:')) {
|
|
95
|
+
unresolvedRefs?.push(child);
|
|
96
|
+
} else {
|
|
97
|
+
textAccumulator += child.trim() + ' ';
|
|
98
|
+
}
|
|
99
|
+
} else if (Array.isArray(child)) {
|
|
100
|
+
const resolvedChild = SnapshotResolver.resolveNode(child, unresolvedRefs);
|
|
101
|
+
if (resolvedChild) {
|
|
102
|
+
children.push(resolvedChild);
|
|
103
|
+
textAccumulator += resolvedChild.text + ' ';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const role = attributes['role'] ?? SnapshotResolver.inferRoleFromTag(tag, attributes);
|
|
109
|
+
const accessibleName =
|
|
110
|
+
attributes['aria-label'] ??
|
|
111
|
+
attributes['alt'] ??
|
|
112
|
+
attributes['title'] ??
|
|
113
|
+
attributes['placeholder'] ??
|
|
114
|
+
textAccumulator.trim();
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
tag,
|
|
118
|
+
attributes,
|
|
119
|
+
text: textAccumulator.trim(),
|
|
120
|
+
children,
|
|
121
|
+
role,
|
|
122
|
+
accessibleName: accessibleName || undefined,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Traverses and collects all nodes in depth-first order as Playwright's snapshotNodes does.
|
|
128
|
+
*/
|
|
129
|
+
public static snapshotNodes(snap: any): any[] {
|
|
130
|
+
if (!snap) return [];
|
|
131
|
+
if (snap._nodes) return snap._nodes;
|
|
132
|
+
const nodes: any[] = [];
|
|
133
|
+
const visit = (n: any) => {
|
|
134
|
+
if (typeof n === 'string' || typeof n === 'number') {
|
|
135
|
+
nodes.push(n);
|
|
136
|
+
} else if (Array.isArray(n) && typeof n[0] === 'string') {
|
|
137
|
+
const [, , ...children] = n;
|
|
138
|
+
for (const child of children) visit(child);
|
|
139
|
+
nodes.push(n);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
if (snap.html) visit(snap.html);
|
|
143
|
+
else if (snap.node) visit(snap.node);
|
|
144
|
+
snap._nodes = nodes;
|
|
145
|
+
return nodes;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Resolves a modern Playwright differential snapshot node with support for subtree references [ [ delta, nodeIndex ] ].
|
|
150
|
+
*/
|
|
151
|
+
public static resolveDifferentialNode(
|
|
152
|
+
n: any,
|
|
153
|
+
snapshotIndex: number,
|
|
154
|
+
allSnapshots: any[],
|
|
155
|
+
unresolvedRefs?: string[]
|
|
156
|
+
): ResolvedElement | string | null {
|
|
157
|
+
if (typeof n === 'string' || typeof n === 'number') {
|
|
158
|
+
return String(n);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check for Playwright subtree reference [ [ delta, nodeIndex ] ]
|
|
162
|
+
if (
|
|
163
|
+
Array.isArray(n) &&
|
|
164
|
+
n.length === 1 &&
|
|
165
|
+
Array.isArray(n[0]) &&
|
|
166
|
+
n[0].length === 2 &&
|
|
167
|
+
typeof n[0][0] === 'number' &&
|
|
168
|
+
typeof n[0][1] === 'number'
|
|
169
|
+
) {
|
|
170
|
+
const referenceIndex = snapshotIndex - n[0][0];
|
|
171
|
+
if (referenceIndex >= 0 && referenceIndex < allSnapshots.length) {
|
|
172
|
+
const nodes = SnapshotResolver.snapshotNodes(allSnapshots[referenceIndex]);
|
|
173
|
+
const nodeIndex = n[0][1];
|
|
174
|
+
if (nodeIndex >= 0 && nodeIndex < nodes.length) {
|
|
175
|
+
return SnapshotResolver.resolveDifferentialNode(
|
|
176
|
+
nodes[nodeIndex],
|
|
177
|
+
referenceIndex,
|
|
178
|
+
allSnapshots,
|
|
179
|
+
unresolvedRefs
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
unresolvedRefs?.push(`Subtree reference out of bounds: [${n[0][0]}, ${n[0][1]}]`);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!Array.isArray(n) || n.length < 2 || typeof n[0] !== 'string') {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const tag = (n[0] as string).toLowerCase();
|
|
192
|
+
const attributes = (n[1] as Record<string, string>) ?? {};
|
|
193
|
+
const rawChildren = n.slice(2);
|
|
194
|
+
|
|
195
|
+
const children: ResolvedElement[] = [];
|
|
196
|
+
let textAccumulator = '';
|
|
197
|
+
|
|
198
|
+
for (const child of rawChildren) {
|
|
199
|
+
const resolved = SnapshotResolver.resolveDifferentialNode(
|
|
200
|
+
child,
|
|
201
|
+
snapshotIndex,
|
|
202
|
+
allSnapshots,
|
|
203
|
+
unresolvedRefs
|
|
204
|
+
);
|
|
205
|
+
if (typeof resolved === 'string') {
|
|
206
|
+
textAccumulator += resolved.trim() + ' ';
|
|
207
|
+
} else if (resolved && typeof resolved === 'object' && 'tag' in resolved) {
|
|
208
|
+
children.push(resolved as ResolvedElement);
|
|
209
|
+
textAccumulator += (resolved as ResolvedElement).text + ' ';
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const role = attributes['role'] ?? SnapshotResolver.inferRoleFromTag(tag, attributes);
|
|
214
|
+
const accessibleName =
|
|
215
|
+
attributes['aria-label'] ??
|
|
216
|
+
attributes['alt'] ??
|
|
217
|
+
attributes['title'] ??
|
|
218
|
+
attributes['placeholder'] ??
|
|
219
|
+
textAccumulator.trim();
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
tag,
|
|
223
|
+
attributes,
|
|
224
|
+
text: textAccumulator.trim(),
|
|
225
|
+
children,
|
|
226
|
+
role,
|
|
227
|
+
accessibleName: accessibleName || undefined,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Serializes a resolved element tree back into clean, formatted HTML.
|
|
233
|
+
*/
|
|
234
|
+
public static toHtml(element: ResolvedElement, indent = 0): string {
|
|
235
|
+
const spacing = ' '.repeat(indent);
|
|
236
|
+
const attrs = Object.entries(element.attributes)
|
|
237
|
+
.map(([k, v]) => `${k}="${v.replace(/"/g, '"')}"`)
|
|
238
|
+
.join(' ');
|
|
239
|
+
const attrString = attrs.length > 0 ? ` ${attrs}` : '';
|
|
240
|
+
|
|
241
|
+
if (element.children.length === 0) {
|
|
242
|
+
return `${spacing}<${element.tag}${attrString}>${element.text}</${element.tag}>`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const childrenHtml = element.children.map((c) => SnapshotResolver.toHtml(c, indent + 1)).join('\n');
|
|
246
|
+
return `${spacing}<${element.tag}${attrString}>\n${childrenHtml}\n${spacing}</${element.tag}>`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Collects all descendant interactive elements (buttons, inputs, links, selects, roles).
|
|
251
|
+
*/
|
|
252
|
+
public static extractInteractiveElements(root: ResolvedElement): ResolvedElement[] {
|
|
253
|
+
const interactiveTags = new Set(['button', 'a', 'input', 'select', 'textarea', 'option']);
|
|
254
|
+
const interactiveRoles = new Set([
|
|
255
|
+
'button',
|
|
256
|
+
'link',
|
|
257
|
+
'textbox',
|
|
258
|
+
'combobox',
|
|
259
|
+
'checkbox',
|
|
260
|
+
'radio',
|
|
261
|
+
'tab',
|
|
262
|
+
'menuitem',
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
const results: ResolvedElement[] = [];
|
|
266
|
+
|
|
267
|
+
function traverse(node: ResolvedElement) {
|
|
268
|
+
const isInteractive =
|
|
269
|
+
interactiveTags.has(node.tag) ||
|
|
270
|
+
(node.role && interactiveRoles.has(node.role.toLowerCase())) ||
|
|
271
|
+
Boolean(node.attributes?.['onclick']) ||
|
|
272
|
+
node.attributes?.['cursor'] === 'pointer';
|
|
273
|
+
|
|
274
|
+
if (isInteractive) {
|
|
275
|
+
results.push(node);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
for (const child of node.children) {
|
|
279
|
+
traverse(child);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
traverse(root);
|
|
284
|
+
return results;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private static inferRoleFromTag(tag: string, attributes: Record<string, string>): string | undefined {
|
|
288
|
+
switch (tag) {
|
|
289
|
+
case 'button':
|
|
290
|
+
return 'button';
|
|
291
|
+
case 'a':
|
|
292
|
+
return attributes['href'] ? 'link' : undefined;
|
|
293
|
+
case 'input': {
|
|
294
|
+
const type = attributes['type']?.toLowerCase() ?? 'text';
|
|
295
|
+
if (type === 'button' || type === 'submit' || type === 'reset') return 'button';
|
|
296
|
+
if (type === 'checkbox') return 'checkbox';
|
|
297
|
+
if (type === 'radio') return 'radio';
|
|
298
|
+
return 'textbox';
|
|
299
|
+
}
|
|
300
|
+
case 'select':
|
|
301
|
+
return 'combobox';
|
|
302
|
+
case 'textarea':
|
|
303
|
+
return 'textbox';
|
|
304
|
+
default:
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|