qa360 2.3.0 → 2.3.1
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 +155 -262
- package/{cli/dist → dist}/commands/ai.js +1 -1
- package/{cli/dist → dist}/commands/coverage.js +1 -1
- package/{cli/dist → dist}/commands/crawl.js +2 -2
- package/{cli/dist → dist}/commands/doctor.js +2 -2
- package/{cli/dist → dist}/commands/explain.js +2 -2
- package/{cli/dist → dist}/commands/flakiness.js +1 -1
- package/{cli/dist → dist}/commands/generate.js +1 -1
- package/{cli/dist → dist}/commands/history.js +1 -1
- package/{cli/dist → dist}/commands/monitor.js +3 -3
- package/{cli/dist → dist}/commands/ollama.js +1 -1
- package/{cli/dist → dist}/commands/pack.js +2 -2
- package/{cli/dist → dist}/commands/regression.js +1 -1
- package/{cli/dist → dist}/commands/repair.js +1 -1
- package/{cli/dist → dist}/commands/retry.js +1 -1
- package/{cli/dist → dist}/commands/run.d.ts +1 -1
- package/{cli/dist → dist}/commands/run.js +1 -1
- package/{cli/dist → dist}/commands/secrets.js +1 -1
- package/{cli/dist → dist}/commands/serve.js +1 -1
- package/{cli/dist → dist}/commands/slo.js +1 -1
- package/{cli/dist → dist}/commands/verify.js +1 -1
- package/{cli/dist → dist}/core/adapters/playwright-native-api.d.ts +2 -0
- package/{cli/dist → dist}/core/adapters/playwright-native-api.js +20 -1
- package/{cli/dist → dist}/core/adapters/playwright-ui.d.ts +21 -0
- package/dist/core/adapters/playwright-ui.js +2050 -0
- package/{cli/dist → dist}/core/ai/ollama-provider.js +15 -3
- package/{cli/dist → dist}/core/artifacts/ui-artifacts.js +24 -4
- package/dist/core/auth/backup-codes-provider.d.ts +91 -0
- package/dist/core/auth/backup-codes-provider.js +215 -0
- package/{cli/dist → dist}/core/auth/basic-auth-provider.d.ts +6 -0
- package/{cli/dist → dist}/core/auth/basic-auth-provider.js +24 -6
- package/dist/core/auth/digest-auth-provider.d.ts +116 -0
- package/dist/core/auth/digest-auth-provider.js +244 -0
- package/dist/core/auth/hcaptcha-handler.d.ts +103 -0
- package/dist/core/auth/hcaptcha-handler.js +288 -0
- package/{cli/dist → dist}/core/auth/index.d.ts +81 -4
- package/{cli/dist → dist}/core/auth/index.js +15 -1
- package/dist/core/auth/oauth-handler.d.ts +408 -0
- package/dist/core/auth/oauth-handler.js +636 -0
- package/{cli/dist → dist}/core/auth/oauth2-provider.d.ts +9 -0
- package/dist/core/auth/oauth2-provider.js +227 -0
- package/dist/core/auth/otp-provider.d.ts +93 -0
- package/dist/core/auth/otp-provider.js +288 -0
- package/dist/core/auth/recaptcha-handler.d.ts +119 -0
- package/dist/core/auth/recaptcha-handler.js +301 -0
- package/dist/core/auth/remember-me-handler.d.ts +142 -0
- package/dist/core/auth/remember-me-handler.js +255 -0
- package/dist/core/auth/saml-handler.d.ts +173 -0
- package/dist/core/auth/saml-handler.js +364 -0
- package/dist/core/auth/webauthn-handler.d.ts +182 -0
- package/dist/core/auth/webauthn-handler.js +310 -0
- package/dist/core/crawler/advanced-interactions.d.ts +342 -0
- package/dist/core/crawler/advanced-interactions.js +1069 -0
- package/dist/core/crawler/blob-url-download-handler.d.ts +145 -0
- package/dist/core/crawler/blob-url-download-handler.js +392 -0
- package/dist/core/crawler/consent-handler.d.ts +49 -0
- package/dist/core/crawler/consent-handler.js +258 -0
- package/dist/core/crawler/cookie-manager.d.ts +166 -0
- package/dist/core/crawler/cookie-manager.js +353 -0
- package/dist/core/crawler/coop-coep-handler.d.ts +136 -0
- package/dist/core/crawler/coop-coep-handler.js +338 -0
- package/dist/core/crawler/csp-handler.d.ts +151 -0
- package/dist/core/crawler/csp-handler.js +415 -0
- package/dist/core/crawler/download-handler.d.ts +155 -0
- package/dist/core/crawler/download-handler.js +370 -0
- package/dist/core/crawler/email-testing-handler.d.ts +214 -0
- package/dist/core/crawler/email-testing-handler.js +398 -0
- package/dist/core/crawler/error-tracking-handler.d.ts +177 -0
- package/dist/core/crawler/error-tracking-handler.js +378 -0
- package/dist/core/crawler/form-handler.d.ts +100 -0
- package/dist/core/crawler/form-handler.js +465 -0
- package/dist/core/crawler/framework-wait-handler.d.ts +96 -0
- package/dist/core/crawler/framework-wait-handler.js +464 -0
- package/dist/core/crawler/geolocation-handler.d.ts +112 -0
- package/dist/core/crawler/geolocation-handler.js +276 -0
- package/dist/core/crawler/index.d.ts +78 -0
- package/{cli/dist → dist}/core/crawler/index.js +74 -1
- package/dist/core/crawler/intelligent-selector-generator.d.ts +164 -0
- package/dist/core/crawler/intelligent-selector-generator.js +612 -0
- package/{cli/dist → dist}/core/crawler/journey-generator.js +44 -1
- package/{cli/dist → dist}/core/crawler/page-analyzer.d.ts +16 -1
- package/{cli/dist → dist}/core/crawler/page-analyzer.js +469 -17
- package/dist/core/crawler/permissions-handler.d.ts +112 -0
- package/dist/core/crawler/permissions-handler.js +236 -0
- package/dist/core/crawler/permissions-policy-handler.d.ts +113 -0
- package/dist/core/crawler/permissions-policy-handler.js +402 -0
- package/dist/core/crawler/presets.d.ts +100 -0
- package/dist/core/crawler/presets.js +887 -0
- package/dist/core/crawler/repl-debug-handler.d.ts +105 -0
- package/dist/core/crawler/repl-debug-handler.js +552 -0
- package/dist/core/crawler/reporting-api-handler.d.ts +212 -0
- package/dist/core/crawler/reporting-api-handler.js +344 -0
- package/{cli/dist → dist}/core/crawler/selector-generator.d.ts +9 -0
- package/{cli/dist → dist}/core/crawler/selector-generator.js +99 -23
- package/dist/core/crawler/site-profiler.d.ts +89 -0
- package/dist/core/crawler/site-profiler.js +290 -0
- package/dist/core/crawler/sourcemaps-handler.d.ts +144 -0
- package/dist/core/crawler/sourcemaps-handler.js +420 -0
- package/dist/core/crawler/stacked-modals-handler.d.ts +118 -0
- package/dist/core/crawler/stacked-modals-handler.js +429 -0
- package/dist/core/crawler/trusted-types-handler.d.ts +149 -0
- package/dist/core/crawler/trusted-types-handler.js +413 -0
- package/{cli/dist → dist}/core/crawler/types.d.ts +68 -2
- package/dist/core/crawler/wait-strategies.d.ts +108 -0
- package/dist/core/crawler/wait-strategies.js +399 -0
- package/dist/core/fixtures/factories.d.ts +180 -0
- package/dist/core/fixtures/factories.js +279 -0
- package/dist/core/fixtures/index.d.ts +6 -0
- package/dist/core/fixtures/index.js +6 -0
- package/{cli/dist → dist}/core/generation/crawler-pack-generator.d.ts +13 -3
- package/dist/core/generation/crawler-pack-generator.js +232 -0
- package/{cli/dist → dist}/core/generation/index.d.ts +2 -0
- package/{cli/dist → dist}/core/generation/index.js +2 -0
- package/{cli/dist → dist}/core/index.d.ts +2 -0
- package/{cli/dist → dist}/core/index.js +4 -0
- package/dist/core/network/index.d.ts +7 -0
- package/dist/core/network/index.js +7 -0
- package/dist/core/network/network-manager.d.ts +237 -0
- package/dist/core/network/network-manager.js +343 -0
- package/dist/core/network/network-simulator.d.ts +158 -0
- package/dist/core/network/network-simulator.js +261 -0
- package/{cli/dist → dist}/core/pack/validator.js +2 -2
- package/{cli/dist → dist}/core/pack-v2/migrator.d.ts +5 -0
- package/{cli/dist → dist}/core/pack-v2/migrator.js +81 -6
- package/{cli/dist → dist}/core/pack-v2/validator.js +4 -3
- package/{cli/dist → dist}/core/pom/base-page.js +1 -1
- package/{cli/dist → dist}/core/pom/loader.js +1 -1
- package/dist/core/reporting/index.d.ts +9 -0
- package/dist/core/reporting/index.js +10 -0
- package/dist/core/reporting/junit-reporter.d.ts +114 -0
- package/dist/core/reporting/junit-reporter.js +306 -0
- package/{cli/dist → dist}/core/runner/e2e-helpers.d.ts +1 -1
- package/{cli/dist → dist}/core/runner/e2e-helpers.js +2 -2
- package/{cli/dist → dist}/core/runner/phase3-runner.d.ts +3 -0
- package/{cli/dist → dist}/core/runner/phase3-runner.js +45 -14
- package/dist/core/sharding/test-sharding.d.ts +137 -0
- package/dist/core/sharding/test-sharding.js +233 -0
- package/dist/core/storage/cookie-manager.d.ts +160 -0
- package/dist/core/storage/cookie-manager.js +268 -0
- package/dist/core/storage/index.d.ts +7 -0
- package/dist/core/storage/index.js +7 -0
- package/dist/core/storage/storage-helpers.d.ts +138 -0
- package/dist/core/storage/storage-helpers.js +315 -0
- package/dist/core/test-helpers/index.d.ts +6 -0
- package/dist/core/test-helpers/index.js +6 -0
- package/dist/core/test-helpers/state-reset.d.ts +119 -0
- package/dist/core/test-helpers/state-reset.js +234 -0
- package/{cli/dist → dist}/core/types/pack-v1.d.ts +15 -2
- package/{cli/dist → dist}/core/types/pack-v2.d.ts +1 -1
- package/dist/core/upload/chunked-uploader.d.ts +150 -0
- package/dist/core/upload/chunked-uploader.js +289 -0
- package/dist/core/upload/index.d.ts +11 -0
- package/dist/core/upload/index.js +8 -0
- package/dist/core/upload/mime-validator.d.ts +119 -0
- package/dist/core/upload/mime-validator.js +373 -0
- package/dist/core/upload/presigned-uploader.d.ts +118 -0
- package/dist/core/upload/presigned-uploader.js +274 -0
- package/dist/core/utils/device-emulation.d.ts +194 -0
- package/dist/core/utils/device-emulation.js +380 -0
- package/dist/core/utils/index.d.ts +8 -0
- package/dist/core/utils/index.js +8 -0
- package/dist/core/utils/retry.d.ts +145 -0
- package/dist/core/utils/retry.js +242 -0
- package/dist/core/utils/smart-wait.d.ts +133 -0
- package/dist/core/utils/smart-wait.js +417 -0
- package/dist/core/visual/index.d.ts +7 -0
- package/dist/core/visual/index.js +7 -0
- package/dist/core/visual/pixel-diff.d.ts +87 -0
- package/dist/core/visual/pixel-diff.js +213 -0
- package/dist/core/visual/screenshot-helper.d.ts +130 -0
- package/dist/core/visual/screenshot-helper.js +223 -0
- package/{cli/dist → dist}/utils/config.d.ts +1 -1
- package/examples/README.md +160 -0
- package/examples/accessibility.yml +48 -0
- package/examples/api-basic.yml +27 -0
- package/examples/complete.yml +146 -0
- package/examples/crawler.yml +38 -0
- package/examples/fullstack.yml +78 -0
- package/examples/security.yml +58 -0
- package/examples/ui-advanced.yml +49 -0
- package/examples/ui-basic.yml +24 -0
- package/package.json +33 -67
- package/CHANGELOG.md +0 -330
- package/CONTRIBUTING.md +0 -273
- package/QUICK_START.md +0 -191
- package/cli/CHANGELOG.md +0 -84
- package/cli/LICENSE +0 -24
- package/cli/README.md +0 -222
- package/cli/dist/core/adapters/playwright-ui.js +0 -864
- package/cli/dist/core/auth/oauth2-provider.js +0 -114
- package/cli/dist/core/coverage/analyzer.d.ts +0 -101
- package/cli/dist/core/coverage/analyzer.js +0 -415
- package/cli/dist/core/coverage/collector.d.ts +0 -74
- package/cli/dist/core/coverage/collector.js +0 -459
- package/cli/dist/core/coverage/config.d.ts +0 -37
- package/cli/dist/core/coverage/config.js +0 -156
- package/cli/dist/core/coverage/index.d.ts +0 -11
- package/cli/dist/core/coverage/index.js +0 -15
- package/cli/dist/core/coverage/types.d.ts +0 -267
- package/cli/dist/core/coverage/types.js +0 -6
- package/cli/dist/core/coverage/vault.d.ts +0 -95
- package/cli/dist/core/coverage/vault.js +0 -405
- package/cli/dist/core/crawler/index.d.ts +0 -57
- package/cli/dist/core/fixtures/index.d.ts +0 -8
- package/cli/dist/core/fixtures/index.js +0 -8
- package/cli/dist/core/generation/crawler-pack-generator.js +0 -231
- package/cli/dist/core/reporting/index.d.ts +0 -6
- package/cli/dist/core/reporting/index.js +0 -6
- package/cli/dist/core/visual/index.d.ts +0 -6
- package/cli/dist/core/visual/index.js +0 -6
- package/cli/package.json +0 -76
- package/core/LICENSE +0 -24
- package/core/README.md +0 -105
- package/core/package.json +0 -90
- package/core/schemas/pack.schema.json +0 -236
- /package/{cli/bin → bin}/qa360.js +0 -0
- /package/{cli/dist → dist}/cli-minimal.d.ts +0 -0
- /package/{cli/dist → dist}/cli-minimal.js +0 -0
- /package/{cli/dist → dist}/commands/ai.d.ts +0 -0
- /package/{cli/dist → dist}/commands/ask.d.ts +0 -0
- /package/{cli/dist → dist}/commands/ask.js +0 -0
- /package/{cli/dist → dist}/commands/coverage.d.ts +0 -0
- /package/{cli/dist → dist}/commands/crawl.d.ts +0 -0
- /package/{cli/dist → dist}/commands/doctor.d.ts +0 -0
- /package/{cli/dist → dist}/commands/examples.d.ts +0 -0
- /package/{cli/dist → dist}/commands/examples.js +0 -0
- /package/{cli/dist → dist}/commands/explain.d.ts +0 -0
- /package/{cli/dist → dist}/commands/flakiness.d.ts +0 -0
- /package/{cli/dist → dist}/commands/generate.d.ts +0 -0
- /package/{cli/dist → dist}/commands/history.d.ts +0 -0
- /package/{cli/dist → dist}/commands/init.d.ts +0 -0
- /package/{cli/dist → dist}/commands/init.js +0 -0
- /package/{cli/dist → dist}/commands/monitor.d.ts +0 -0
- /package/{cli/dist → dist}/commands/ollama.d.ts +0 -0
- /package/{cli/dist → dist}/commands/pack.d.ts +0 -0
- /package/{cli/dist → dist}/commands/regression.d.ts +0 -0
- /package/{cli/dist → dist}/commands/repair.d.ts +0 -0
- /package/{cli/dist → dist}/commands/report.d.ts +0 -0
- /package/{cli/dist → dist}/commands/report.js +0 -0
- /package/{cli/dist → dist}/commands/retry.d.ts +0 -0
- /package/{cli/dist → dist}/commands/scan.d.ts +0 -0
- /package/{cli/dist → dist}/commands/scan.js +0 -0
- /package/{cli/dist → dist}/commands/secrets.d.ts +0 -0
- /package/{cli/dist → dist}/commands/serve.d.ts +0 -0
- /package/{cli/dist → dist}/commands/slo.d.ts +0 -0
- /package/{cli/dist → dist}/commands/verify.d.ts +0 -0
- /package/{cli/dist → dist}/core/adapters/gitleaks-secrets.d.ts +0 -0
- /package/{cli/dist → dist}/core/adapters/gitleaks-secrets.js +0 -0
- /package/{cli/dist → dist}/core/adapters/jest-adapter.d.ts +0 -0
- /package/{cli/dist → dist}/core/adapters/jest-adapter.js +0 -0
- /package/{cli/dist → dist}/core/adapters/k6-perf.d.ts +0 -0
- /package/{cli/dist → dist}/core/adapters/k6-perf.js +0 -0
- /package/{cli/dist → dist}/core/adapters/osv-deps.d.ts +0 -0
- /package/{cli/dist → dist}/core/adapters/osv-deps.js +0 -0
- /package/{cli/dist → dist}/core/adapters/playwright-native-adapter.d.ts +0 -0
- /package/{cli/dist → dist}/core/adapters/playwright-native-adapter.js +0 -0
- /package/{cli/dist → dist}/core/adapters/pytest-adapter.d.ts +0 -0
- /package/{cli/dist → dist}/core/adapters/pytest-adapter.js +0 -0
- /package/{cli/dist → dist}/core/adapters/semgrep-sast.d.ts +0 -0
- /package/{cli/dist → dist}/core/adapters/semgrep-sast.js +0 -0
- /package/{cli/dist → dist}/core/adapters/unit-test-types.d.ts +0 -0
- /package/{cli/dist → dist}/core/adapters/unit-test-types.js +0 -0
- /package/{cli/dist → dist}/core/adapters/vitest-adapter.d.ts +0 -0
- /package/{cli/dist → dist}/core/adapters/vitest-adapter.js +0 -0
- /package/{cli/dist → dist}/core/adapters/zap-dast.d.ts +0 -0
- /package/{cli/dist → dist}/core/adapters/zap-dast.js +0 -0
- /package/{cli/dist → dist}/core/ai/anthropic-provider.d.ts +0 -0
- /package/{cli/dist → dist}/core/ai/anthropic-provider.js +0 -0
- /package/{cli/dist → dist}/core/ai/deepseek-provider.d.ts +0 -0
- /package/{cli/dist → dist}/core/ai/deepseek-provider.js +0 -0
- /package/{cli/dist → dist}/core/ai/index.d.ts +0 -0
- /package/{cli/dist → dist}/core/ai/index.js +0 -0
- /package/{cli/dist → dist}/core/ai/llm-client.d.ts +0 -0
- /package/{cli/dist → dist}/core/ai/llm-client.js +0 -0
- /package/{cli/dist → dist}/core/ai/mock-provider.d.ts +0 -0
- /package/{cli/dist → dist}/core/ai/mock-provider.js +0 -0
- /package/{cli/dist → dist}/core/ai/ollama-provider.d.ts +0 -0
- /package/{cli/dist → dist}/core/ai/openai-provider.d.ts +0 -0
- /package/{cli/dist → dist}/core/ai/openai-provider.js +0 -0
- /package/{cli/dist → dist}/core/ai/provider-factory.d.ts +0 -0
- /package/{cli/dist → dist}/core/ai/provider-factory.js +0 -0
- /package/{cli/dist → dist}/core/artifacts/index.d.ts +0 -0
- /package/{cli/dist → dist}/core/artifacts/index.js +0 -0
- /package/{cli/dist → dist}/core/artifacts/ui-artifacts.d.ts +0 -0
- /package/{cli/dist → dist}/core/assertions/engine.d.ts +0 -0
- /package/{cli/dist → dist}/core/assertions/engine.js +0 -0
- /package/{cli/dist → dist}/core/assertions/index.d.ts +0 -0
- /package/{cli/dist → dist}/core/assertions/index.js +0 -0
- /package/{cli/dist → dist}/core/assertions/types.d.ts +0 -0
- /package/{cli/dist → dist}/core/assertions/types.js +0 -0
- /package/{cli/dist → dist}/core/auth/api-key-provider.d.ts +0 -0
- /package/{cli/dist → dist}/core/auth/api-key-provider.js +0 -0
- /package/{cli/dist → dist}/core/auth/aws-iam-provider.d.ts +0 -0
- /package/{cli/dist → dist}/core/auth/aws-iam-provider.js +0 -0
- /package/{cli/dist → dist}/core/auth/azure-ad-provider.d.ts +0 -0
- /package/{cli/dist → dist}/core/auth/azure-ad-provider.js +0 -0
- /package/{cli/dist → dist}/core/auth/gcp-adc-provider.d.ts +0 -0
- /package/{cli/dist → dist}/core/auth/gcp-adc-provider.js +0 -0
- /package/{cli/dist → dist}/core/auth/jwt-provider.d.ts +0 -0
- /package/{cli/dist → dist}/core/auth/jwt-provider.js +0 -0
- /package/{cli/dist → dist}/core/auth/manager.d.ts +0 -0
- /package/{cli/dist → dist}/core/auth/manager.js +0 -0
- /package/{cli/dist → dist}/core/auth/totp-provider.d.ts +0 -0
- /package/{cli/dist → dist}/core/auth/totp-provider.js +0 -0
- /package/{cli/dist → dist}/core/auth/ui-login-provider.d.ts +0 -0
- /package/{cli/dist → dist}/core/auth/ui-login-provider.js +0 -0
- /package/{cli/dist → dist}/core/cache/index.d.ts +0 -0
- /package/{cli/dist → dist}/core/cache/index.js +0 -0
- /package/{cli/dist → dist}/core/cache/lru-cache.d.ts +0 -0
- /package/{cli/dist → dist}/core/cache/lru-cache.js +0 -0
- /package/{cli/dist/core → dist}/core/coverage/analyzer.d.ts +0 -0
- /package/{cli/dist/core → dist}/core/coverage/analyzer.js +0 -0
- /package/{cli/dist/core → dist}/core/coverage/collector.d.ts +0 -0
- /package/{cli/dist/core → dist}/core/coverage/collector.js +0 -0
- /package/{cli/dist/core → dist}/core/coverage/config.d.ts +0 -0
- /package/{cli/dist/core → dist}/core/coverage/config.js +0 -0
- /package/{cli/dist/core → dist}/core/coverage/index.d.ts +0 -0
- /package/{cli/dist/core → dist}/core/coverage/index.js +0 -0
- /package/{cli/dist/core → dist}/core/coverage/types.d.ts +0 -0
- /package/{cli/dist/core → dist}/core/coverage/types.js +0 -0
- /package/{cli/dist/core → dist}/core/coverage/vault.d.ts +0 -0
- /package/{cli/dist/core → dist}/core/coverage/vault.js +0 -0
- /package/{cli/dist → dist}/core/crawler/journey-generator.d.ts +0 -0
- /package/{cli/dist → dist}/core/crawler/types.js +0 -0
- /package/{cli/dist → dist}/core/dashboard/assets.d.ts +0 -0
- /package/{cli/dist → dist}/core/dashboard/assets.js +0 -0
- /package/{cli/dist → dist}/core/dashboard/index.d.ts +0 -0
- /package/{cli/dist → dist}/core/dashboard/index.js +0 -0
- /package/{cli/dist → dist}/core/dashboard/server.d.ts +0 -0
- /package/{cli/dist → dist}/core/dashboard/server.js +0 -0
- /package/{cli/dist → dist}/core/dashboard/types.d.ts +0 -0
- /package/{cli/dist → dist}/core/dashboard/types.js +0 -0
- /package/{cli/dist → dist}/core/discoverer/index.d.ts +0 -0
- /package/{cli/dist → dist}/core/discoverer/index.js +0 -0
- /package/{cli/dist → dist}/core/fixtures/loader.d.ts +0 -0
- /package/{cli/dist → dist}/core/fixtures/loader.js +0 -0
- /package/{cli/dist → dist}/core/fixtures/resolver.d.ts +0 -0
- /package/{cli/dist → dist}/core/fixtures/resolver.js +0 -0
- /package/{cli/dist → dist}/core/fixtures/types.d.ts +0 -0
- /package/{cli/dist → dist}/core/fixtures/types.js +0 -0
- /package/{cli/dist → dist}/core/flakiness/index.d.ts +0 -0
- /package/{cli/dist → dist}/core/flakiness/index.js +0 -0
- /package/{cli/dist → dist}/core/generation/code-formatter.d.ts +0 -0
- /package/{cli/dist → dist}/core/generation/code-formatter.js +0 -0
- /package/{cli/dist → dist}/core/generation/code-generator.d.ts +0 -0
- /package/{cli/dist → dist}/core/generation/code-generator.js +0 -0
- /package/{cli/dist → dist}/core/generation/generator.d.ts +0 -0
- /package/{cli/dist → dist}/core/generation/generator.js +0 -0
- /package/{cli/dist → dist}/core/generation/pack-generator.d.ts +0 -0
- /package/{cli/dist → dist}/core/generation/pack-generator.js +0 -0
- /package/{cli/dist → dist}/core/generation/prompt-builder.d.ts +0 -0
- /package/{cli/dist → dist}/core/generation/prompt-builder.js +0 -0
- /package/{cli/dist → dist}/core/generation/source-analyzer.d.ts +0 -0
- /package/{cli/dist → dist}/core/generation/source-analyzer.js +0 -0
- /package/{cli/dist → dist}/core/generation/test-optimizer.d.ts +0 -0
- /package/{cli/dist → dist}/core/generation/test-optimizer.js +0 -0
- /package/{cli/dist → dist}/core/generation/types.d.ts +0 -0
- /package/{cli/dist → dist}/core/generation/types.js +0 -0
- /package/{cli/dist → dist}/core/hooks/compose.d.ts +0 -0
- /package/{cli/dist → dist}/core/hooks/compose.js +0 -0
- /package/{cli/dist → dist}/core/hooks/runner.d.ts +0 -0
- /package/{cli/dist → dist}/core/hooks/runner.js +0 -0
- /package/{cli/dist → dist}/core/pack/migrator.d.ts +0 -0
- /package/{cli/dist → dist}/core/pack/migrator.js +0 -0
- /package/{cli/dist → dist}/core/pack/validator.d.ts +0 -0
- /package/{cli/dist → dist}/core/pack-v2/index.d.ts +0 -0
- /package/{cli/dist → dist}/core/pack-v2/index.js +0 -0
- /package/{cli/dist → dist}/core/pack-v2/loader.d.ts +0 -0
- /package/{cli/dist → dist}/core/pack-v2/loader.js +0 -0
- /package/{cli/dist → dist}/core/pack-v2/validator.d.ts +0 -0
- /package/{cli/dist → dist}/core/parallel/index.d.ts +0 -0
- /package/{cli/dist → dist}/core/parallel/index.js +0 -0
- /package/{cli/dist → dist}/core/parallel/parallel-runner.d.ts +0 -0
- /package/{cli/dist → dist}/core/parallel/parallel-runner.js +0 -0
- /package/{cli/dist → dist}/core/pom/base-page.d.ts +0 -0
- /package/{cli/dist → dist}/core/pom/index.d.ts +0 -0
- /package/{cli/dist → dist}/core/pom/index.js +0 -0
- /package/{cli/dist → dist}/core/pom/loader.d.ts +0 -0
- /package/{cli/dist → dist}/core/pom/types.d.ts +0 -0
- /package/{cli/dist → dist}/core/pom/types.js +0 -0
- /package/{cli/dist → dist}/core/proof/bundle.d.ts +0 -0
- /package/{cli/dist → dist}/core/proof/bundle.js +0 -0
- /package/{cli/dist → dist}/core/proof/canonicalize.d.ts +0 -0
- /package/{cli/dist → dist}/core/proof/canonicalize.js +0 -0
- /package/{cli/dist → dist}/core/proof/index.d.ts +0 -0
- /package/{cli/dist → dist}/core/proof/index.js +0 -0
- /package/{cli/dist → dist}/core/proof/schema.d.ts +0 -0
- /package/{cli/dist → dist}/core/proof/schema.js +0 -0
- /package/{cli/dist → dist}/core/proof/signer.d.ts +0 -0
- /package/{cli/dist → dist}/core/proof/signer.js +0 -0
- /package/{cli/dist → dist}/core/proof/verifier.d.ts +0 -0
- /package/{cli/dist → dist}/core/proof/verifier.js +0 -0
- /package/{cli/dist → dist}/core/regression/detector.d.ts +0 -0
- /package/{cli/dist → dist}/core/regression/detector.js +0 -0
- /package/{cli/dist → dist}/core/regression/index.d.ts +0 -0
- /package/{cli/dist → dist}/core/regression/index.js +0 -0
- /package/{cli/dist → dist}/core/regression/trend-analyzer.d.ts +0 -0
- /package/{cli/dist → dist}/core/regression/trend-analyzer.js +0 -0
- /package/{cli/dist → dist}/core/regression/types.d.ts +0 -0
- /package/{cli/dist → dist}/core/regression/types.js +0 -0
- /package/{cli/dist → dist}/core/regression/vault.d.ts +0 -0
- /package/{cli/dist → dist}/core/regression/vault.js +0 -0
- /package/{cli/dist → dist}/core/repair/engine/fixer.d.ts +0 -0
- /package/{cli/dist → dist}/core/repair/engine/fixer.js +0 -0
- /package/{cli/dist → dist}/core/repair/engine/suggestion-engine.d.ts +0 -0
- /package/{cli/dist → dist}/core/repair/engine/suggestion-engine.js +0 -0
- /package/{cli/dist → dist}/core/repair/index.d.ts +0 -0
- /package/{cli/dist → dist}/core/repair/index.js +0 -0
- /package/{cli/dist → dist}/core/repair/repairer.d.ts +0 -0
- /package/{cli/dist → dist}/core/repair/repairer.js +0 -0
- /package/{cli/dist → dist}/core/repair/types.d.ts +0 -0
- /package/{cli/dist → dist}/core/repair/types.js +0 -0
- /package/{cli/dist → dist}/core/repair/utils/error-analyzer.d.ts +0 -0
- /package/{cli/dist → dist}/core/repair/utils/error-analyzer.js +0 -0
- /package/{cli/dist → dist}/core/reporting/html-reporter.d.ts +0 -0
- /package/{cli/dist → dist}/core/reporting/html-reporter.js +0 -0
- /package/{cli/dist → dist}/core/retry/flakiness-integration.d.ts +0 -0
- /package/{cli/dist → dist}/core/retry/flakiness-integration.js +0 -0
- /package/{cli/dist → dist}/core/retry/index.d.ts +0 -0
- /package/{cli/dist → dist}/core/retry/index.js +0 -0
- /package/{cli/dist → dist}/core/retry/retry-engine.d.ts +0 -0
- /package/{cli/dist → dist}/core/retry/retry-engine.js +0 -0
- /package/{cli/dist → dist}/core/retry/types.d.ts +0 -0
- /package/{cli/dist → dist}/core/retry/types.js +0 -0
- /package/{cli/dist → dist}/core/retry/vault.d.ts +0 -0
- /package/{cli/dist → dist}/core/retry/vault.js +0 -0
- /package/{cli/dist → dist}/core/schemas/pack.schema.json +0 -0
- /package/{cli/dist → dist}/core/secrets/crypto.d.ts +0 -0
- /package/{cli/dist → dist}/core/secrets/crypto.js +0 -0
- /package/{cli/dist → dist}/core/secrets/manager.d.ts +0 -0
- /package/{cli/dist → dist}/core/secrets/manager.js +0 -0
- /package/{cli/dist → dist}/core/security/redaction-patterns-extended.d.ts +0 -0
- /package/{cli/dist → dist}/core/security/redaction-patterns-extended.js +0 -0
- /package/{cli/dist → dist}/core/security/redactor.d.ts +0 -0
- /package/{cli/dist → dist}/core/security/redactor.js +0 -0
- /package/{cli/dist → dist}/core/self-healing/assertion-healer.d.ts +0 -0
- /package/{cli/dist → dist}/core/self-healing/assertion-healer.js +0 -0
- /package/{cli/dist → dist}/core/self-healing/engine.d.ts +0 -0
- /package/{cli/dist → dist}/core/self-healing/engine.js +0 -0
- /package/{cli/dist → dist}/core/self-healing/index.d.ts +0 -0
- /package/{cli/dist → dist}/core/self-healing/index.js +0 -0
- /package/{cli/dist → dist}/core/self-healing/selector-healer.d.ts +0 -0
- /package/{cli/dist → dist}/core/self-healing/selector-healer.js +0 -0
- /package/{cli/dist → dist}/core/self-healing/types.d.ts +0 -0
- /package/{cli/dist → dist}/core/self-healing/types.js +0 -0
- /package/{cli/dist → dist}/core/serve/diagnostics-collector.d.ts +0 -0
- /package/{cli/dist → dist}/core/serve/diagnostics-collector.js +0 -0
- /package/{cli/dist → dist}/core/serve/health-checker.d.ts +0 -0
- /package/{cli/dist → dist}/core/serve/health-checker.js +0 -0
- /package/{cli/dist → dist}/core/serve/index.d.ts +0 -0
- /package/{cli/dist → dist}/core/serve/index.js +0 -0
- /package/{cli/dist → dist}/core/serve/metrics-collector.d.ts +0 -0
- /package/{cli/dist → dist}/core/serve/metrics-collector.js +0 -0
- /package/{cli/dist → dist}/core/serve/process-manager.d.ts +0 -0
- /package/{cli/dist → dist}/core/serve/process-manager.js +0 -0
- /package/{cli/dist → dist}/core/serve/server.d.ts +0 -0
- /package/{cli/dist → dist}/core/serve/server.js +0 -0
- /package/{cli/dist → dist}/core/slo/config.d.ts +0 -0
- /package/{cli/dist → dist}/core/slo/config.js +0 -0
- /package/{cli/dist → dist}/core/slo/index.d.ts +0 -0
- /package/{cli/dist → dist}/core/slo/index.js +0 -0
- /package/{cli/dist → dist}/core/slo/sli-calculator.d.ts +0 -0
- /package/{cli/dist → dist}/core/slo/sli-calculator.js +0 -0
- /package/{cli/dist → dist}/core/slo/slo-tracker.d.ts +0 -0
- /package/{cli/dist → dist}/core/slo/slo-tracker.js +0 -0
- /package/{cli/dist → dist}/core/slo/types.d.ts +0 -0
- /package/{cli/dist → dist}/core/slo/types.js +0 -0
- /package/{cli/dist → dist}/core/slo/vault.d.ts +0 -0
- /package/{cli/dist → dist}/core/slo/vault.js +0 -0
- /package/{cli/dist → dist}/core/tui/index.d.ts +0 -0
- /package/{cli/dist → dist}/core/tui/index.js +0 -0
- /package/{cli/dist → dist}/core/tui/monitor.d.ts +0 -0
- /package/{cli/dist → dist}/core/tui/monitor.js +0 -0
- /package/{cli/dist → dist}/core/tui/renderer.d.ts +0 -0
- /package/{cli/dist → dist}/core/tui/renderer.js +0 -0
- /package/{cli/dist → dist}/core/tui/types.d.ts +0 -0
- /package/{cli/dist → dist}/core/tui/types.js +0 -0
- /package/{cli/dist → dist}/core/types/pack-v1.js +0 -0
- /package/{cli/dist → dist}/core/types/pack-v2.js +0 -0
- /package/{cli/dist → dist}/core/types/trust-score.d.ts +0 -0
- /package/{cli/dist → dist}/core/types/trust-score.js +0 -0
- /package/{cli/dist → dist}/core/vault/cas.d.ts +0 -0
- /package/{cli/dist → dist}/core/vault/cas.js +0 -0
- /package/{cli/dist → dist}/core/vault/index.d.ts +0 -0
- /package/{cli/dist → dist}/core/vault/index.js +0 -0
- /package/{cli/dist → dist}/core/visual/visual-regression.d.ts +0 -0
- /package/{cli/dist → dist}/core/visual/visual-regression.js +0 -0
- /package/{cli/dist → dist}/core/watch/index.d.ts +0 -0
- /package/{cli/dist → dist}/core/watch/index.js +0 -0
- /package/{cli/dist → dist}/core/watch/watch-mode.d.ts +0 -0
- /package/{cli/dist → dist}/core/watch/watch-mode.js +0 -0
- /package/{cli/dist → dist}/generators/index.d.ts +0 -0
- /package/{cli/dist → dist}/generators/index.js +0 -0
- /package/{cli/dist → dist}/generators/json-reporter.d.ts +0 -0
- /package/{cli/dist → dist}/generators/json-reporter.js +0 -0
- /package/{cli/dist → dist}/generators/test-generator.d.ts +0 -0
- /package/{cli/dist → dist}/generators/test-generator.js +0 -0
- /package/{cli/dist → dist}/index.d.ts +0 -0
- /package/{cli/dist → dist}/index.js +0 -0
- /package/{cli/dist → dist}/scanners/dom-scanner.d.ts +0 -0
- /package/{cli/dist → dist}/scanners/dom-scanner.js +0 -0
- /package/{cli/dist → dist}/scanners/index.d.ts +0 -0
- /package/{cli/dist → dist}/scanners/index.js +0 -0
- /package/{cli/dist → dist}/schemas/pack.schema.json +0 -0
- /package/{cli/dist → dist}/types/scan.d.ts +0 -0
- /package/{cli/dist → dist}/types/scan.js +0 -0
- /package/{cli/dist → dist}/utils/config.js +0 -0
|
@@ -0,0 +1,1069 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Advanced Interactions Handler
|
|
3
|
+
*
|
|
4
|
+
* P0 Features for Enterprise-Grade Testing:
|
|
5
|
+
* - Shadow DOM support (auto-piercing)
|
|
6
|
+
* - Iframe handling (same-origin + cross-origin)
|
|
7
|
+
* - File upload/download
|
|
8
|
+
*
|
|
9
|
+
* @since 2.3.0
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Advanced Interactions Handler
|
|
13
|
+
*
|
|
14
|
+
* Handles complex DOM interactions that go beyond simple element selection.
|
|
15
|
+
*/
|
|
16
|
+
export class AdvancedInteractionsHandler {
|
|
17
|
+
page;
|
|
18
|
+
options;
|
|
19
|
+
// Track discovered shadow roots for reuse
|
|
20
|
+
shadowRootCache = new Map();
|
|
21
|
+
// Track discovered frames
|
|
22
|
+
frameCache = new Map();
|
|
23
|
+
constructor(page, options = {}) {
|
|
24
|
+
this.page = page;
|
|
25
|
+
this.options = {
|
|
26
|
+
maxShadowDepth: options.maxShadowDepth ?? 5,
|
|
27
|
+
autoShadowPiercing: options.autoShadowPiercing ?? true,
|
|
28
|
+
handleIframes: options.handleIframes ?? true,
|
|
29
|
+
frameTimeout: options.frameTimeout ?? 5000,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
34
|
+
* SHADOW DOM SUPPORT
|
|
35
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Find an element with automatic shadow DOM piercing
|
|
39
|
+
*
|
|
40
|
+
* Attempts to find an element in the main DOM first,
|
|
41
|
+
* then recursively searches through shadow DOM trees.
|
|
42
|
+
*
|
|
43
|
+
* @param selector - CSS selector to search for
|
|
44
|
+
* @param options - Search options
|
|
45
|
+
* @returns Element location with context
|
|
46
|
+
*/
|
|
47
|
+
async findWithShadowPiercing(selector, options = {}) {
|
|
48
|
+
const { timeout = 5000, maxDepth = this.options.maxShadowDepth } = options;
|
|
49
|
+
// First, try normal locator (non-shadow DOM)
|
|
50
|
+
// Use JavaScript to check if element exists in regular DOM only
|
|
51
|
+
const inRegularDOM = await this.page.evaluate((sel) => {
|
|
52
|
+
const element = document.querySelector(sel);
|
|
53
|
+
if (!element)
|
|
54
|
+
return false;
|
|
55
|
+
// Check if element is in a shadow root using getRootNode()
|
|
56
|
+
// getRootNode() returns the ShadowRoot if element is within one,
|
|
57
|
+
// otherwise returns the Document
|
|
58
|
+
const rootNode = element.getRootNode();
|
|
59
|
+
return rootNode === document; // true if in regular DOM, false if in shadow DOM
|
|
60
|
+
}, selector).catch(() => false);
|
|
61
|
+
if (inRegularDOM) {
|
|
62
|
+
// Element exists in regular DOM, not shadow DOM
|
|
63
|
+
const normalLocator = this.page.locator(selector).first();
|
|
64
|
+
return {
|
|
65
|
+
locator: normalLocator,
|
|
66
|
+
isInShadowDom: false,
|
|
67
|
+
shadowDepth: 0,
|
|
68
|
+
isInIframe: false,
|
|
69
|
+
path: [selector],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// Shadow DOM piercing not enabled
|
|
73
|
+
if (!this.options.autoShadowPiercing) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
// Search through shadow DOM
|
|
77
|
+
const result = await this.searchInShadowDom(selector, maxDepth);
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Recursively search for element in shadow DOM
|
|
82
|
+
* Supports nested shadow DOM (shadow roots within shadow roots)
|
|
83
|
+
*/
|
|
84
|
+
async searchInShadowDom(selector, maxDepth, currentDepth = 0) {
|
|
85
|
+
if (currentDepth >= maxDepth) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
// JavaScript-based shadow DOM piercing with nested shadow root support
|
|
89
|
+
const result = await this.page.evaluate(({ sel, maxD }) => {
|
|
90
|
+
// Helper to get shadow root
|
|
91
|
+
function getShadowRoot(element) {
|
|
92
|
+
if ('shadowRoot' in element) {
|
|
93
|
+
return element.shadowRoot;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
// BFS through shadow DOM with nested shadow root tracking
|
|
98
|
+
// Start with document.body, not in shadow yet
|
|
99
|
+
const queue = [
|
|
100
|
+
{ element: document.body, shadowDepth: 0, path: [], parentShadowRoot: null },
|
|
101
|
+
];
|
|
102
|
+
const visited = new Set();
|
|
103
|
+
let deepestShadowDepth = 0;
|
|
104
|
+
while (queue.length > 0) {
|
|
105
|
+
const { element, shadowDepth, path, parentShadowRoot } = queue.shift();
|
|
106
|
+
if (visited.has(element))
|
|
107
|
+
continue;
|
|
108
|
+
visited.add(element);
|
|
109
|
+
// Check if element has shadow root
|
|
110
|
+
const shadowRoot = getShadowRoot(element);
|
|
111
|
+
if (shadowRoot) {
|
|
112
|
+
// Track deepest shadow depth encountered
|
|
113
|
+
const newShadowDepth = shadowDepth + 1;
|
|
114
|
+
if (newShadowDepth > deepestShadowDepth) {
|
|
115
|
+
deepestShadowDepth = newShadowDepth;
|
|
116
|
+
}
|
|
117
|
+
// Check max depth
|
|
118
|
+
if (newShadowDepth > maxD) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// Search in this shadow root
|
|
122
|
+
const found = shadowRoot.querySelector(sel);
|
|
123
|
+
if (found) {
|
|
124
|
+
return {
|
|
125
|
+
found: true,
|
|
126
|
+
path: path,
|
|
127
|
+
depth: newShadowDepth,
|
|
128
|
+
shadowDepth: newShadowDepth,
|
|
129
|
+
tagName: found.tagName.toLowerCase(),
|
|
130
|
+
id: found.id,
|
|
131
|
+
className: found.className,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// Add all children in this shadow root to queue
|
|
135
|
+
// They are at the new shadow depth
|
|
136
|
+
const shadowChildren = Array.from(shadowRoot.children);
|
|
137
|
+
for (let i = 0; i < shadowChildren.length; i++) {
|
|
138
|
+
const child = shadowChildren[i];
|
|
139
|
+
// Check if this child has its own shadow root (nested shadow)
|
|
140
|
+
const childShadowRoot = getShadowRoot(child);
|
|
141
|
+
if (childShadowRoot) {
|
|
142
|
+
// Nested shadow root - add at increased depth
|
|
143
|
+
queue.push({
|
|
144
|
+
element: child,
|
|
145
|
+
shadowDepth: newShadowDepth,
|
|
146
|
+
path: [...path, i],
|
|
147
|
+
parentShadowRoot: shadowRoot,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// Regular element within shadow root
|
|
152
|
+
queue.push({
|
|
153
|
+
element: child,
|
|
154
|
+
shadowDepth: newShadowDepth,
|
|
155
|
+
path: [...path, i],
|
|
156
|
+
parentShadowRoot: shadowRoot,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else if (parentShadowRoot) {
|
|
162
|
+
// Element is within a shadow root but doesn't have its own shadow
|
|
163
|
+
// Add its children at the same shadow depth
|
|
164
|
+
const children = Array.from(element.children);
|
|
165
|
+
for (let i = 0; i < children.length; i++) {
|
|
166
|
+
const child = children[i];
|
|
167
|
+
const childShadowRoot = getShadowRoot(child);
|
|
168
|
+
queue.push({
|
|
169
|
+
element: child,
|
|
170
|
+
shadowDepth: childShadowRoot ? shadowDepth + 1 : shadowDepth,
|
|
171
|
+
path: [...path, i],
|
|
172
|
+
parentShadowRoot: parentShadowRoot,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
// Element is in regular DOM (before first shadow root)
|
|
178
|
+
const children = Array.from(element.children);
|
|
179
|
+
for (let i = 0; i < children.length; i++) {
|
|
180
|
+
queue.push({
|
|
181
|
+
element: children[i],
|
|
182
|
+
shadowDepth: 0,
|
|
183
|
+
path: [...path, i],
|
|
184
|
+
parentShadowRoot: null,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return { found: false };
|
|
190
|
+
}, { sel: selector, maxD: maxDepth });
|
|
191
|
+
if (!result || !result.found) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const shadowResult = result;
|
|
195
|
+
// Create locator for the found element
|
|
196
|
+
const shadowSelector = this.buildShadowDomSelector(selector, shadowResult.shadowDepth ?? 0, shadowResult.path);
|
|
197
|
+
return {
|
|
198
|
+
locator: this.page.locator(shadowSelector).first(),
|
|
199
|
+
isInShadowDom: true,
|
|
200
|
+
shadowDepth: shadowResult.shadowDepth ?? 0,
|
|
201
|
+
nestedShadowDepth: shadowResult.shadowDepth ?? 0,
|
|
202
|
+
isInIframe: false,
|
|
203
|
+
path: [shadowSelector],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Build a Playwright-compatible selector for shadow DOM elements
|
|
208
|
+
*
|
|
209
|
+
* Playwright supports shadow DOM piercing with >> >>
|
|
210
|
+
*/
|
|
211
|
+
buildShadowDomSelector(baseSelector, depth, path) {
|
|
212
|
+
if (depth === 0) {
|
|
213
|
+
return baseSelector;
|
|
214
|
+
}
|
|
215
|
+
// For shadow DOM, we need to pierce through shadow roots
|
|
216
|
+
// Playwright supports: selector >> >>> inner-selector
|
|
217
|
+
// But we don't know the exact path, so we use a generic approach
|
|
218
|
+
// For now, use the base selector - the actual shadow piercing
|
|
219
|
+
// will be done via JavaScript in the action methods
|
|
220
|
+
return baseSelector;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Click an element with shadow DOM support
|
|
224
|
+
*/
|
|
225
|
+
async clickWithShadowPiercing(selector) {
|
|
226
|
+
const location = await this.findWithShadowPiercing(selector);
|
|
227
|
+
if (!location) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
if (location.isInShadowDom) {
|
|
232
|
+
// For shadow DOM, use JavaScript click with proper shadow piercing
|
|
233
|
+
const result = await this.page.evaluate((sel) => {
|
|
234
|
+
// Find element in shadow DOM
|
|
235
|
+
const findInShadow = (root) => {
|
|
236
|
+
// Direct match
|
|
237
|
+
const direct = root.querySelector(sel);
|
|
238
|
+
if (direct)
|
|
239
|
+
return direct;
|
|
240
|
+
// Search in shadow roots
|
|
241
|
+
const elements = root.querySelectorAll('*');
|
|
242
|
+
for (const el of elements) {
|
|
243
|
+
if ('shadowRoot' in el) {
|
|
244
|
+
const shadow = el.shadowRoot;
|
|
245
|
+
if (shadow) {
|
|
246
|
+
const found = findInShadow(shadow);
|
|
247
|
+
if (found)
|
|
248
|
+
return found;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
};
|
|
254
|
+
const element = findInShadow(document);
|
|
255
|
+
if (element) {
|
|
256
|
+
element.click();
|
|
257
|
+
return { found: true };
|
|
258
|
+
}
|
|
259
|
+
return { found: false };
|
|
260
|
+
}, selector);
|
|
261
|
+
return result?.found ?? false;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
// Normal DOM - use Playwright locator
|
|
265
|
+
await location.locator.click();
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Fill an input with shadow DOM support
|
|
275
|
+
*/
|
|
276
|
+
async fillWithShadowPiercing(selector, value) {
|
|
277
|
+
const location = await this.findWithShadowPiercing(selector);
|
|
278
|
+
if (!location) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
if (location.isInShadowDom) {
|
|
283
|
+
const result = await this.page.evaluate(({ sel, val }) => {
|
|
284
|
+
const findInShadow = (root) => {
|
|
285
|
+
const direct = root.querySelector(sel);
|
|
286
|
+
if (direct)
|
|
287
|
+
return direct;
|
|
288
|
+
const elements = root.querySelectorAll('*');
|
|
289
|
+
for (const el of elements) {
|
|
290
|
+
if ('shadowRoot' in el) {
|
|
291
|
+
const shadow = el.shadowRoot;
|
|
292
|
+
if (shadow) {
|
|
293
|
+
const found = findInShadow(shadow);
|
|
294
|
+
if (found)
|
|
295
|
+
return found;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
};
|
|
301
|
+
const element = findInShadow(document);
|
|
302
|
+
if (element) {
|
|
303
|
+
element.value = val;
|
|
304
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
305
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
return false;
|
|
309
|
+
}, { sel: selector, val: value });
|
|
310
|
+
return result ?? false;
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
await location.locator.fill(value);
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
323
|
+
* IFRAME HANDLING
|
|
324
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
325
|
+
*/
|
|
326
|
+
/**
|
|
327
|
+
* Find an element within iframes
|
|
328
|
+
*
|
|
329
|
+
* Searches through same-origin iframes recursively.
|
|
330
|
+
*
|
|
331
|
+
* @param selector - CSS selector to search for
|
|
332
|
+
* @param options - Search options
|
|
333
|
+
* @returns Element location with iframe context
|
|
334
|
+
*/
|
|
335
|
+
async findInIframes(selector, options = {}) {
|
|
336
|
+
const { timeout = 5000, maxDepth = 3, allowCrossOrigin = true } = options;
|
|
337
|
+
if (!this.options.handleIframes) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
const baseUrl = this.page.url();
|
|
341
|
+
// First, try same-origin iframes (faster, can use frame API directly)
|
|
342
|
+
const sameOriginResult = await this.findInSameOriginIframes(selector, { timeout, maxDepth, baseUrl });
|
|
343
|
+
if (sameOriginResult) {
|
|
344
|
+
return sameOriginResult;
|
|
345
|
+
}
|
|
346
|
+
// Then try cross-origin iframes if allowed
|
|
347
|
+
if (allowCrossOrigin) {
|
|
348
|
+
return await this.findInCrossOriginIframes(selector, { timeout, maxDepth });
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Find element in same-origin iframes (faster, direct frame access)
|
|
354
|
+
*/
|
|
355
|
+
async findInSameOriginIframes(selector, options) {
|
|
356
|
+
const { timeout, maxDepth, baseUrl } = options;
|
|
357
|
+
const frames = this.page.frames();
|
|
358
|
+
for (const frame of frames) {
|
|
359
|
+
if (frame === this.page.mainFrame()) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
const frameUrl = frame.url();
|
|
364
|
+
// Skip cross-origin or empty frames
|
|
365
|
+
if (!frameUrl || !this.isSameOrigin(baseUrl, frameUrl)) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
// Use frame's waitForSelector (works for same-origin)
|
|
369
|
+
const element = await frame.waitForSelector(selector, { timeout: timeout / frames.length }).catch(() => null);
|
|
370
|
+
if (element) {
|
|
371
|
+
const frameSelector = await this.getFrameSelector(frame);
|
|
372
|
+
const locator = this.page.frameLocator(frameSelector || 'iframe').locator(selector);
|
|
373
|
+
return {
|
|
374
|
+
locator,
|
|
375
|
+
isInShadowDom: false,
|
|
376
|
+
shadowDepth: 0,
|
|
377
|
+
isInIframe: true,
|
|
378
|
+
isCrossOrigin: false,
|
|
379
|
+
frameSelector,
|
|
380
|
+
path: [`${frameSelector || 'iframe'} >> ${selector}`],
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
// Recursively search nested iframes
|
|
384
|
+
const nestedResult = await this.findInNestedFrames(frame, selector, maxDepth - 1);
|
|
385
|
+
if (nestedResult) {
|
|
386
|
+
return nestedResult;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
// Continue to next frame
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Find element in cross-origin iframes using frameLocator
|
|
397
|
+
*
|
|
398
|
+
* For cross-origin iframes, we cannot use frame.waitForSelector() due to SOP.
|
|
399
|
+
* Instead, we use Playwright's frameLocator which handles cross-origin transparently.
|
|
400
|
+
*/
|
|
401
|
+
async findInCrossOriginIframes(selector, options) {
|
|
402
|
+
const { timeout, maxDepth } = options;
|
|
403
|
+
// Get all iframe elements in the page
|
|
404
|
+
const iframeElements = await this.page.$$('iframe');
|
|
405
|
+
for (const iframe of iframeElements) {
|
|
406
|
+
try {
|
|
407
|
+
// Get iframe selector for this iframe
|
|
408
|
+
const frameSelector = await this.getFrameSelectorForElement(iframe);
|
|
409
|
+
// Use frameLocator to check if element exists (works for cross-origin)
|
|
410
|
+
const frameLocator = this.page.frameLocator(frameSelector);
|
|
411
|
+
const elementLocator = frameLocator.locator(selector);
|
|
412
|
+
// Try to wait for element with short timeout
|
|
413
|
+
try {
|
|
414
|
+
await elementLocator.waitFor({ state: 'attached', timeout: timeout / iframeElements.length });
|
|
415
|
+
// Element found!
|
|
416
|
+
return {
|
|
417
|
+
locator: elementLocator,
|
|
418
|
+
isInShadowDom: false,
|
|
419
|
+
shadowDepth: 0,
|
|
420
|
+
isInIframe: true,
|
|
421
|
+
isCrossOrigin: true,
|
|
422
|
+
frameSelector,
|
|
423
|
+
path: [`${frameSelector} >> ${selector}`],
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// Element not found in this iframe, continue
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
// Error accessing this iframe, continue
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Recursively search nested iframes
|
|
438
|
+
*/
|
|
439
|
+
async findInNestedFrames(parentFrame, selector, maxDepth) {
|
|
440
|
+
if (maxDepth <= 0) {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
const childFrames = parentFrame.childFrames();
|
|
444
|
+
for (const frame of childFrames) {
|
|
445
|
+
try {
|
|
446
|
+
// Use frame's waitForSelector instead of locator
|
|
447
|
+
const element = await frame.waitForSelector(selector, { timeout: 1000 }).catch(() => null);
|
|
448
|
+
if (element) {
|
|
449
|
+
const frameSelector = await this.getFrameSelector(frame);
|
|
450
|
+
const page = frame.page();
|
|
451
|
+
// Create a locator using page.frameLocator
|
|
452
|
+
const locator = (page || this.page).frameLocator(frameSelector || 'iframe').locator(selector);
|
|
453
|
+
return {
|
|
454
|
+
locator,
|
|
455
|
+
isInShadowDom: false,
|
|
456
|
+
shadowDepth: 0,
|
|
457
|
+
isInIframe: true,
|
|
458
|
+
frameSelector,
|
|
459
|
+
path: [`${frameSelector || 'iframe'} >> ${selector}`],
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
// Recurse deeper
|
|
463
|
+
const nestedResult = await this.findInNestedFrames(frame, selector, maxDepth - 1);
|
|
464
|
+
if (nestedResult) {
|
|
465
|
+
return nestedResult;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
// Continue
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Get the selector for a frame
|
|
476
|
+
*/
|
|
477
|
+
async getFrameSelector(frame) {
|
|
478
|
+
// Try to find the iframe element in the parent
|
|
479
|
+
const page = frame.page();
|
|
480
|
+
const frameUrl = frame.url();
|
|
481
|
+
// Search for iframe with matching src
|
|
482
|
+
const iframes = await page.$$('iframe');
|
|
483
|
+
for (const iframe of iframes) {
|
|
484
|
+
const src = await iframe.getAttribute('src');
|
|
485
|
+
const name = await iframe.getAttribute('name');
|
|
486
|
+
if (src && frameUrl.includes(src)) {
|
|
487
|
+
// Found by src - generate selector
|
|
488
|
+
const id = await iframe.getAttribute('id');
|
|
489
|
+
if (id) {
|
|
490
|
+
return `#${id}`;
|
|
491
|
+
}
|
|
492
|
+
if (name) {
|
|
493
|
+
return `iframe[name="${name}"]`;
|
|
494
|
+
}
|
|
495
|
+
// Use nth
|
|
496
|
+
const index = await page.$$eval('iframe', (frames, target) => {
|
|
497
|
+
return frames.findIndex((f) => f === target);
|
|
498
|
+
}, iframe);
|
|
499
|
+
return `iframe >> nth=${index}`;
|
|
500
|
+
}
|
|
501
|
+
if (name) {
|
|
502
|
+
return `iframe[name="${name}"]`;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
// Fallback: use frame's name
|
|
506
|
+
return `iframe`;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Get the selector for an iframe element
|
|
510
|
+
* Used for cross-origin iframe handling where we have an ElementHandle
|
|
511
|
+
*/
|
|
512
|
+
async getFrameSelectorForElement(iframe) {
|
|
513
|
+
// Try to get a unique identifier for the iframe
|
|
514
|
+
const id = await iframe.getAttribute('id');
|
|
515
|
+
if (id) {
|
|
516
|
+
return `#${id}`;
|
|
517
|
+
}
|
|
518
|
+
const name = await iframe.getAttribute('name');
|
|
519
|
+
if (name) {
|
|
520
|
+
return `iframe[name="${name}"]`;
|
|
521
|
+
}
|
|
522
|
+
// Try to get src for identification
|
|
523
|
+
const src = await iframe.getAttribute('src');
|
|
524
|
+
if (src) {
|
|
525
|
+
// Use src as part of selector (with wildcard for partial matches)
|
|
526
|
+
// Note: For Playwright frameLocator, we need a reliable selector
|
|
527
|
+
// Try to find index by comparing with all iframes
|
|
528
|
+
const iframes = await this.page.$$('iframe');
|
|
529
|
+
for (let i = 0; i < iframes.length; i++) {
|
|
530
|
+
if (await iframes[i].evaluate((el, target) => el === target, iframe)) {
|
|
531
|
+
return `iframe >> nth=${i}`;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// Fallback: use iframe with nth
|
|
536
|
+
const iframes = await this.page.$$('iframe');
|
|
537
|
+
for (let i = 0; i < iframes.length; i++) {
|
|
538
|
+
if (await iframes[i].evaluate((el, target) => el === target, iframe)) {
|
|
539
|
+
return `iframe >> nth=${i}`;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return 'iframe';
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Check if two URLs are same-origin
|
|
546
|
+
*/
|
|
547
|
+
isSameOrigin(url1, url2) {
|
|
548
|
+
try {
|
|
549
|
+
const u1 = new URL(url1);
|
|
550
|
+
const u2 = new URL(url2);
|
|
551
|
+
return u1.origin === u2.origin;
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Interact with element in iframe
|
|
559
|
+
*
|
|
560
|
+
* @param iframeSelector - Selector for the iframe
|
|
561
|
+
* @param elementSelector - Selector for element within iframe
|
|
562
|
+
* @param action - Action to perform
|
|
563
|
+
* @returns Action result
|
|
564
|
+
*/
|
|
565
|
+
async interactInIframe(iframeSelector, elementSelector, action) {
|
|
566
|
+
const frameLocator = this.page.frameLocator(iframeSelector);
|
|
567
|
+
const frame = this.page.frame(iframeSelector);
|
|
568
|
+
if (!frame) {
|
|
569
|
+
throw new Error(`Frame not found: ${iframeSelector}`);
|
|
570
|
+
}
|
|
571
|
+
return await action(frame);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
575
|
+
* FILE OPERATIONS
|
|
576
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
577
|
+
*/
|
|
578
|
+
/**
|
|
579
|
+
* Upload files to an input element
|
|
580
|
+
*
|
|
581
|
+
* @param selector - File input selector
|
|
582
|
+
* @param filePaths - Paths to files to upload
|
|
583
|
+
* @returns Upload result
|
|
584
|
+
*/
|
|
585
|
+
async uploadFiles(selector, filePaths) {
|
|
586
|
+
const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
|
|
587
|
+
try {
|
|
588
|
+
// First try normal upload
|
|
589
|
+
const locator = this.page.locator(selector);
|
|
590
|
+
const count = await locator.count();
|
|
591
|
+
if (count === 0) {
|
|
592
|
+
// Try in shadow DOM with a shorter timeout for quick existence check
|
|
593
|
+
const shadowLocation = await this.findWithShadowPiercing(selector, { timeout: 1000 });
|
|
594
|
+
if (!shadowLocation) {
|
|
595
|
+
return {
|
|
596
|
+
success: false,
|
|
597
|
+
count: 0,
|
|
598
|
+
fileNames: [],
|
|
599
|
+
error: `Element not found: ${selector}`,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
// For shadow DOM, we need to use JS to set files
|
|
603
|
+
const result = await this.uploadFilesInShadowDom(selector, paths);
|
|
604
|
+
return result;
|
|
605
|
+
}
|
|
606
|
+
// Use Playwright's setInputFiles
|
|
607
|
+
await locator.setInputFiles(paths);
|
|
608
|
+
return {
|
|
609
|
+
success: true,
|
|
610
|
+
count: paths.length,
|
|
611
|
+
fileNames: paths.map(p => p.split('/').pop() || p.split('\\').pop() || p),
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
return {
|
|
616
|
+
success: false,
|
|
617
|
+
count: 0,
|
|
618
|
+
fileNames: [],
|
|
619
|
+
error: error instanceof Error ? error.message : String(error),
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Upload files to an input in shadow DOM
|
|
625
|
+
*/
|
|
626
|
+
async uploadFilesInShadowDom(selector, filePaths) {
|
|
627
|
+
// For shadow DOM, we need to:
|
|
628
|
+
// 1. Find the element
|
|
629
|
+
// 2. Create a FileList object
|
|
630
|
+
// 3. Set it on the input
|
|
631
|
+
// This is complex - for now, try using the page's evaluate with the file paths
|
|
632
|
+
// In a real scenario, you'd need to read the files as buffers/DataURLs
|
|
633
|
+
try {
|
|
634
|
+
const result = await this.page.evaluate(async ({ sel, count }) => {
|
|
635
|
+
const findInShadow = (root) => {
|
|
636
|
+
const direct = root.querySelector(sel);
|
|
637
|
+
if (direct)
|
|
638
|
+
return direct;
|
|
639
|
+
const elements = root.querySelectorAll('*');
|
|
640
|
+
for (const el of elements) {
|
|
641
|
+
if ('shadowRoot' in el) {
|
|
642
|
+
const shadow = el.shadowRoot;
|
|
643
|
+
if (shadow) {
|
|
644
|
+
const found = findInShadow(shadow);
|
|
645
|
+
if (found)
|
|
646
|
+
return found;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return null;
|
|
651
|
+
};
|
|
652
|
+
const input = findInShadow(document);
|
|
653
|
+
if (input && input.type === 'file') {
|
|
654
|
+
// Create a DataTransfer to simulate file selection
|
|
655
|
+
const dt = new DataTransfer();
|
|
656
|
+
// Note: We can't actually create files from paths in the browser
|
|
657
|
+
// This would need to be done differently - via the Playwright API
|
|
658
|
+
return { found: true, needsApi: true };
|
|
659
|
+
}
|
|
660
|
+
return { found: false };
|
|
661
|
+
}, { sel: selector, count: filePaths.length });
|
|
662
|
+
if (result.found && result.needsApi) {
|
|
663
|
+
// Use Playwright's API by getting the actual element handle
|
|
664
|
+
const element = await this.page.waitForSelector(selector, { timeout: 1000 });
|
|
665
|
+
if (element) {
|
|
666
|
+
await element.setInputFiles(filePaths);
|
|
667
|
+
return {
|
|
668
|
+
success: true,
|
|
669
|
+
count: filePaths.length,
|
|
670
|
+
fileNames: filePaths.map(p => p.split('/').pop() || p),
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return {
|
|
675
|
+
success: false,
|
|
676
|
+
count: 0,
|
|
677
|
+
fileNames: [],
|
|
678
|
+
error: 'Could not find file input in shadow DOM',
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
return {
|
|
683
|
+
success: false,
|
|
684
|
+
count: 0,
|
|
685
|
+
fileNames: [],
|
|
686
|
+
error: error instanceof Error ? error.message : String(error),
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Handle file download with interception
|
|
692
|
+
*
|
|
693
|
+
* @param triggerSelector - Selector for element that triggers download
|
|
694
|
+
* @param options - Download options
|
|
695
|
+
* @returns Download result
|
|
696
|
+
*/
|
|
697
|
+
async handleDownload(triggerSelector, options = {}) {
|
|
698
|
+
const { action = 'click', timeout = 30000, savePath } = options;
|
|
699
|
+
try {
|
|
700
|
+
// Check if trigger element exists first to avoid download timeout
|
|
701
|
+
const triggerElement = await this.page.$(triggerSelector);
|
|
702
|
+
if (!triggerElement) {
|
|
703
|
+
return {
|
|
704
|
+
success: false,
|
|
705
|
+
error: `Trigger element not found: ${triggerSelector}`,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
// Set up download handler
|
|
709
|
+
const downloadPromise = this.page.waitForEvent('download', { timeout });
|
|
710
|
+
// Trigger download
|
|
711
|
+
if (action === 'click') {
|
|
712
|
+
await this.page.click(triggerSelector);
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
// Find the form and submit it
|
|
716
|
+
const form = await this.page.$(`${triggerSelector} form`);
|
|
717
|
+
if (form) {
|
|
718
|
+
await form.evaluate((f) => f.submit());
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
await this.page.click(triggerSelector);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
// Wait for download to start
|
|
725
|
+
const download = await downloadPromise;
|
|
726
|
+
// Get file info
|
|
727
|
+
const fileName = download.suggestedFilename();
|
|
728
|
+
const failure = download.failure();
|
|
729
|
+
if (failure) {
|
|
730
|
+
return {
|
|
731
|
+
success: false,
|
|
732
|
+
error: `Download failed: ${failure}`,
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
// Save to path if specified
|
|
736
|
+
let finalPath;
|
|
737
|
+
let size;
|
|
738
|
+
if (savePath) {
|
|
739
|
+
// saveAs returns void, so we use savePath as the final path
|
|
740
|
+
await download.saveAs(savePath);
|
|
741
|
+
finalPath = savePath;
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
// Save to temp dir and get the path
|
|
745
|
+
finalPath = await download.path();
|
|
746
|
+
}
|
|
747
|
+
// Get file size if we have a path
|
|
748
|
+
if (finalPath) {
|
|
749
|
+
try {
|
|
750
|
+
const fs = await import('fs/promises');
|
|
751
|
+
const stats = await fs.stat(finalPath);
|
|
752
|
+
size = stats.size;
|
|
753
|
+
}
|
|
754
|
+
catch {
|
|
755
|
+
// Size not available
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return {
|
|
759
|
+
success: true,
|
|
760
|
+
path: finalPath,
|
|
761
|
+
fileName,
|
|
762
|
+
size,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
catch (error) {
|
|
766
|
+
return {
|
|
767
|
+
success: false,
|
|
768
|
+
error: error instanceof Error ? error.message : String(error),
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
774
|
+
* COMBINED SEARCH (Shadow DOM + Iframes)
|
|
775
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
776
|
+
*/
|
|
777
|
+
/**
|
|
778
|
+
* Find element anywhere (normal DOM, shadow DOM, or iframes)
|
|
779
|
+
*
|
|
780
|
+
* This is the main entry point for finding elements that may be
|
|
781
|
+
* hidden in shadow DOM or inside iframes.
|
|
782
|
+
*
|
|
783
|
+
* @param selector - CSS selector
|
|
784
|
+
* @param options - Search options
|
|
785
|
+
* @returns Element location or null
|
|
786
|
+
*/
|
|
787
|
+
async findElementAnywhere(selector, options = {}) {
|
|
788
|
+
const { timeout = 5000 } = options;
|
|
789
|
+
// 1. First, check if element exists in regular DOM ONLY (without shadow piercing)
|
|
790
|
+
const inRegularDOM = await this.page.evaluate((sel) => {
|
|
791
|
+
const element = document.querySelector(sel);
|
|
792
|
+
return element !== null;
|
|
793
|
+
}, selector).catch(() => false);
|
|
794
|
+
if (inRegularDOM) {
|
|
795
|
+
// Element exists in regular DOM, not shadow DOM
|
|
796
|
+
const normalLocator = this.page.locator(selector).first();
|
|
797
|
+
return {
|
|
798
|
+
locator: normalLocator,
|
|
799
|
+
isInShadowDom: false,
|
|
800
|
+
shadowDepth: 0,
|
|
801
|
+
nestedShadowDepth: 0,
|
|
802
|
+
isInIframe: false,
|
|
803
|
+
path: [selector],
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
// 2. Try shadow DOM (with piercing)
|
|
807
|
+
const shadowResult = await this.findWithShadowPiercing(selector, { timeout });
|
|
808
|
+
if (shadowResult) {
|
|
809
|
+
return shadowResult;
|
|
810
|
+
}
|
|
811
|
+
// 3. Try iframes
|
|
812
|
+
const iframeResult = await this.findInIframes(selector, { timeout });
|
|
813
|
+
if (iframeResult) {
|
|
814
|
+
return iframeResult;
|
|
815
|
+
}
|
|
816
|
+
// Not found
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* P0: Handle print dialog (window.print())
|
|
821
|
+
*
|
|
822
|
+
* Uses CDP to automatically dismiss or capture print dialogs.
|
|
823
|
+
*
|
|
824
|
+
* @param options - Print handling options
|
|
825
|
+
* @returns Promise that resolves when print dialog is handled
|
|
826
|
+
*/
|
|
827
|
+
async handlePrintDialog(options = {}) {
|
|
828
|
+
const { action = 'dismiss', path } = options;
|
|
829
|
+
try {
|
|
830
|
+
// For Chromium, use CDP to handle print
|
|
831
|
+
const client = await this.page.context().newCDPSession(this.page);
|
|
832
|
+
if (action === 'capture' && path) {
|
|
833
|
+
// Enable page printing
|
|
834
|
+
await client.send('Page.enable');
|
|
835
|
+
// Generate PDF on print
|
|
836
|
+
// Note: This is a simplified approach - actual print-to-PDF would need more setup
|
|
837
|
+
const pdf = await this.page.pdf({
|
|
838
|
+
path,
|
|
839
|
+
printBackground: true,
|
|
840
|
+
});
|
|
841
|
+
return { handled: true, pdfPath: path };
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
// Just dismiss the print dialog
|
|
845
|
+
// In headless mode, print dialogs don't show, but we track that print was called
|
|
846
|
+
return { handled: true };
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
catch (error) {
|
|
850
|
+
// Fallback: print dialog might not have shown (headless mode)
|
|
851
|
+
return { handled: false };
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* P0: Hover over an element
|
|
856
|
+
*
|
|
857
|
+
* Triggers hover state and waits for any visual changes.
|
|
858
|
+
* Supports elements in shadow DOM and iframes.
|
|
859
|
+
*
|
|
860
|
+
* @param selector - CSS selector for the element
|
|
861
|
+
* @param options - Hover options
|
|
862
|
+
* @returns Hover result
|
|
863
|
+
*/
|
|
864
|
+
async hover(options) {
|
|
865
|
+
const { selector, waitForAnimation = true, animationWait = 100, force = false, } = options;
|
|
866
|
+
try {
|
|
867
|
+
// First do a quick check if element exists to avoid long timeout when not found
|
|
868
|
+
const quickCheck = await this.page.$(selector);
|
|
869
|
+
if (!quickCheck) {
|
|
870
|
+
// Try shadow DOM quick check
|
|
871
|
+
const shadowCheck = await this.findWithShadowPiercing(selector, { timeout: 1000 });
|
|
872
|
+
if (!shadowCheck) {
|
|
873
|
+
return {
|
|
874
|
+
success: false,
|
|
875
|
+
error: `Element not found: ${selector}`,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
// Try to find element anywhere (including shadow DOM)
|
|
880
|
+
const location = await this.findElementAnywhere(selector, { timeout: 5000 });
|
|
881
|
+
if (!location) {
|
|
882
|
+
// Try standard locator
|
|
883
|
+
const locator = this.page.locator(selector);
|
|
884
|
+
await locator.hover({ force });
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
await location.locator.hover({ force });
|
|
888
|
+
}
|
|
889
|
+
// Wait for animations/tooltip/dropdown to appear
|
|
890
|
+
if (waitForAnimation) {
|
|
891
|
+
await this.sleep(animationWait);
|
|
892
|
+
}
|
|
893
|
+
return { success: true };
|
|
894
|
+
}
|
|
895
|
+
catch (error) {
|
|
896
|
+
return {
|
|
897
|
+
success: false,
|
|
898
|
+
error: error instanceof Error ? error.message : String(error),
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* P0: Drag and drop operation
|
|
904
|
+
*
|
|
905
|
+
* Drags an element and drops it onto a target element.
|
|
906
|
+
* Supports elements in shadow DOM and iframes.
|
|
907
|
+
*
|
|
908
|
+
* @param options - Drag and drop options
|
|
909
|
+
* @returns Drag result
|
|
910
|
+
*/
|
|
911
|
+
async dragAndDrop(options) {
|
|
912
|
+
const { from, to, position = 'center', verifyDrop = true } = options;
|
|
913
|
+
try {
|
|
914
|
+
// Find source element
|
|
915
|
+
const fromLocator = this.page.locator(from).first();
|
|
916
|
+
const fromCount = await fromLocator.count();
|
|
917
|
+
if (fromCount === 0) {
|
|
918
|
+
return {
|
|
919
|
+
success: false,
|
|
920
|
+
error: `Source element not found: ${from}`,
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
// Find target element
|
|
924
|
+
const toLocator = this.page.locator(to).first();
|
|
925
|
+
const toCount = await toLocator.count();
|
|
926
|
+
if (toCount === 0) {
|
|
927
|
+
return {
|
|
928
|
+
success: false,
|
|
929
|
+
error: `Target element not found: ${to}`,
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
// Perform drag and drop
|
|
933
|
+
await fromLocator.dragTo(toLocator, {
|
|
934
|
+
targetPosition: position === 'center' ? undefined : this.getPositionForDrop(position),
|
|
935
|
+
});
|
|
936
|
+
// Wait for animations
|
|
937
|
+
await this.sleep(100);
|
|
938
|
+
// Verify drop if requested
|
|
939
|
+
if (verifyDrop) {
|
|
940
|
+
// Check if source is no longer at original position (moved)
|
|
941
|
+
// or if target now contains source
|
|
942
|
+
await this.sleep(100);
|
|
943
|
+
}
|
|
944
|
+
return { success: true };
|
|
945
|
+
}
|
|
946
|
+
catch (error) {
|
|
947
|
+
return {
|
|
948
|
+
success: false,
|
|
949
|
+
error: error instanceof Error ? error.message : String(error),
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* P0: Drag element to specific coordinates
|
|
955
|
+
*
|
|
956
|
+
* Drags an element from its current position to specific x, y coordinates.
|
|
957
|
+
*
|
|
958
|
+
* @param options - Drag to coordinates options
|
|
959
|
+
* @returns Drag result
|
|
960
|
+
*/
|
|
961
|
+
async dragToCoordinates(options) {
|
|
962
|
+
const { selector, x, y, fromX, fromY, steps = 10 } = options;
|
|
963
|
+
try {
|
|
964
|
+
const locator = this.page.locator(selector).first();
|
|
965
|
+
const count = await locator.count();
|
|
966
|
+
if (count === 0) {
|
|
967
|
+
return {
|
|
968
|
+
success: false,
|
|
969
|
+
error: `Element not found: ${selector}`,
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
const box = await locator.boundingBox();
|
|
973
|
+
if (!box) {
|
|
974
|
+
return {
|
|
975
|
+
success: false,
|
|
976
|
+
error: `Could not get bounding box for: ${selector}`,
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
const startX = fromX ?? box.x + box.width / 2;
|
|
980
|
+
const startY = fromY ?? box.y + box.height / 2;
|
|
981
|
+
// Perform the drag using mouse API
|
|
982
|
+
await this.page.mouse.move(startX, startY);
|
|
983
|
+
await this.page.mouse.down();
|
|
984
|
+
await this.page.mouse.move(x, y, { steps });
|
|
985
|
+
await this.page.mouse.up();
|
|
986
|
+
// Wait for animations
|
|
987
|
+
await this.sleep(100);
|
|
988
|
+
return { success: true };
|
|
989
|
+
}
|
|
990
|
+
catch (error) {
|
|
991
|
+
return {
|
|
992
|
+
success: false,
|
|
993
|
+
error: error instanceof Error ? error.message : String(error),
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* P0: Multi-select drag (select multiple elements)
|
|
999
|
+
*
|
|
1000
|
+
* Performs Ctrl+Click on multiple elements to select them.
|
|
1001
|
+
*
|
|
1002
|
+
* @param selectors - Array of CSS selectors to select
|
|
1003
|
+
* @returns Selection result
|
|
1004
|
+
*/
|
|
1005
|
+
async multiSelect(selectors) {
|
|
1006
|
+
const errors = [];
|
|
1007
|
+
let selected = 0;
|
|
1008
|
+
for (const selector of selectors) {
|
|
1009
|
+
try {
|
|
1010
|
+
const locator = this.page.locator(selector).first();
|
|
1011
|
+
const count = await locator.count();
|
|
1012
|
+
if (count > 0) {
|
|
1013
|
+
// Ctrl+Click to add to selection
|
|
1014
|
+
await locator.click({
|
|
1015
|
+
modifiers: ['Control'],
|
|
1016
|
+
});
|
|
1017
|
+
selected++;
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
errors.push(`Element not found: ${selector}`);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
catch (error) {
|
|
1024
|
+
errors.push(`Error selecting ${selector}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return {
|
|
1028
|
+
success: errors.length === 0,
|
|
1029
|
+
selected,
|
|
1030
|
+
errors,
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Get position offset for drop position
|
|
1035
|
+
*/
|
|
1036
|
+
getPositionForDrop(position) {
|
|
1037
|
+
switch (position) {
|
|
1038
|
+
case 'top':
|
|
1039
|
+
return { x: 0.5, y: 0.25 }; // Top center
|
|
1040
|
+
case 'right':
|
|
1041
|
+
return { x: 0.75, y: 0.5 }; // Right center
|
|
1042
|
+
case 'bottom':
|
|
1043
|
+
return { x: 0.5, y: 0.75 }; // Bottom center
|
|
1044
|
+
case 'left':
|
|
1045
|
+
return { x: 0.25, y: 0.5 }; // Left center
|
|
1046
|
+
default:
|
|
1047
|
+
return { x: 0.5, y: 0.5 }; // Center
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Sleep utility for waiting
|
|
1052
|
+
*/
|
|
1053
|
+
sleep(ms) {
|
|
1054
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Clear caches
|
|
1058
|
+
*/
|
|
1059
|
+
clearCache() {
|
|
1060
|
+
this.shadowRootCache.clear();
|
|
1061
|
+
this.frameCache.clear();
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Factory function to create handler
|
|
1066
|
+
*/
|
|
1067
|
+
export function createAdvancedInteractionsHandler(page, options) {
|
|
1068
|
+
return new AdvancedInteractionsHandler(page, options);
|
|
1069
|
+
}
|