diff --git a/.azure-pipelines/publish.yml b/.azure-pipelines/publish.yml new file mode 100644 index 0000000000000..ccbd5692a7d5b --- /dev/null +++ b/.azure-pipelines/publish.yml @@ -0,0 +1,169 @@ +# Publishes npm packages via ESRP. Manual trigger only, regular publishing +# is done from GitHub Actions, see .github/workflows/publish_release.yml. +# Depending on the selected ref, a manual run publishes: +# - @next (alpha with commit timestamp) from main +# - @beta (beta with commit timestamp) from release-* branches +# - @latest from v* release tags +trigger: none + +pr: none + +resources: + repositories: + - repository: 1esPipelines + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release + +extends: + template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines + parameters: + pool: + name: DevDivPlaywrightAzurePipelinesUbuntu2204 + os: linux + sdl: + sourceAnalysisPool: + # SDL tools require windows, see https://aka.ms/AAo6v8e + name: DevDivPlaywrightAzurePipelinesWindows2022 + os: windows + stages: + - stage: Stage + jobs: + - job: Build + displayName: "Build npm packages" + templateContext: + outputs: + - output: pipelineArtifact + path: $(Build.ArtifactStagingDirectory)/esrp-build + artifact: esrp-build + steps: + - checkout: self + displayName: "Checkout code" + + - task: Bash@3 + displayName: "Check the branch is main, release-* or a v* tag" + inputs: + targetType: "inline" + script: | + if [[ "$BUILD_SOURCE_BRANCH" != "refs/heads/main" && "$BUILD_SOURCE_BRANCH" != refs/heads/release-* && "$BUILD_SOURCE_BRANCH" != refs/tags/v* ]]; then + echo "Can only publish from main, release-* branches or v* tags." + echo "Unexpected branch: $BUILD_SOURCE_BRANCH" + exit 1 + fi + env: + BUILD_SOURCE_BRANCH: $(Build.SourceBranch) + + - task: UseNode@1 + inputs: + version: '26.x' + displayName: "Install Node.js" + + - task: Bash@3 + displayName: "setup .npmrc" + inputs: + targetType: "inline" + script: | + echo "registry=https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/DevDiv_PublicPackages/npm/registry/" >> .npmrc + echo "registry=https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/DevDiv_PublicPackages/npm/registry/" >> tests/playwright-test/stable-test-runner/.npmrc + + - task: npmAuthenticate@0 + displayName: "authenticate the private npm registry" + inputs: + workingFile: .npmrc + + - task: npmAuthenticate@0 + displayName: "authenticate the private npm registry for stable-test-runner" + inputs: + workingFile: tests/playwright-test/stable-test-runner/.npmrc + + - script: npm ci + displayName: "npm ci" + + - script: npm run build + displayName: "npm run build" + + - task: Bash@3 + name: setVersion + displayName: "Set version and dist-tag" + inputs: + targetType: "inline" + script: | + set -e + if [[ "$BUILD_SOURCE_BRANCH" == refs/tags/v* ]]; then + # Release version is already checked in, only publish what the tag points at. + NPM_DIST_TAG="latest" + elif [[ "$BUILD_SOURCE_BRANCH" == refs/heads/release-* ]]; then + NPM_DIST_TAG="beta" + node utils/build/update_canary_version.js --beta --commit-timestamp + elif [[ "$BUILD_REASON" == "Schedule" ]]; then + NPM_DIST_TAG="next" + node utils/build/update_canary_version.js --alpha --today-date + else + NPM_DIST_TAG="next" + node utils/build/update_canary_version.js --alpha --commit-timestamp + fi + node utils/workspace.js --ensure-consistent + VERSION=$(node utils/workspace.js --get-version) + if [[ "$NPM_DIST_TAG" == "latest" && "$BUILD_SOURCE_BRANCH" != "refs/tags/v$VERSION" ]]; then + echo "ERROR: version '$VERSION' does not match tag '$BUILD_SOURCE_BRANCH'" + exit 1 + fi + if [[ "$NPM_DIST_TAG" == "beta" && "$VERSION" != *-beta-* ]]; then + echo "ERROR: unexpected version '$VERSION', must be a beta version" + exit 1 + fi + if [[ "$NPM_DIST_TAG" == "next" && "$VERSION" != *-alpha-* ]]; then + echo "ERROR: unexpected version '$VERSION', must be an alpha version" + exit 1 + fi + echo "Publishing version $VERSION with dist-tag $NPM_DIST_TAG" + echo "##vso[task.setvariable variable=npmDistTag;isOutput=true]$NPM_DIST_TAG" + env: + BUILD_SOURCE_BRANCH: $(Build.SourceBranch) + BUILD_REASON: $(Build.Reason) + + - task: Bash@3 + displayName: "Pack all packages" + inputs: + targetType: "inline" + script: | + set -e + mkdir -p "$(Build.ArtifactStagingDirectory)/esrp-build" + node utils/workspace.js --list-public-package-paths | while read package; do + npm pack --pack-destination="$(Build.ArtifactStagingDirectory)/esrp-build" "$package" + done + ls -la "$(Build.ArtifactStagingDirectory)/esrp-build" + + - job: Publish + displayName: "ESRP Release to npm" + dependsOn: Build + variables: + npmDistTag: $[ dependencies.Build.outputs['setVersion.npmDistTag'] ] + templateContext: + type: releaseJob + isProduction: true + inputs: + - input: pipelineArtifact + artifactName: esrp-build + targetPath: $(Build.ArtifactStagingDirectory)/esrp-build + steps: + - checkout: none + - task: EsrpRelease@11 + inputs: + connectedservicename: 'Playwright-ESRP-PME' + usemanagedidentity: true + keyvaultname: 'playwright-esrp-pme' + signcertname: 'ESRP-Release-Sign' + clientid: '13434a40-7de4-4c23-81a3-d843dc81c2c5' + intent: 'PackageDistribution' + contenttype: 'npm' + # npm dist-tag to publish with. + productstate: '$(npmDistTag)' + folderlocation: '$(Build.ArtifactStagingDirectory)/esrp-build' + waitforreleasecompletion: true + owners: 'yurys@microsoft.com' + approvers: 'yurys@microsoft.com' + serviceendpointurl: 'https://api.esrp.microsoft.com' + mainpublisher: 'Playwright' + domaintenantid: '975f013f-7f24-47e8-a7d3-abc4752bf346' + displayName: 'ESRP Release to npm' diff --git a/.claude/skills/playwright-dev/trace_system_guide.md b/.claude/skills/playwright-dev/trace_system_guide.md index ef6345e0daac3..c8312b7523924 100644 --- a/.claude/skills/playwright-dev/trace_system_guide.md +++ b/.claude/skills/playwright-dev/trace_system_guide.md @@ -15,22 +15,20 @@ The Playwright trace system is a comprehensive recording and visualization frame ## 2. File Structure -### packages/trace/src/ - Trace Type Definitions -Located in `/home/pfeldman/code/playwright/packages/trace/src/` +### packages/isomorphic/trace/ - Trace Type Definitions **Key Files:** -- **trace.ts** - Core trace event type definitions -- **har.ts** - HTTP Archive format (network traffic) -- **snapshot.ts** - DOM snapshot data structures -- **DEPS.list** - Dependencies marker +- **trace.ts** - Current trace event and snapshot types; re-exports the latest version +- **versions/** - One file per trace format version, plus legacy formats kept for modernization +- **versions/har.ts** - HTTP Archive format (network traffic) **File List:** ``` -trace/src/ -├── trace.ts (183 lines) - Main trace event types -├── har.ts (189 lines) - HAR format types -├── snapshot.ts (62 lines) - Snapshot data structures -└── DEPS.list - Dependencies file +isomorphic/trace/ +├── trace.ts - Current trace event + snapshot types (re-export) +└── versions/ + ├── traceV*.ts - Per-version trace event types + └── har.ts - HAR format types ``` --- @@ -206,7 +204,7 @@ type ErrorTraceEvent = { --- -## 4. HAR Format (har.ts) +## 4. HAR Format (versions/har.ts) Follows HTTP Archive 1.2 specification. Key structure for network traffic: @@ -247,7 +245,7 @@ type Entry = { --- -## 5. Snapshot Format (snapshot.ts) +## 5. Snapshot Format (trace.ts) ### FrameSnapshot ```typescript @@ -915,9 +913,8 @@ Every action uses a unique `callId` to correlate: | File | Size | Purpose | |------|------|---------| -| `trace/src/trace.ts` | 183 lines | Trace event types | -| `trace/src/har.ts` | 189 lines | Network HAR types | -| `trace/src/snapshot.ts` | 62 lines | Snapshot types | +| `isomorphic/trace/trace.ts` | Trace event and snapshot types | +| `isomorphic/trace/versions/har.ts` | Network HAR types | | `playwright-core/.../tracing.ts` | 700+ lines | Recording engine | | `playwright-core/.../traceParser.ts` | 62 lines | ZIP backend | | `playwright-core/.../traceViewer.ts` | 288 lines | Viewer server | diff --git a/.claude/skills/playwright-triage/SKILL.md b/.claude/skills/playwright-triage/SKILL.md index 5b1ef32d36830..bed461bf3ccf8 100644 --- a/.claude/skills/playwright-triage/SKILL.md +++ b/.claude/skills/playwright-triage/SKILL.md @@ -58,7 +58,7 @@ result to report, not a non-finding. match. (A version ending in `-next`, e.g. `1.62.0-next`, is **not** an npm version — it means tip-of-tree, which is the `@next` build you already tried.) -To step through a test interactively, use the [playwright-cli](../playwright-cli/SKILL.md) skill. +To step through a test interactively, use the [playwright-cli](../../../packages/playwright-core/src/tools/skills/playwright-cli/SKILL.md) skill. Reports sometimes target another part of the Playwright project — `@playwright/mcp` (its source is in this repo), `playwright-vscode`, `playwright-python`, `playwright-java`, `playwright-dotnet`. diff --git a/.github/actions/download-artifact/action.yml b/.github/actions/download-artifact/action.yml index 641f72083d4bb..d8a5b92b6c5e2 100644 --- a/.github/actions/download-artifact/action.yml +++ b/.github/actions/download-artifact/action.yml @@ -16,7 +16,7 @@ runs: shell: bash run: mkdir -p '${{ inputs.path }}/artifacts' - name: Download artifacts - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 with: script: | console.log(`downloading artifacts for workflow_run: ${context.payload.workflow_run.id}`); diff --git a/.github/actions/run-test/action.yml b/.github/actions/run-test/action.yml index bc985f15efff0..8f17503adeb16 100644 --- a/.github/actions/run-test/action.yml +++ b/.github/actions/run-test/action.yml @@ -39,7 +39,7 @@ inputs: runs: using: composite steps: - - uses: actions/setup-node@v6 + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 with: node-version: ${{ inputs.node-version }} - run: | @@ -81,7 +81,7 @@ runs: env: PW_TAG: "@${{ inputs.bot-name }}" - name: Azure Login - uses: azure/login@v2 + uses: azure/login@7ddb5af1ef8758cf1353cf3b42f940aee27ba21c # v3.0.2 if: ${{ !cancelled() && env.PLAYWRIGHT_SETUP_COMPLETE == 'true' && github.event_name == 'push' && github.repository == 'microsoft/playwright' }} with: client-id: ${{ inputs.flakiness-client-id }} diff --git a/.github/actions/upload-blob-report/action.yml b/.github/actions/upload-blob-report/action.yml index 658145fcf0e9d..e623366d2e136 100644 --- a/.github/actions/upload-blob-report/action.yml +++ b/.github/actions/upload-blob-report/action.yml @@ -20,7 +20,7 @@ runs: echo "::endgroup::" - name: Upload blob report to GitHub if: ${{ !cancelled() }} - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: blob-report-${{ inputs.job_name }} path: ${{ inputs.report_dir }}/** diff --git a/.github/actions/upload-parquet-report/action.yml b/.github/actions/upload-parquet-report/action.yml index af8ae5567a995..2c7911a5b719b 100644 --- a/.github/actions/upload-parquet-report/action.yml +++ b/.github/actions/upload-parquet-report/action.yml @@ -13,7 +13,7 @@ runs: steps: - name: Upload parquet report to GitHub if: ${{ hashFiles(inputs.report_file) != '' }} - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: parquet-report-${{ inputs.job_name }} path: ${{ inputs.report_file }} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000000..74efe116f108d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,21 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + groups: + github-actions: + patterns: ["*"] + schedule: + interval: "weekly" + cooldown: + default-days: 7 + + - package-ecosystem: "npm" + directory: "/" + groups: + npm: + patterns: ["*"] + schedule: + interval: "weekly" + cooldown: + default-days: 7 diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index e0bc98790d21e..56781490852bd 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -8,8 +8,8 @@ jobs: contents: read steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: lts/* - run: npm ci diff --git a/.github/workflows/create_test_report.yml b/.github/workflows/create_test_report.yml index 0bc76438dac5d..2716035797788 100644 --- a/.github/workflows/create_test_report.yml +++ b/.github/workflows/create_test_report.yml @@ -23,8 +23,8 @@ jobs: env: MARKDOWN_OUTPUT_FILE: ${{ github.workspace }}/report.md steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: lts/* - run: npm ci @@ -54,7 +54,7 @@ jobs: - name: Upload HTML report id: upload-report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: path: playwright-report/index.html archive: false # Upload as a single, browser-openable file (no zip) @@ -74,16 +74,23 @@ jobs: echo "number=$NUMBER" >> "$GITHUB_OUTPUT" AUTHOR=$(gh pr view --repo "${{ github.repository }}" "$HEAD_REF" --json author --jq '.author.login' 2>/dev/null || true) - ALLOWED="github-actions[bot] pavelfeldman yury-s dgozman Skn0tt dcrousso" TRIAGE_ALLOWED=false - for a in $ALLOWED; do - if [ "$a" = "$AUTHOR" ]; then TRIAGE_ALLOWED=true; break; fi - done + case "$AUTHOR" in + app/github-actions|app/microsoft-playwright-automation) + TRIAGE_ALLOWED=true + ;; + *) + if [ -n "$AUTHOR" ]; then + PERM=$(gh api "repos/${{ github.repository }}/collaborators/$AUTHOR/permission" --jq .permission 2>/dev/null || echo none) + case "$PERM" in write|admin) TRIAGE_ALLOWED=true ;; esac + fi + ;; + esac echo "triage_allowed=$TRIAGE_ALLOWED" >> "$GITHUB_OUTPUT" - name: Post report comment to PR if: ${{ steps.pr.outputs.number }} - uses: actions/github-script@v8 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: HTML_REPORT_URL: ${{ steps.upload-report.outputs.artifact-url }} PR_NUMBER: ${{ steps.pr.outputs.number }} @@ -101,7 +108,7 @@ jobs: }); - name: Publish Report URL as Commit Status - uses: actions/github-script@v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | await github.rest.repos.createCommitStatus({ @@ -116,109 +123,11 @@ jobs: triage: needs: merge-reports if: ${{ needs.merge-reports.outputs.has_failures == 'true' && needs.merge-reports.outputs.pr_number && needs.merge-reports.outputs.triage_allowed == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 20 permissions: + contents: read + actions: read + pull-requests: write copilot-requests: write - outputs: - has_draft: ${{ steps.triage.outputs.has_draft }} + uses: ./.github/workflows/pr-ci-triage.yml + with: pr_number: ${{ needs.merge-reports.outputs.pr_number }} - env: - PR_NUMBER: ${{ needs.merge-reports.outputs.pr_number }} - GH_TOKEN: ${{ github.token }} - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Set up Node.js - uses: actions/setup-node@v6 - with: - node-version: "24" - - - name: Install Copilot CLI - run: npm install -g @github/copilot - - - name: Triage failures with Copilot CLI - id: triage - env: - COPILOT_GITHUB_TOKEN: ${{ github.token }} - run: | - mkdir -p output - PROMPT=$(cat <> "$GITHUB_OUTPUT" - else - echo "has_draft=false" >> "$GITHUB_OUTPUT" - fi - - - name: Add session transcript to job summary - if: ${{ always() }} - run: | - { - echo "## CI triage session transcript (PR #$PR_NUMBER)" - echo '' - cat "output/copilot-session.md" 2>/dev/null || echo "(no transcript)" - } >> "$GITHUB_STEP_SUMMARY" - - - name: Upload output - if: ${{ always() }} - uses: actions/upload-artifact@v4 - with: - name: ci-triage-${{ needs.merge-reports.outputs.pr_number }} - path: output/triage.md - if-no-files-found: warn - - post: - needs: triage - if: needs.triage.outputs.has_draft == 'true' - runs-on: ubuntu-latest - permissions: - pull-requests: write - env: - PR_NUMBER: ${{ needs.triage.outputs.pr_number }} - GH_TOKEN: ${{ github.token }} - WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Download triage output - uses: actions/download-artifact@v4 - with: - name: ci-triage-${{ needs.triage.outputs.pr_number }} - path: output - - - name: Post triage comment - uses: actions/github-script@v9 - with: - script: | - const fs = require('fs'); - const { collapsePreviousComments } = require('./tests/config/postReportComment'); - const sentinel = ``; - const prNumber = +process.env.PR_NUMBER; - await collapsePreviousComments(github, context, prNumber, sentinel); - - const triage = fs.readFileSync('output/triage.md', 'utf8'); - const body = `${triage}\n\nTriaged by the Playwright bot - [agent run](${process.env.WORKFLOW_URL})\n${sentinel}\n`; - await github.rest.issues.createComment({ - ...context.repo, - issue_number: prNumber, - body, - }); diff --git a/.github/workflows/fix-flakes-prompt.md b/.github/workflows/fix-flakes-prompt.md deleted file mode 100644 index 03eb4b328cd72..0000000000000 --- a/.github/workflows/fix-flakes-prompt.md +++ /dev/null @@ -1,143 +0,0 @@ -# Playwright: Fix a Flaky or Red Test - -Turn CI test-results data into **one** concrete fix: pick a high-impact flaky-or-red test that is reproducible on your OS, -confirm nobody's on it, fix the root cause *or* scope a skip, pick a reviewer, and hand off a -single commit that becomes the PR. Fully autonomous — no approval stops. - -The GitHub CLI (`gh`) is not authenticated in this job. Do not use it for GitHub API operations; -use GitHub MCP tools instead. - -## 1. Pick one target - -Query the DB following the patterns in `.claude/skills/playwright-test-results/SKILL.md`. -Two families: - -- **Cross-run flake** — the *final* verdict (after retries) flips between runs. -- **Consistent red** — `expected_status = 'passed'` yet failing in ~every run. - -Rank by **impact**: fail %, run count (floor `runs >= 10` so it isn't a one-off), and how many -bots/PRs it disrupts — **not** by "has a tidy error message". -Pick a candidate whose failing `bot_name` OS matches yours so you can reproduce it. -Keep the ranked list — you fall back down it in step 2. Say why you picked the top one. -Once you have the target, use the "Generate a linked emoji run history" recipe in -`.claude/skills/playwright-test-results/SKILL.md`. Include the complete output in the PR body: -never trim it or remove green runs, and check the square count matches the per-run row count. - -## 2. Check nobody's on it — and that it isn't already fixed - -Two dead ends to rule out before touching a candidate. If either fires, drop it and go -down your step-1 ranking, re-checking each; only stop once the whole shortlist is covered. - -- **Already being worked on** — search PRs/issues (open *and* recently merged/closed) for - the test title words and file path, plus any issue linked in the test's `annotation`. -- **Already fixed** — the DB is a rolling window that still holds the failing runs from - before a fix landed, so a test fixed mid-window looks maximally bimodal (old fails + new - passes), which is exactly what floats it up step 1. Check whether a fix has landed since it - last flaked: find the commit its most recent failing run ran on, and look at what's changed - in that test's file since. Only move on if enough subsequent runs show the failures stopped; - a browser roll alone is not evidence of a fix. - -Compare the test across every bot that runs it, not only the failing bot. A clean boundary -between stable, beta, and dev browser channels often points to a browser-version regression. -Read the exact versions from the job logs and record the failing and passing versions; channel -names alone do not identify the affected behaviour. - -## 3. Reproduce on this OS - -Read the test and its `error_message`. - -Build first (`npm run build`; watch is **not** running — if you touch generated-code files see -`CLAUDE.md`). Then reproduce, scoped to the failing target. - -The DB `project_name` **is** the Playwright `--project`; `bot_name` encodes the OS. **Always -pass a `:` filter** — a bare run launches the whole suite. The browser test scripts -are project-locked, so use the one matching the failing bot: - -| Failing target | Command | -|---|---| -| chromium | `npm run ctest -- :` | -| firefox | `npm run ftest -- :` | -| webkit | `npm run wtest -- :` | -| `tests/playwright-test/**` | `npm run ttest -- :` | -| `tests/mcp/**` | `npm run test-mcp -- --project= :` | - -Other suites (electron `etest`, etc.): see `package.json` scripts. -Add `--repeat-each=N` to force a flake's flip. - -**You can only reproduce what this OS and architecture reaches.** Match both from `bot_name`; -non-reproduction under lighter local load or on another architecture is not itself a skip candidate. -Keep any OS/architecture handling keyed on the *current* environment, never hardcoded. -Broader OS coverage comes from different agent runs on other OSes, not from you. - -## 4. Fix — root cause or scoped skip - -- **Tractable → fix the source.** Usually a test-side race: a missing `await`, waiting on the - wrong signal, an under-specified locator, leaked state. Fix the test (or the product bug). -- **Timeout → check the budget before skipping.** Compare passing durations and neighboring tests; - if working tests share an expensive operation near the timeout, prefer a scoped `it.slow()`. -- **Engine/OS-specific, unreachable here, or genuinely hard → scope a skip**, narrowed to the - exact failing condition — never a whole file or browser: - - ```ts - it.fixme(browserName === 'webkit' && isLinux, 'https://github.com/microsoft/playwright/issues/NNNNN'); - ``` - - For browser-specific failures, prefer the narrowest observed version predicate over a channel - predicate. Channels roll forward, so `channel === 'chrome'` can keep skipping the test after - the browser fixes the bug. If stable fails while beta/dev passes, use `browserVersion` or - `browserMajorVersion` and gate only the affected version: - - ```ts - it.fixme(browserName === 'chromium' && browserMajorVersion === 150, 'https://github.com/microsoft/playwright/issues/NNNNN'); - ``` - - Do not invent a wider threshold for untested versions. Keep a channel predicate only when the - same browser version behaves differently between channels. - - Link an issue if one exists or explain why the test is skipped. **Default to `fixme`** — it parks - the debt and stays greppable. Use `skip` only if reproduction shows the test is genuinely - mis-scoped for that config (`skip` claims "this failure is expected and correct," which a - flake-fighter rarely is). Never `skip` just to turn a red green. - -**Verify locally — your PR gets no CI (step 6), so this is the only proof:** flake → re-run with -`--repeat-each` and confirm it's stable; slow → confirm it still runs and passes; skip → confirm -it's skipped on the target config and **still runs elsewhere**. Then run `npm run flint`. Record -exactly what you ran — it goes in the PR body. - -## 5. Pick a reviewer - -No CODEOWNERS. Derive one from the touched file(s) — recent authors and frequent committers: - -```bash -git log --format='%an %ae' -n 20 -- -git log --format='%an' -n 200 -- | sort | uniq -c | sort -rn -``` - -Also skim recent merged PRs on those paths for who reviewed them. Pick someone with real recency -+ ownership. Record it as a git trailer so it survives the handoff: - -``` -Suggested-reviewer: dgozman -``` - -## 6. Commit and hand off (you never open the PR in CI) - -You **never push or open a PR** in CI or locally. The harness does that for you, after you commit. - -- **Exactly one commit** on the current branch (`CLAUDE.md` conventions; no - co-author / "generated with" trailers; never amend). -- **The commit message _is_ the PR** — the harness runs `gh pr create --fill`, mapping - subject→title and body→description. Write both in the Playwright bot voice - (`.github/workflows/bot-voice.md`) — verdict first, short, no slop. The - body must carry: the **DB evidence** (fail %, runs, bots) + any related issue; the - **linked emoji run history** for the selected test and bot; **what you verified locally** - and on which OS. -- **Nothing actionable?** Make no commit — the harness then skips the PR. - -Report what you committed, the reviewer, and why. - -## Guardrails - -- **One target, one commit.** Don't batch. -- **Scoped skips only** — narrow to the failing condition, never a whole file or browser matrix. -- **No unverified PRs** — if you cannot validate a scoped mitigation, make no commit. diff --git a/.github/workflows/fix-flakes.yml b/.github/workflows/fix-flakes.yml deleted file mode 100644 index c2fcb65e2d8fc..0000000000000 --- a/.github/workflows/fix-flakes.yml +++ /dev/null @@ -1,300 +0,0 @@ -name: "Fix flaky tests" - -on: - workflow_dispatch: - inputs: - hint: - description: "Optional focus hint passed to the triage and fix agents (e.g. 'webkit flakes')" - type: string - schedule: - - cron: "17 6 * * 1-5" - -permissions: {} - -jobs: - triage: - runs-on: ubuntu-latest - if: github.repository == 'microsoft/playwright' - outputs: - runner: ${{ steps.validate.outputs.runner }} - has_target: ${{ steps.validate.outputs.has_target }} - permissions: - actions: read - copilot-requests: write - concurrency: - group: fix-flakes-triage - cancel-in-progress: false - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Set up Node.js - uses: actions/setup-node@v6 - with: - node-version: lts/* - - - name: Install dependencies - run: npm ci - - - name: Download and refresh test-results DB - env: - GITHUB_TOKEN: ${{ github.token }} - run: | - node utils/test-results-db/cli.ts download - node utils/test-results-db/cli.ts update --lookback-days 3 - - - name: Install Copilot CLI - run: npm install -g @github/copilot - - - name: Pick an eligible OS with Copilot CLI - shell: bash - env: - COPILOT_GITHUB_TOKEN: ${{ github.token }} - HINT: ${{ inputs.hint }} - run: | - mkdir -p output - cat > output/triage-prompt.md <> "$GITHUB_OUTPUT" - exit 0 - fi - case "$runner" in - ubuntu-22.04|ubuntu-22.04-arm|ubuntu-24.04|macos-14-xlarge|macos-15-large|macos-15-xlarge|windows-latest) ;; - *) echo "::error::Triage chose a non-allowlisted runner: '$runner'." >&2; exit 1 ;; - esac - echo "Triage chose runner: $runner" - echo "runner=$runner" >> "$GITHUB_OUTPUT" - echo "has_target=true" >> "$GITHUB_OUTPUT" - - - name: Add triage transcript to job summary - if: always() - shell: bash - run: | - { - echo "## Fix-flakes triage transcript" - echo '' - cat "output/copilot-session.md" 2>/dev/null || echo "(no transcript)" - } >> "$GITHUB_STEP_SUMMARY" - - fix: - needs: triage - if: needs.triage.outputs.has_target == 'true' - runs-on: ${{ needs.triage.outputs.runner }} - timeout-minutes: 60 - outputs: - has_fix: ${{ steps.export.outputs.has_fix }} - permissions: - actions: read - copilot-requests: write - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ github.sha }} - fetch-depth: 0 - - - name: Set up Node.js - uses: actions/setup-node@v6 - with: - node-version: lts/* - - - name: Install dependencies - run: npm ci - - - name: Build - run: npm run build - - - name: Install browsers - shell: bash - run: | - if [ "$RUNNER_OS" = "Linux" ]; then - npx playwright install --with-deps - else - npx playwright install - fi - - - name: Download and refresh test-results DB - env: - GITHUB_TOKEN: ${{ github.token }} - run: | - node utils/test-results-db/cli.ts download - node utils/test-results-db/cli.ts update --lookback-days 3 - - - name: Install Copilot CLI - run: npm install -g @github/copilot - - - name: Configure git identity - shell: bash - run: | - git config user.name "Copilot" - git config user.email "223556219+Copilot@users.noreply.github.com" - - - name: Record base commit - id: base - shell: bash - run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - - - name: Fix a flaky/red test with Copilot CLI - shell: bash - env: - COPILOT_GITHUB_TOKEN: ${{ github.token }} - RUNNER: ${{ needs.triage.outputs.runner }} - HINT: ${{ inputs.hint }} - WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - run: | - mkdir -p output - cat > output/fix-prompt.md <> "$GITHUB_OUTPUT" - exit 0 - fi - if [ "$count" -ne 1 ]; then - echo "::error::Expected exactly one commit, got $count. Refusing to hand off." >&2 - exit 1 - fi - mkdir -p handoff - git format-patch -1 HEAD --stdout > handoff/fix.patch - echo "has_fix=true" >> "$GITHUB_OUTPUT" - - - name: Upload handoff (fix patch) - if: steps.export.outputs.has_fix == 'true' - uses: actions/upload-artifact@v4 - with: - name: fix-flakes-handoff-${{ needs.triage.outputs.runner }} - path: handoff/ - if-no-files-found: error - - - name: Add session transcript to job summary - if: always() - shell: bash - run: | - { - echo "## Fix-flakes session transcript (${{ needs.triage.outputs.runner }})" - echo '' - cat "output/copilot-session.md" 2>/dev/null || echo "(no transcript)" - } >> "$GITHUB_STEP_SUMMARY" - - - name: Upload session transcript - if: always() - uses: actions/upload-artifact@v4 - with: - name: fix-flakes-session-${{ needs.triage.outputs.runner }} - path: output/copilot-session.md - if-no-files-found: warn - - open_pr: - needs: [triage, fix] - if: needs.fix.outputs.has_fix == 'true' - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - - name: Checkout base commit - uses: actions/checkout@v6 - with: - ref: ${{ github.sha }} - fetch-depth: 0 - - - name: Download handoff - uses: actions/download-artifact@v4 - with: - name: fix-flakes-handoff-${{ needs.triage.outputs.runner }} - path: handoff - - - uses: actions/create-github-app-token@v3 - id: app-token - with: - client-id: ${{ vars.PLAYWRIGHT_APP_CLIENT_ID }} - private-key: ${{ secrets.PLAYWRIGHT_PRIVATE_KEY }} - - - name: Apply commit and open PR - env: - GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} - RUNNER: ${{ needs.triage.outputs.runner }} - REVIEW_OVERRIDE: skn0tt - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git fetch --no-tags origin main:refs/remotes/origin/main - branch="fix-flakes/${RUNNER}-${{ github.run_id }}" - git checkout -b "$branch" - git am handoff/fix.patch - git push origin "$branch" - suggested=$(git log -1 --format='%(trailers:key=Suggested-reviewer,valueonly)' | head -n1 | tr -d '[:space:]@') - reviewer="${REVIEW_OVERRIDE:-$suggested}" - args=(--repo "${{ github.repository }}" --base main --head "$branch" --fill) - if [ -n "$reviewer" ]; then - args+=(--reviewer "$reviewer") - else - echo "::warning::No reviewer (empty override and no Suggested-reviewer trailer)." - fi - gh pr create "${args[@]}" diff --git a/.github/workflows/infra.yml b/.github/workflows/infra.yml index fe5ef6cfbf7d6..12e120ca1c9ba 100644 --- a/.github/workflows/infra.yml +++ b/.github/workflows/infra.yml @@ -15,8 +15,8 @@ jobs: name: "docs & lint" runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: lts/* - run: npm ci @@ -37,17 +37,17 @@ jobs: name: "Lint snippets" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: lts/* - - uses: actions/setup-python@v6 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.11' - - uses: actions/setup-dotnet@v5 + - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: dotnet-version: 8.0.x - - uses: actions/setup-java@v5 + - uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0 with: distribution: 'zulu' java-version: '21' diff --git a/.github/workflows/pr-ci-triage.yml b/.github/workflows/pr-ci-triage.yml new file mode 100644 index 0000000000000..0b60d35749d68 --- /dev/null +++ b/.github/workflows/pr-ci-triage.yml @@ -0,0 +1,134 @@ +# Reusable CI triage for PR test failures. Callable from other repos +# (e.g. microsoft/playwright-browsers) via workflow_call. +name: PR CI Triage + +on: + workflow_call: + inputs: + pr_number: + description: 'PR number to triage' + required: true + type: string + +permissions: {} + +jobs: + triage: + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + actions: read + pull-requests: read + copilot-requests: write + outputs: + has_draft: ${{ steps.triage.outputs.has_draft }} + env: + PR_NUMBER: ${{ inputs.pr_number }} + TARGET_REPO: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: microsoft/playwright + ref: main + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + + - name: Install Copilot CLI + run: npm install -g @github/copilot + + - name: Triage failures with Copilot CLI + id: triage + env: + COPILOT_GITHUB_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + mkdir -p output + PROMPT=$(cat <> "$GITHUB_OUTPUT" + else + echo "has_draft=false" >> "$GITHUB_OUTPUT" + fi + + - name: Add session transcript to job summary + if: ${{ always() }} + run: | + { + echo "## CI triage session transcript (PR #$PR_NUMBER on $TARGET_REPO)" + echo '' + cat output/copilot-session.md 2>/dev/null || echo "(no transcript)" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload triage draft + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ci-triage-${{ inputs.pr_number }} + path: output/triage.md + if-no-files-found: warn + + post: + needs: triage + if: ${{ needs.triage.outputs.has_draft == 'true' }} + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + env: + PR_NUMBER: ${{ inputs.pr_number }} + GH_TOKEN: ${{ github.token }} + WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + REPORT_NAME: ${{ github.event.workflow_run.name || github.workflow }} + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: microsoft/playwright + ref: main + + - name: Download triage draft + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ci-triage-${{ inputs.pr_number }} + path: output + + - name: Post triage comment + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const { collapsePreviousComments } = require('./tests/config/postReportComment'); + const sentinel = ``; + const prNumber = +process.env.PR_NUMBER; + await collapsePreviousComments(github, context, prNumber, sentinel); + const triage = fs.readFileSync('output/triage.md', 'utf8'); + const body = `${triage}\n\nTriaged by the Playwright bot - [agent run](${process.env.WORKFLOW_URL})\n${sentinel}\n`; + await github.rest.issues.createComment({ + ...context.repo, + issue_number: prNumber, + body, + }); diff --git a/.github/workflows/publish_extension.yml b/.github/workflows/publish_extension.yml index a681e52c9dee3..d6453d8cd94bc 100644 --- a/.github/workflows/publish_extension.yml +++ b/.github/workflows/publish_extension.yml @@ -7,8 +7,8 @@ jobs: runs-on: ubuntu-latest environment: allow-publishing-extension-to-cws steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: lts/* cache: 'npm' diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index 18c678511d196..f78cb6e2c86c7 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -19,8 +19,8 @@ jobs: id-token: write # This is required for OIDC login (NPM publish) to succeed contents: read # This is required for actions/checkout to succeed steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: lts/* registry-url: 'https://registry.npmjs.org' @@ -51,11 +51,11 @@ jobs: runs-on: ubuntu-24.04 if: github.repository == 'microsoft/playwright' steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: lts/* - - uses: actions/create-github-app-token@v3 + - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 id: app-token with: client-id: ${{ vars.PLAYWRIGHT_APP_CLIENT_ID }} diff --git a/.github/workflows/roll_nodejs.yml b/.github/workflows/roll_nodejs.yml index 1287bbe7c9c0a..8fa66e685eea3 100644 --- a/.github/workflows/roll_nodejs.yml +++ b/.github/workflows/roll_nodejs.yml @@ -12,8 +12,8 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: lts/* - run: node utils/build/update-playwright-node.mjs @@ -33,14 +33,14 @@ jobs: git add . git commit -m "chore: roll driver/Dockerfile to recent Node.js LTS version" git push origin $BRANCH_NAME - - uses: actions/create-github-app-token@v3 + - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 id: app-token with: client-id: ${{ vars.PLAYWRIGHT_APP_CLIENT_ID }} private-key: ${{ secrets.PLAYWRIGHT_PRIVATE_KEY }} - name: Create Pull Request if: ${{ steps.prepare-branch.outputs.HAS_CHANGES == '1' }} - uses: actions/github-script@v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ steps.app-token.outputs.token }} script: | diff --git a/.github/workflows/roll_stable_test_runner.yml b/.github/workflows/roll_stable_test_runner.yml index 0e262839a61a0..7dc920a392a96 100644 --- a/.github/workflows/roll_stable_test_runner.yml +++ b/.github/workflows/roll_stable_test_runner.yml @@ -12,8 +12,8 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: lts/* - run: | @@ -38,14 +38,14 @@ jobs: git add . git commit -m "test: roll stable-test-runner to ${{ steps.bump.outputs.VERSION }}" git push origin $BRANCH_NAME - - uses: actions/create-github-app-token@v3 + - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 id: app-token with: client-id: ${{ vars.PLAYWRIGHT_APP_CLIENT_ID }} private-key: ${{ secrets.PLAYWRIGHT_PRIVATE_KEY }} - name: Create Pull Request if: ${{ steps.prepare-branch.outputs.HAS_CHANGES == '1' }} - uses: actions/github-script@v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ steps.app-token.outputs.token }} script: | diff --git a/.github/workflows/tests_bidi.yml b/.github/workflows/tests_bidi.yml index b11400d438fb4..e70fbd5a00384 100644 --- a/.github/workflows/tests_bidi.yml +++ b/.github/workflows/tests_bidi.yml @@ -39,13 +39,13 @@ jobs: - isPullRequest: true channel: bidi-chromium steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 if: github.event_name != 'workflow_dispatch' - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 if: github.event_name == 'workflow_dispatch' with: ref: ${{ github.event.inputs.ref }} - - uses: actions/setup-node@v6 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 20 - run: npm ci @@ -64,7 +64,7 @@ jobs: PWTEST_USE_BIDI_EXPECTATIONS: ${{ matrix.isPullRequest && '1' || '' }} - name: Upload csv report to GitHub if: ${{ !cancelled() }} - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: csv-report-${{ matrix.channel }} path: test-results/report.csv @@ -72,7 +72,7 @@ jobs: - name: Upload json report to GitHub if: ${{ !cancelled() }} - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: json-report-${{ matrix.channel }} path: test-results/report.json @@ -86,7 +86,7 @@ jobs: - name: Azure Login if: ${{ !cancelled() && github.ref == 'refs/heads/main' }} - uses: azure/login@v3 + uses: azure/login@7ddb5af1ef8758cf1353cf3b42f940aee27ba21c # v3.0.2 with: client-id: ${{ secrets.AZURE_BLOB_REPORTS_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_BLOB_REPORTS_TENANT_ID }} diff --git a/.github/workflows/tests_components.yml b/.github/workflows/tests_components.yml deleted file mode 100644 index e525ea0de3bec..0000000000000 --- a/.github/workflows/tests_components.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: "components" - -on: - push: - branches: - - main - - release-* - pull_request: - paths-ignore: - - 'browser_patches/**' - - 'docs/**' - - 'packages/extension/**' - - 'packages/playwright-core/src/server/bidi/**' - - 'packages/playwright-core/src/tools/**' - - 'tests/bidi/**' - - 'tests/extension/**' - - 'tests/mcp/**' - branches: - - main - - release-* - -env: - FORCE_COLOR: 1 - -jobs: - test_components: - name: ${{ matrix.os }} - Node.js ${{ matrix.node-version }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - node-version: [20] - include: - - os: ubuntu-latest - node-version: 22 - - os: ubuntu-latest - node-version: 24 - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 - with: - node-version: ${{ matrix.node-version }} - - run: npm ci - - run: npm run build - - run: npx playwright install --with-deps - - run: npm run ct diff --git a/.github/workflows/tests_docker.yml b/.github/workflows/tests_docker.yml index 2fd7dc34cb7a8..59f7077694a4b 100644 --- a/.github/workflows/tests_docker.yml +++ b/.github/workflows/tests_docker.yml @@ -39,8 +39,8 @@ jobs: steps: - name: Create ~/.azure directory run: mkdir -p ~/.azure - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 env: @@ -51,7 +51,7 @@ jobs: # main repo, where the secrets are present; forks fall back to Docker Hub. - name: Azure Login if: ${{ github.event_name == 'push' && github.repository == 'microsoft/playwright' }} - uses: azure/login@v3 + uses: azure/login@7ddb5af1ef8758cf1353cf3b42f940aee27ba21c # v3.0.2 with: client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }} @@ -114,7 +114,7 @@ jobs: - name: Azure Login if: ${{ !cancelled() && github.event_name == 'push' && github.repository == 'microsoft/playwright' }} - uses: azure/login@v3 + uses: azure/login@7ddb5af1ef8758cf1353cf3b42f940aee27ba21c # v3.0.2 with: client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }} diff --git a/.github/workflows/tests_extension.yml b/.github/workflows/tests_extension.yml index 38a413f319c95..bcee6798551ff 100644 --- a/.github/workflows/tests_extension.yml +++ b/.github/workflows/tests_extension.yml @@ -41,8 +41,8 @@ jobs: os: [macos-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 - run: npm ci diff --git a/.github/workflows/tests_mcp.yml b/.github/workflows/tests_mcp.yml index 8a5d04390435b..88dc1041c6229 100644 --- a/.github/workflows/tests_mcp.yml +++ b/.github/workflows/tests_mcp.yml @@ -54,7 +54,7 @@ jobs: id-token: write # This is required for OIDC login (azure/login) to succeed contents: read # This is required for actions/checkout to succeed steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/run-test with: node-version: "22" diff --git a/.github/workflows/tests_primary.yml b/.github/workflows/tests_primary.yml index a4da6a0e7f061..c01376f55ab0e 100644 --- a/.github/workflows/tests_primary.yml +++ b/.github/workflows/tests_primary.yml @@ -54,7 +54,7 @@ jobs: id-token: write # This is required for OIDC login (azure/login) to succeed contents: read # This is required for actions/checkout to succeed steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/run-test with: node-version: ${{ matrix.node-version }} @@ -127,7 +127,7 @@ jobs: id-token: write # This is required for OIDC login (azure/login) to succeed contents: read # This is required for actions/checkout to succeed steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/run-test with: node-version: ${{matrix.node-version}} @@ -152,7 +152,7 @@ jobs: package: [html-reporter, web] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/run-test with: node-version: 20 @@ -167,8 +167,8 @@ jobs: name: VSCode Extension runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 20 - run: npm ci @@ -215,7 +215,7 @@ jobs: id-token: write # This is required for OIDC login (azure/login) to succeed contents: read # This is required for actions/checkout to succeed steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - run: npm install -g yarn@1 - run: npm install -g pnpm@8 - name: Setup Ubuntu Binary Installation # TODO: Remove when https://github.com/electron/electron/issues/42510 is fixed @@ -247,7 +247,7 @@ jobs: clock: [frozen, realtime] runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/run-test with: node-version: 22 diff --git a/.github/workflows/tests_secondary.yml b/.github/workflows/tests_secondary.yml index 7e04617c61066..5622c1bb06b35 100644 --- a/.github/workflows/tests_secondary.yml +++ b/.github/workflows/tests_secondary.yml @@ -36,7 +36,7 @@ jobs: browser: chromium runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/run-test with: browsers-to-install: ${{ matrix.browser }} chromium @@ -56,7 +56,7 @@ jobs: browser: [chromium, firefox, webkit] runs-on: windows-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/run-test with: browsers-to-install: ${{ matrix.browser }} chromium @@ -83,7 +83,7 @@ jobs: node_version: 26 timeout-minutes: 30 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - run: npm install -g yarn@1 - run: npm install -g pnpm@8 - name: Setup Ubuntu Binary Installation # TODO: Remove when https://github.com/electron/electron/issues/42510 is fixed @@ -109,7 +109,7 @@ jobs: fail-fast: false runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/run-test with: browsers-to-install: chromium @@ -137,7 +137,7 @@ jobs: runs-on: ubuntu-24.04 runs-on: ${{ matrix.runs-on }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/run-test with: browsers-to-install: ${{ matrix.browser }} chromium @@ -170,7 +170,7 @@ jobs: - channel: msedge-dev runs-on: windows-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/run-test with: browsers-to-install: ${{ matrix.channel }} @@ -187,8 +187,8 @@ jobs: environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} runs-on: playwright-x64-ubuntu24-64-core steps: - - uses: actions/checkout@v6 - - uses: actions/setup-java@v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0 with: distribution: 'temurin' java-version: '21' @@ -197,7 +197,7 @@ jobs: echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - uses: actions/setup-node@v6 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22 - name: Create Android Emulator @@ -225,7 +225,7 @@ jobs: matrix: clock: [frozen, realtime] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/run-test with: node-version: 22 @@ -249,7 +249,7 @@ jobs: contents: read # This is required for actions/checkout to succeed runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Ubuntu Binary Installation # TODO: Remove when https://github.com/electron/electron/issues/42510 is fixed if: ${{ runner.os == 'Linux' }} run: | diff --git a/.github/workflows/tests_webview_simulator.yml b/.github/workflows/tests_webview_simulator.yml index 0a8c1cea2ba9e..681594c4d2f8c 100644 --- a/.github/workflows/tests_webview_simulator.yml +++ b/.github/workflows/tests_webview_simulator.yml @@ -25,8 +25,8 @@ jobs: matrix: shard: [1, 2, 3, 4] steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 20 @@ -81,7 +81,7 @@ jobs: echo "::endgroup::" - name: Boot iOS Simulator - uses: futureware-tech/simulator-action@v5 + uses: futureware-tech/simulator-action@e89aa8f93d3aec35083ff49d2854d07f7186f7f5 # v5 with: # Per wiki/Devices-macos-15.md only iPhone 16/17 series ship pre-installed; iPhone 15 isn't. model: 'iPhone 16' @@ -152,7 +152,7 @@ jobs: - name: Upload artifacts if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: webview-simulator-logs-${{ matrix.shard }} path: | diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index 7e3a766fef6c4..0a8188c619166 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -27,10 +27,10 @@ jobs: ISSUE: ${{ github.event.issue.number || inputs.issue }} steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: lts/* @@ -79,7 +79,7 @@ jobs: - name: Upload output if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: triage-${{ github.event.issue.number || inputs.issue }} path: output/triage.md @@ -97,7 +97,7 @@ jobs: WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} steps: - name: Download triage output - uses: actions/download-artifact@v4 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: triage-${{ github.event.issue.number || inputs.issue }} path: output diff --git a/.github/workflows/update_test_results_db.yml b/.github/workflows/update_test_results_db.yml index 067516b2f5bc4..bac2781f69d62 100644 --- a/.github/workflows/update_test_results_db.yml +++ b/.github/workflows/update_test_results_db.yml @@ -18,8 +18,8 @@ jobs: actions: read contents: read steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: lts/* - run: npm ci @@ -38,7 +38,7 @@ jobs: run: node utils/test-results-db/cli.ts truncate --max-runs 2000 - name: Upload database if: steps.ingest.outputs.imported != '0' - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: test-results-db path: utils/test-results-db/test-results.duckdb diff --git a/.gitignore b/.gitignore index df4ab0745589b..a1028105e3b80 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ DEPS.true .idea yarn.lock /packages/playwright-core/src/generated -/packages/playwright-ct-core/src/generated packages/*/lib/ drivers/ .android-sdk/ diff --git a/CLAUDE.md b/CLAUDE.md index a8034c3343168..e5f8fca852592 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,15 +23,11 @@ | `web` | Shared web UI components | | `injected` | Scripts injected into browser pages | -### Component Testing - -`playwright-ct-core`, `playwright-ct-react`, `playwright-ct-vue` - ### Key Directories | Directory | Purpose | |-----------|---------| -| `tests/` | All test suites (page, library, playwright-test, mcp, components, etc.) | +| `tests/` | All test suites (page, library, playwright-test, mcp, etc.) | | `docs/src/` | API documentation — **source of truth** for public TypeScript types | | `docs/src/api/` | Per-class API reference (`class-page.md`, `class-locator.md`, etc.) | | `utils/` | Build scripts, code generation, linting, doc tools | @@ -134,11 +130,26 @@ EOF )" ``` -Never add Co-Authored-By agents in commit message. -Never add "Generated with" in commit message. Never add test plan to PR description. Keep PR description short — a few bullet points at most. Branch naming for issue fixes: `fix-` +### No agent attribution — overrides agent defaults + +Coding agents ship with built-in instructions to append attribution footers — Claude Code, for +example, defaults to a `Co-Authored-By: Claude ...` trailer on every commit and a +`🤖 Generated with [Claude Code](...)` footer on every PR body. **Those defaults are revoked in +this repo.** Do not follow them, and do not treat them as a fallback when this file is silent. + +Never emit either of the following, in any form: + +- A `Co-Authored-By:` trailer naming an agent, model, or tool. +- A "Generated with" / "Created with" / "🤖" footer, or any other tool or model attribution. + +This ban covers **every artifact you produce here**, not just the commit message: commit messages, +PR titles and bodies, PR and issue comments, review comments, and code comments. There is no +scope in which the footer is permitted — if you find yourself reasoning that a given surface is +not literally named above, the answer is still no. + **Never amend commits.** Always create a new commit for follow-up changes, even when iterating on an open PR. Amending rewrites history and forces a force-push, losing the incremental review trail. Only amend if the user explicitly says so. **Never `git push` without an explicit instruction to push.** Applies even when a PR is already open for the branch — additional commits are immediately visible to reviewers. Commit locally, report what was committed, and wait. Only push when the user's message contains "push", "upload", "create PR", "ship it", or equivalent. diff --git a/README.md b/README.md index 57210c3677139..6705c3234f207 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🎭 Playwright -[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-151.0.7922.34-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-153.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-26.5-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord) +[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-153.0.8010.12-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-155.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-26.6-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord) ## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) @@ -296,9 +296,9 @@ The [Playwright VS Code extension](https://marketplace.visualstudio.com/items?it | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium1 151.0.7922.34 | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| WebKit 26.5 | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| Firefox 153.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium1 153.0.8010.12 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| WebKit 26.6 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Firefox 155.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | Headless and headed execution on all platforms. 1 Uses [Chrome for Testing](https://developer.chrome.com/blog/chrome-for-testing) by default. diff --git a/browser_patches/firefox/UPSTREAM_CONFIG.sh b/browser_patches/firefox/UPSTREAM_CONFIG.sh index ab997865af1d1..98c9002b25e03 100644 --- a/browser_patches/firefox/UPSTREAM_CONFIG.sh +++ b/browser_patches/firefox/UPSTREAM_CONFIG.sh @@ -1,3 +1,3 @@ REMOTE_URL="https://github.com/mozilla-firefox/firefox" BASE_BRANCH="release" -BASE_REVISION="f1b6c0f86b96b7e0688c26f65803576f27cdaf88" +BASE_REVISION="d065a04bc5610f496762935dee56604a78b91b51" diff --git a/browser_patches/firefox/juggler/NetworkObserver.js b/browser_patches/firefox/juggler/NetworkObserver.js index 85264a49dff62..2b4b3c5463f7c 100644 --- a/browser_patches/firefox/juggler/NetworkObserver.js +++ b/browser_patches/firefox/juggler/NetworkObserver.js @@ -301,13 +301,12 @@ class NetworkRequest { const proxy = this._networkObserver._targetRegistry.getProxyInfo(aChannel); credentials = proxy ? {username: proxy.username, password: proxy.password} : null; } else { - credentials = pageNetwork._target.browserContext().httpCredentials; + const origin = (aChannel.URI.scheme + '://' + aChannel.URI.hostPort).toLowerCase(); + const httpCredentials = pageNetwork._target.browserContext().httpCredentials || []; + credentials = httpCredentials.find(c => !c.origin || c.origin.toLowerCase() === origin) || null; } if (!credentials) return false; - const origin = aChannel.URI.scheme + '://' + aChannel.URI.hostPort; - if (credentials.origin && origin.toLowerCase() !== credentials.origin.toLowerCase()) - return false; authInfo.username = credentials.username; authInfo.password = credentials.password; // This will produce a new request with respective auth header set. @@ -543,6 +542,9 @@ class NetworkRequest { try { remoteIPAddress = this.httpChannel.remoteAddress; remotePort = this.httpChannel.remotePort; + // Gecko reports bare IPv6 addresses, bracket them to match Chromium. + if (remoteIPAddress && remoteIPAddress.includes(':')) + remoteIPAddress = `[${remoteIPAddress}]`; } catch (e) { // remoteAddress is not defined for cached requests. } @@ -907,7 +909,12 @@ class ResponseStorage { // Note: fulfilled request comes with decoded body right away. if ((request.httpChannel instanceof Ci.nsIEncodedChannel) && request.httpChannel.contentEncodings && !request.httpChannel.applyConversion && !request._fulfilled) { const encodingHeader = request.httpChannel.getResponseHeader("Content-Encoding"); - encodings = encodingHeader.split(/\s*\t*,\s*\t*/); + // Firefox itself skips "identity" and empty encodings when applying content + // conversions, and there is no stream converter registered for them. + encodings = encodingHeader.split(/\s*\t*,\s*\t*/).filter(encoding => { + const normalized = encoding.trim().toLowerCase(); + return normalized && normalized !== 'identity' && normalized !== 'x-identity'; + }); } this._responses.set(request.requestId, { body, diff --git a/browser_patches/firefox/juggler/TargetRegistry.js b/browser_patches/firefox/juggler/TargetRegistry.js index 0511315cf27b0..582f4e62f17ae 100644 --- a/browser_patches/firefox/juggler/TargetRegistry.js +++ b/browser_patches/firefox/juggler/TargetRegistry.js @@ -4,7 +4,7 @@ const {Helper} = ChromeUtils.importESModule('chrome://juggler/content/Helper.js'); const {Preferences} = ChromeUtils.importESModule("resource://gre/modules/Preferences.sys.mjs"); -const {ContextualIdentityService} = ChromeUtils.importESModule("resource://gre/modules/ContextualIdentityService.sys.mjs"); +const {ContextualIdentityService} = ChromeUtils.importESModule("moz-src:///toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs"); const {NetUtil} = ChromeUtils.importESModule('resource://gre/modules/NetUtil.sys.mjs'); const {AppConstants} = ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs"); diff --git a/browser_patches/firefox/juggler/components/Juggler.js b/browser_patches/firefox/juggler/components/Juggler.js index 7595958619381..ac9e6c26b78e9 100644 --- a/browser_patches/firefox/juggler/components/Juggler.js +++ b/browser_patches/firefox/juggler/components/Juggler.js @@ -40,6 +40,7 @@ ActorManagerParent.addJSWindowActors({ }, }, allFrames: true, + safeForUntrustedWebProcess: true, }, }); @@ -158,4 +159,3 @@ const jugglerInstance = new Juggler(); export var JugglerFactory = function() { return jugglerInstance; }; - diff --git a/browser_patches/firefox/juggler/content/Runtime.js b/browser_patches/firefox/juggler/content/Runtime.js index a29af41b2038c..78c05de7acee4 100644 --- a/browser_patches/firefox/juggler/content/Runtime.js +++ b/browser_patches/firefox/juggler/content/Runtime.js @@ -56,6 +56,10 @@ const disallowedMessageCategories = new Set([ class Runtime { constructor(isWorker = false) { this._debugger = new Debugger(); + // A debuggee global with these flags unset is pinned to the debuggable wasm/asm.js + // baseline tier, with the optimizing compiler disabled entirely. + this._debugger.allowUnobservedWasm = true; + this._debugger.allowUnobservedAsmJS = true; this._pendingPromises = new Map(); this._executionContexts = new Map(); this._windowToExecutionContext = new Map(); @@ -257,29 +261,37 @@ class Runtime { resolve = a; reject = b; }); - this._pendingPromises.set(obj.promiseID, {resolve, reject, executionContext, exceptionDetails}); + this._pendingPromises.set(obj.promiseID, {resolve, reject, executionContext, exceptionDetails, promiseObj: obj}); + // Debugger.onPromiseSettled hook was removed in Bug 2044167. Instead, attach + // reactions inside the debuggee that run a `debugger;` statement upon settling, + // and sweep pending promises from the onDebuggerStatement hook. Unlike + // dereferencing the promise and adding reactions from the privileged + // compartment, this also works in workers where there are no Xrays. if (this._pendingPromises.size === 1) - this._debugger.onPromiseSettled = this._onPromiseSettled.bind(this); + this._debugger.onDebuggerStatement = this._onDebuggerStatement.bind(this); + executionContext._debuggee.executeInGlobalWithBindings( + 'p.then(() => { debugger; }, () => { debugger; })', {p: obj}, {useInnerBindings: true}); return await promise; } - _onPromiseSettled(obj) { - const pendingPromise = this._pendingPromises.get(obj.promiseID); - if (!pendingPromise) - return; - this._pendingPromises.delete(obj.promiseID); + _onDebuggerStatement() { + for (const [promiseID, pendingPromise] of this._pendingPromises) { + const obj = pendingPromise.promiseObj; + if (obj.promiseState === 'pending') + continue; + this._pendingPromises.delete(promiseID); + if (obj.promiseState === 'fulfilled') { + pendingPromise.resolve({success: true, obj: obj.promiseValue}); + continue; + } + const debuggee = pendingPromise.executionContext._debuggee; + const errorInfo = debuggee.executeInGlobalWithBindings('({m: e?.message, s: e?.stack})', {e: obj.promiseReason}, {useInnerBindings: true}).return; + pendingPromise.exceptionDetails.text = errorInfo.getOwnPropertyDescriptor('m').value; + pendingPromise.exceptionDetails.stack = errorInfo.getOwnPropertyDescriptor('s').value; + pendingPromise.resolve({success: false, obj: null}); + } if (!this._pendingPromises.size) - this._debugger.onPromiseSettled = undefined; - - if (obj.promiseState === 'fulfilled') { - pendingPromise.resolve({success: true, obj: obj.promiseValue}); - return; - }; - const debuggee = pendingPromise.executionContext._debuggee; - const errorInfo = debuggee.executeInGlobalWithBindings('({m: e?.message, s: e?.stack})', {e: obj.promiseReason}, {useInnerBindings: true}).return; - pendingPromise.exceptionDetails.text = errorInfo.getOwnPropertyDescriptor('m').value; - pendingPromise.exceptionDetails.stack = errorInfo.getOwnPropertyDescriptor('s').value; - pendingPromise.resolve({success: false, obj: null}); + this._debugger.onDebuggerStatement = undefined; } createExecutionContext(domWindow, contextGlobal, auxData) { @@ -307,7 +319,7 @@ class Runtime { } } if (!this._pendingPromises.size) - this._debugger.onPromiseSettled = undefined; + this._debugger.onDebuggerStatement = undefined; this._debugger.removeDebuggee(destroyedContext._contextGlobal); this._executionContexts.delete(destroyedContext._id); if (destroyedContext._domWindow) diff --git a/browser_patches/firefox/juggler/content/WorkerMain.js b/browser_patches/firefox/juggler/content/WorkerMain.js index 99a6623e7623f..555a97a874e02 100644 --- a/browser_patches/firefox/juggler/content/WorkerMain.js +++ b/browser_patches/firefox/juggler/content/WorkerMain.js @@ -22,6 +22,10 @@ const runtime = new Runtime(true /* isWorker */); // Create execution context in the runtime only when the script // source was actually evaluated in it. const dbg = new Debugger(global); + // A debuggee global with these flags unset is pinned to the debuggable wasm/asm.js + // baseline tier, with the optimizing compiler disabled entirely. + dbg.allowUnobservedWasm = true; + dbg.allowUnobservedAsmJS = true; if (dbg.findScripts({global}).length) { runtime.createExecutionContext(null /* domWindow */, global, {}); } else { diff --git a/browser_patches/firefox/juggler/protocol/Protocol.js b/browser_patches/firefox/juggler/protocol/Protocol.js index db4f203fbaebc..f6a3f89f81ee2 100644 --- a/browser_patches/firefox/juggler/protocol/Protocol.js +++ b/browser_patches/firefox/juggler/protocol/Protocol.js @@ -273,7 +273,7 @@ const Browser = { 'setHTTPCredentials': { params: { browserContextId: t.Optional(t.String), - credentials: t.Nullable(networkTypes.HTTPCredentials), + credentials: t.Nullable(t.Array(networkTypes.HTTPCredentials)), }, }, 'setRequestInterception': { diff --git a/browser_patches/firefox/patches/bootstrap.diff b/browser_patches/firefox/patches/bootstrap.diff index bf56dc51a6976..daaca1705e07d 100644 --- a/browser_patches/firefox/patches/bootstrap.diff +++ b/browser_patches/firefox/patches/bootstrap.diff @@ -31,7 +31,7 @@ index 8337336dc894b44ea696bb780e448dfbdd8b6357..9eb83f33bb0415f28d1bcf66507d33d0 DWORD creationFlags = CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT; diff --git a/browser/installer/allowed-dupes.mn b/browser/installer/allowed-dupes.mn -index 96706c155f4dd317d93d7e5bf18f67598cce66a4..8f61011841cf06689a59210fb021d23f81db4fa1 100644 +index 3bc36691ce4b754fb80fa7d81d47dba786669dac..dbd3d465f66801e167e6e26d6eb6c474b4b1532c 100644 --- a/browser/installer/allowed-dupes.mn +++ b/browser/installer/allowed-dupes.mn @@ -66,6 +66,12 @@ browser/chrome/browser/builtin-addons/webcompat/shims/empty-shim.txt @@ -48,10 +48,10 @@ index 96706c155f4dd317d93d7e5bf18f67598cce66a4..8f61011841cf06689a59210fb021d23f browser/chrome/browser/content/activity-stream/data/content/tippytop/favicons/allegro-pl.ico browser/defaults/settings/main/search-config-icons/96327a73-c433-5eb4-a16d-b090cadfb80b diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in -index 203811a0fb980b47f113729cb1a16f56f376bfe5..83ac2cc8cdae99006fbc34319ed854c8ef88fcc4 100644 +index 9944fbec40229fe327cd311eb74be04f1d0f8b92..b3b60437eeb7ee729a3be040b8af7f0f738fa6c8 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in -@@ -204,6 +204,9 @@ +@@ -195,6 +195,9 @@ @RESPATH@/chrome/remote.manifest #endif @@ -109,10 +109,10 @@ index 257e87fe0c618684eb4216c8028ac886ef2d5562..8c13f020100dad9bc4e093d50bd3a1f9 const transportProvider = { setListener(upgradeListener) { diff --git a/docshell/base/BrowsingContext.cpp b/docshell/base/BrowsingContext.cpp -index d7155e79364de64fbf90c7fa37ac1713ed00bfa3..b38aece7733011cad0eac055c560e42b85ad467b 100644 +index 6662d0e720008838d4130ce6201ddac5f8390c98..40d41d987b6d59fddaac238cff86affe720bc9ed 100644 --- a/docshell/base/BrowsingContext.cpp +++ b/docshell/base/BrowsingContext.cpp -@@ -118,8 +118,11 @@ struct ParamTraits +@@ -119,8 +119,11 @@ struct ParamTraits template <> struct ParamTraits @@ -126,7 +126,7 @@ index d7155e79364de64fbf90c7fa37ac1713ed00bfa3..b38aece7733011cad0eac055c560e42b template <> struct ParamTraits -@@ -489,7 +492,11 @@ already_AddRefed BrowsingContext::CreateDetached( +@@ -484,7 +487,11 @@ already_AddRefed BrowsingContext::CreateDetached( fields.Get() = true; @@ -139,7 +139,7 @@ index d7155e79364de64fbf90c7fa37ac1713ed00bfa3..b38aece7733011cad0eac055c560e42b fields.Get() = inherit ? inherit->GetAllowJavascript() : true; -@@ -3544,6 +3551,15 @@ void BrowsingContext::DidSet(FieldIndex, +@@ -3558,6 +3565,15 @@ void BrowsingContext::DidSet(FieldIndex, }); } @@ -155,17 +155,17 @@ index d7155e79364de64fbf90c7fa37ac1713ed00bfa3..b38aece7733011cad0eac055c560e42b void BrowsingContext::DidSet(FieldIndex, nsString&& aOldValue) { MOZ_ASSERT(IsTop()); -@@ -3824,7 +3840,7 @@ void BrowsingContext::SetGeolocationServiceOverride( +@@ -3838,7 +3854,7 @@ void BrowsingContext::SetGeolocationServiceOverride( if (aGeolocationOverride.WasPassed()) { if (!mGeolocationServiceOverride) { - mGeolocationServiceOverride = MakeRefPtr(); + mGeolocationServiceOverride = MakeRefPtr(); - mGeolocationServiceOverride->Init(); + mGeolocationServiceOverride->Init(true /* isOverride */); } mGeolocationServiceOverride->Update(aGeolocationOverride.Value()); - } else if (RefPtr serviceOverride = + } else if (RefPtr serviceOverride = diff --git a/docshell/base/BrowsingContext.h b/docshell/base/BrowsingContext.h -index 6f7f58bba3be6f08b3b27e81f30864c61d683351..ac0d2dc75f3f9e99e97100958108f4504292283f 100644 +index aba8b50b04a5f8abe6670cddf16313a0f3156dbd..c9054e883de27f45796d644286f49e79210c2bf1 100644 --- a/docshell/base/BrowsingContext.h +++ b/docshell/base/BrowsingContext.h @@ -211,11 +211,11 @@ struct EmbedderColorSchemes { @@ -225,10 +225,10 @@ index 6f7f58bba3be6f08b3b27e81f30864c61d683351..ac0d2dc75f3f9e99e97100958108f450 void WalkPresContexts(Callback&&); void PresContextAffectingFieldChanged(); diff --git a/docshell/base/CanonicalBrowsingContext.cpp b/docshell/base/CanonicalBrowsingContext.cpp -index 00c69a2310f1056e8ef1199156cb593140af8a63..c6f5181e71b952fe36d83640eb776aa55c31eb2c 100644 +index 4a2127ed9b7e8b16a496b56e3382d910750b7a0e..c310686b8dc17da8def684df49fc0fbfa2a13afd 100644 --- a/docshell/base/CanonicalBrowsingContext.cpp +++ b/docshell/base/CanonicalBrowsingContext.cpp -@@ -303,6 +303,11 @@ void CanonicalBrowsingContext::ReplacedBy( +@@ -316,6 +316,11 @@ void CanonicalBrowsingContext::ReplacedBy( txn.SetInnerSizeSpoofedForRFP(GetInnerSizeSpoofedForRFP()); txn.SetIPAddressSpace(GetIPAddressSpace()); txn.SetParentalControlsEnabled(GetParentalControlsEnabled()); @@ -240,7 +240,7 @@ index 00c69a2310f1056e8ef1199156cb593140af8a63..c6f5181e71b952fe36d83640eb776aa5 if (!GetLanguageOverride().IsEmpty()) { // Reapply language override to update the corresponding realm. -@@ -1946,6 +1951,12 @@ void CanonicalBrowsingContext::LoadURI(nsIURI* aURI, +@@ -1976,6 +1981,12 @@ void CanonicalBrowsingContext::LoadURI(nsIURI* aURI, (void)SetIsCaptivePortalTab(true); } @@ -254,7 +254,7 @@ index 00c69a2310f1056e8ef1199156cb593140af8a63..c6f5181e71b952fe36d83640eb776aa5 } diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp -index 275d0ce1d33e7aa887eebeadac786aac44e0ba10..bff2b58d2169bbd40aa4ac48c656d7f9a0519489 100644 +index 5a6d13023782c1b4f677b9a8cc88491d0ea5f533..312899d39bf4d9fe2515bc32b8fa8c8da9eee652 100644 --- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -16,6 +16,12 @@ @@ -270,7 +270,15 @@ index 275d0ce1d33e7aa887eebeadac786aac44e0ba10..bff2b58d2169bbd40aa4ac48c656d7f9 #include "mozilla/Attributes.h" #include "mozilla/AutoRestore.h" #include "mozilla/BasePrincipal.h" -@@ -64,6 +70,7 @@ +@@ -51,6 +57,7 @@ + #include "mozilla/Telemetry.h" + + #include "mozilla/WidgetUtils.h" ++#include "mozilla/GeolocationService.h" + + #include "mozilla/dom/AutoEntryScript.h" + #include "mozilla/dom/ChildProcessChannelListener.h" +@@ -64,6 +71,7 @@ #include "mozilla/dom/DocGroup.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/FragmentDirective.h" @@ -278,7 +286,7 @@ index 275d0ce1d33e7aa887eebeadac786aac44e0ba10..bff2b58d2169bbd40aa4ac48c656d7f9 #include "mozilla/dom/HTMLAnchorElement.h" #include "mozilla/dom/HTMLIFrameElement.h" #include "mozilla/dom/Navigation.h" -@@ -96,6 +103,7 @@ +@@ -96,6 +104,7 @@ #include "mozilla/dom/DocumentBinding.h" #include "mozilla/glean/DocshellMetrics.h" #include "mozilla/ipc/ProtocolUtils.h" @@ -286,7 +294,7 @@ index 275d0ce1d33e7aa887eebeadac786aac44e0ba10..bff2b58d2169bbd40aa4ac48c656d7f9 #include "mozilla/net/DocumentChannel.h" #include "mozilla/net/DocumentChannelChild.h" #include "mozilla/net/ParentChannelWrapper.h" -@@ -120,6 +128,7 @@ +@@ -120,6 +129,7 @@ #include "nsIDocumentViewer.h" #include "mozilla/dom/Document.h" #include "nsHTMLDocument.h" @@ -294,7 +302,7 @@ index 275d0ce1d33e7aa887eebeadac786aac44e0ba10..bff2b58d2169bbd40aa4ac48c656d7f9 #include "nsIDocumentLoaderFactory.h" #include "nsIDOMWindow.h" #include "nsIEditingSession.h" -@@ -215,6 +224,7 @@ +@@ -216,6 +226,7 @@ #include "nsGlobalWindowInner.h" #include "nsGlobalWindowOuter.h" #include "nsJSEnvironment.h" @@ -302,7 +310,7 @@ index 275d0ce1d33e7aa887eebeadac786aac44e0ba10..bff2b58d2169bbd40aa4ac48c656d7f9 #include "nsNetCID.h" #include "nsNetUtil.h" #include "nsObjectLoadingContent.h" -@@ -355,6 +365,14 @@ nsDocShell::nsDocShell(BrowsingContext* aBrowsingContext, +@@ -356,6 +367,14 @@ nsDocShell::nsDocShell(BrowsingContext* aBrowsingContext, mAllowDNSPrefetch(true), mAllowWindowControl(true), mCSSErrorReportingEnabled(false), @@ -317,7 +325,7 @@ index 275d0ce1d33e7aa887eebeadac786aac44e0ba10..bff2b58d2169bbd40aa4ac48c656d7f9 mAllowAuth(mItemType == typeContent), mAllowKeywordFixup(false), mDisableMetaRefreshWhenInactive(false), -@@ -2976,6 +2994,174 @@ nsDocShell::GetMessageManager(ContentFrameMessageManager** aMessageManager) { +@@ -2978,6 +2997,174 @@ nsDocShell::GetMessageManager(ContentFrameMessageManager** aMessageManager) { return NS_OK; } @@ -415,7 +423,7 @@ index 275d0ce1d33e7aa887eebeadac786aac44e0ba10..bff2b58d2169bbd40aa4ac48c656d7f9 + ToSupports(element), "juggler-file-picker-shown", nullptr); +} + -+RefPtr nsDocShell::GetGeolocationServiceOverride() { ++RefPtr nsDocShell::GetGeolocationServiceOverride() { + return GetRootDocShell()->mGeolocationServiceOverride; +} + @@ -423,8 +431,8 @@ index 275d0ce1d33e7aa887eebeadac786aac44e0ba10..bff2b58d2169bbd40aa4ac48c656d7f9 +nsDocShell::SetGeolocationOverride(nsIDOMGeoPosition* aGeolocationOverride) { + if (aGeolocationOverride) { + if (!mGeolocationServiceOverride) { -+ mGeolocationServiceOverride = new nsGeolocationService(); -+ mGeolocationServiceOverride->Init(); ++ mGeolocationServiceOverride = new GeolocationService(); ++ mGeolocationServiceOverride->Init(true /* isOverride */); + } + mGeolocationServiceOverride->Update(aGeolocationOverride); + } else { @@ -492,7 +500,7 @@ index 275d0ce1d33e7aa887eebeadac786aac44e0ba10..bff2b58d2169bbd40aa4ac48c656d7f9 NS_IMETHODIMP nsDocShell::GetIsNavigating(bool* aOut) { *aOut = mIsNavigating; -@@ -4655,7 +4841,7 @@ nsDocShell::GetVisibility(bool* aVisibility) { +@@ -4671,7 +4858,7 @@ nsDocShell::GetVisibility(bool* aVisibility) { } void nsDocShell::ActivenessMaybeChanged() { @@ -501,7 +509,7 @@ index 275d0ce1d33e7aa887eebeadac786aac44e0ba10..bff2b58d2169bbd40aa4ac48c656d7f9 if (RefPtr presShell = GetPresShell()) { presShell->ActivenessMaybeChanged(); } -@@ -7651,6 +7837,12 @@ nsresult nsDocShell::PerformRetargeting(nsDocShellLoadState* aLoadState) { +@@ -7686,6 +7873,12 @@ nsresult nsDocShell::PerformRetargeting(nsDocShellLoadState* aLoadState) { true, // aForceNoOpener getter_AddRefs(newBC)); MOZ_ASSERT(!newBC); @@ -514,7 +522,7 @@ index 275d0ce1d33e7aa887eebeadac786aac44e0ba10..bff2b58d2169bbd40aa4ac48c656d7f9 return rv; } -@@ -8883,6 +9075,16 @@ nsresult nsDocShell::InternalLoad(nsDocShellLoadState* aLoadState, +@@ -8926,6 +9119,16 @@ nsresult nsDocShell::InternalLoad(nsDocShellLoadState* aLoadState, attrs.SetFirstPartyDomain(isTopLevelDoc, aLoadState->URI()); nsCOMPtr req; @@ -531,7 +539,7 @@ index 275d0ce1d33e7aa887eebeadac786aac44e0ba10..bff2b58d2169bbd40aa4ac48c656d7f9 rv = DoURILoad(aLoadState, aCacheKey, getter_AddRefs(req)); if (NS_SUCCEEDED(rv)) { -@@ -12019,6 +12221,9 @@ class OnLinkClickEvent : public Runnable { +@@ -12036,6 +12239,9 @@ class OnLinkClickEvent : public CancelableRunnable, public SupportsWeakPtr { mHandler->OnLinkClickSync(mContent, mLoadState, mNoOpenerImplied, mTriggeringPrincipal); } @@ -541,20 +549,32 @@ index 275d0ce1d33e7aa887eebeadac786aac44e0ba10..bff2b58d2169bbd40aa4ac48c656d7f9 return NS_OK; } -@@ -12136,6 +12341,8 @@ nsresult nsDocShell::OnLinkClick( +@@ -12107,6 +12313,11 @@ nsresult nsDocShell::OnFormSubmit(HTMLFormElement* aForm, + return OnLinkClickSync(aForm, aLoadState, false, aForm->NodePrincipal()); + } + ++ nsCOMPtr observerService = ++ mozilla::services::GetObserverService(); ++ observerService->NotifyObservers(ToSupports(aForm), "juggler-link-click", ++ nullptr); ++ + auto result = OnLinkClickWithLoadState(aForm, aLoadState, false, + aForm->NodePrincipal()); + if (result.isErr()) { +@@ -12245,6 +12456,8 @@ nsresult nsDocShell::OnLinkClick( + ownerDoc->GetScriptTrackingFlags()); + loadState->SetHistoryBehavior(NavigationHistoryBehavior::Auto); - RefPtr ev = MakeRefPtr( - this, aContent, loadState, noOpenerImplied, aTriggeringPrincipal); + nsCOMPtr observerService = mozilla::services::GetObserverService(); + observerService->NotifyObservers(ToSupports(aContent), "juggler-link-click", nullptr); - return Dispatch(ev.forget()); - } - + auto result = OnLinkClickWithLoadState(aContent, loadState, noOpenerImplied, + aTriggeringPrincipal); + return result.isErr() ? result.unwrapErr() : NS_OK; diff --git a/docshell/base/nsDocShell.h b/docshell/base/nsDocShell.h -index aa834b35a1fb2ff6fe3cdd6de9582e48da510be5..ee2f86ddd28a9e18ee33cf509438b5f28cc8c952 100644 +index 5b17b1b5a7b61f6032ad05fbc65fb6672a64c09c..008d49bd6d6f1a8e7290ca1b59a8babe988dfd9f 100644 --- a/docshell/base/nsDocShell.h +++ b/docshell/base/nsDocShell.h -@@ -15,6 +15,7 @@ +@@ -16,6 +16,7 @@ #include "mozilla/dom/BrowsingContext.h" #include "mozilla/dom/NavigationBinding.h" #include "mozilla/dom/SessionHistoryEntry.h" @@ -562,15 +582,15 @@ index aa834b35a1fb2ff6fe3cdd6de9582e48da510be5..ee2f86ddd28a9e18ee33cf509438b5f2 #include "mozilla/dom/WindowProxyHolder.h" #include "nsCOMPtr.h" #include "nsCharsetSource.h" -@@ -84,6 +85,7 @@ class nsCommandManager; - class nsDocShellEditorData; - class nsDOMNavigationTiming; - class nsDSURIContentListener; -+class nsGeolocationService; - class nsGlobalWindowOuter; - - class FramingChecker; -@@ -397,6 +399,15 @@ class nsDocShell final : public nsDocLoader, +@@ -42,6 +43,7 @@ + + namespace mozilla { + class Encoding; ++class GeolocationService; + class HTMLEditor; + class ObservedDocShell; + class ScrollContainerFrame; +@@ -383,6 +385,15 @@ class nsDocShell final : public nsDocLoader, void SetWillChangeProcess() { mWillChangeProcess = true; } bool WillChangeProcess() { return mWillChangeProcess; } @@ -581,12 +601,12 @@ index aa834b35a1fb2ff6fe3cdd6de9582e48da510be5..ee2f86ddd28a9e18ee33cf509438b5f2 + + bool IsBypassCSPEnabled(); + -+ RefPtr GetGeolocationServiceOverride(); ++ RefPtr GetGeolocationServiceOverride(); + // Creates a real network channel (not a DocumentChannel) using the specified // parameters. // Used by nsDocShell when not using DocumentChannel, by DocumentLoadListener -@@ -988,6 +999,8 @@ class nsDocShell final : public nsDocLoader, +@@ -1000,6 +1011,8 @@ class nsDocShell final : public nsDocLoader, bool CSSErrorReportingEnabled() const { return mCSSErrorReportingEnabled; } @@ -595,7 +615,7 @@ index aa834b35a1fb2ff6fe3cdd6de9582e48da510be5..ee2f86ddd28a9e18ee33cf509438b5f2 // Handles retrieval of subframe session history for nsDocShell::LoadURI. If a // load is requested in a subframe of the current DocShell, the subframe // loadType may need to reflect the loadType of the parent document, or in -@@ -1299,6 +1312,16 @@ class nsDocShell final : public nsDocLoader, +@@ -1323,6 +1336,16 @@ class nsDocShell final : public nsDocLoader, bool mAllowDNSPrefetch : 1; bool mAllowWindowControl : 1; bool mCSSErrorReportingEnabled : 1; @@ -604,7 +624,7 @@ index aa834b35a1fb2ff6fe3cdd6de9582e48da510be5..ee2f86ddd28a9e18ee33cf509438b5f2 + bool mBypassCSPEnabled : 1; + bool mForceActiveState : 1; + bool mDisallowBFCache : 1; -+ RefPtr mGeolocationServiceOverride; ++ RefPtr mGeolocationServiceOverride; + ReducedMotionOverride mReducedMotionOverride; + ForcedColorsOverride mForcedColorsOverride; + ContrastOverride mContrastOverride; @@ -613,7 +633,7 @@ index aa834b35a1fb2ff6fe3cdd6de9582e48da510be5..ee2f86ddd28a9e18ee33cf509438b5f2 bool mAllowKeywordFixup : 1; bool mDisableMetaRefreshWhenInactive : 1; diff --git a/docshell/base/nsIDocShell.idl b/docshell/base/nsIDocShell.idl -index 4aa56b4ce16915d334f32c18953604765699e05a..ef80a46511d870c909dda6182e332c155a8e250c 100644 +index db0721841de3732d297b1ca31ae009e9e6a5356b..2c95a3ebbc437d31d393b5243b923f4f6a1f3483 100644 --- a/docshell/base/nsIDocShell.idl +++ b/docshell/base/nsIDocShell.idl @@ -43,6 +43,7 @@ interface nsIURI; @@ -667,10 +687,10 @@ index 4aa56b4ce16915d334f32c18953604765699e05a..ef80a46511d870c909dda6182e332c15 * This attempts to save any applicable layout history state (like * scroll position) in the nsISHEntry. This is normally done diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp -index 1081a150609c6068cde605228da6e95f84f06d6b..f51d5f53ea1d8985e7f75e4aba2a6996ac594d21 100644 +index 9602541b0cb109fdfb0051ac0419f1a2b3d9527e..50e4b92b62e557880d6e75dfeda5a3f1e5528838 100644 --- a/dom/base/Document.cpp +++ b/dom/base/Document.cpp -@@ -3655,6 +3655,9 @@ void Document::SendToConsole(nsCOMArray& aMessages) { +@@ -3845,6 +3845,9 @@ void Document::SendToConsole(nsCOMArray& aMessages) { } void Document::ApplySettingsFromCSP(bool aSpeculative) { @@ -680,7 +700,7 @@ index 1081a150609c6068cde605228da6e95f84f06d6b..f51d5f53ea1d8985e7f75e4aba2a6996 nsresult rv = NS_OK; if (!aSpeculative) { nsIContentSecurityPolicy* csp = PolicyContainer::GetCSP(mPolicyContainer); -@@ -3752,6 +3755,11 @@ nsresult Document::InitCSP(nsIChannel* aChannel) { +@@ -3942,6 +3945,11 @@ nsresult Document::InitCSP(nsIChannel* aChannel) { MOZ_ASSERT(mPolicyContainer, "Policy container must be initialized before CSP!"); @@ -692,7 +712,7 @@ index 1081a150609c6068cde605228da6e95f84f06d6b..f51d5f53ea1d8985e7f75e4aba2a6996 // If this is a data document - no need to set CSP. if (mLoadedAsData) { return NS_OK; -@@ -4722,6 +4730,10 @@ bool Document::HasFocus(ErrorResult& rv) const { +@@ -4923,6 +4931,10 @@ bool Document::HasFocus(ErrorResult& rv) const { return false; } @@ -704,10 +724,10 @@ index 1081a150609c6068cde605228da6e95f84f06d6b..f51d5f53ea1d8985e7f75e4aba2a6996 return false; } diff --git a/dom/base/Navigator.cpp b/dom/base/Navigator.cpp -index 079746f77996ca03464c8f30b00cd34817b8430d..770a58853159c5d4972aab6002ab68254ee22360 100644 +index 818005d2cf111f84d891d7f4f925fbaa193ad873..ccf2153d03a77e322d9d771cbce2ec4758718ac4 100644 --- a/dom/base/Navigator.cpp +++ b/dom/base/Navigator.cpp -@@ -2381,7 +2381,8 @@ bool Navigator::Webdriver() { +@@ -2371,7 +2371,8 @@ bool Navigator::Webdriver() { } #endif @@ -718,10 +738,10 @@ index 079746f77996ca03464c8f30b00cd34817b8430d..770a58853159c5d4972aab6002ab6825 AutoplayPolicy Navigator::GetAutoplayPolicy(AutoplayPolicyMediaType aType) { diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp -index 55a786dd5bb66d2bb51568407bc942c8ac2e5fe4..c73e34868b03a11b44d195f419fb3fb2e7869965 100644 +index d5c7b398dfb967c4cf283264857c3037fa47f16f..9e1a463705fe962c4266463df1aebb18b914fbfc 100644 --- a/dom/base/nsContentUtils.cpp +++ b/dom/base/nsContentUtils.cpp -@@ -9873,6 +9873,7 @@ Result nsContentUtils::SynthesizeMouseEvent( +@@ -10380,6 +10380,7 @@ Result nsContentUtils::SynthesizeMouseEvent( EventMessage msg; Maybe exitFrom; bool contextMenuKey = false; @@ -729,7 +749,7 @@ index 55a786dd5bb66d2bb51568407bc942c8ac2e5fe4..c73e34868b03a11b44d195f419fb3fb2 if (aType.EqualsLiteral("mousedown")) { msg = eMouseDown; } else if (aType.EqualsLiteral("mouseup")) { -@@ -9899,13 +9900,26 @@ Result nsContentUtils::SynthesizeMouseEvent( +@@ -10406,13 +10407,26 @@ Result nsContentUtils::SynthesizeMouseEvent( msg = eMouseHitTest; } else if (aType.EqualsLiteral("MozMouseExploreByTouch")) { msg = eMouseExploreByTouch; @@ -757,7 +777,7 @@ index 55a786dd5bb66d2bb51568407bc942c8ac2e5fe4..c73e34868b03a11b44d195f419fb3fb2 if (MOZ_UNLIKELY(aOptions.mIsWidgetEventSynthesized)) { MOZ_ASSERT_UNREACHABLE( "The event shouldn't be dispatched as a synthesized event"); -@@ -9933,6 +9947,7 @@ Result nsContentUtils::SynthesizeMouseEvent( +@@ -10440,6 +10454,7 @@ Result nsContentUtils::SynthesizeMouseEvent( mozilla::widget::AutoSynthesizedEventCallbackNotifier notifier(callback); WidgetMouseEvent& mouseOrPointerEvent = @@ -765,7 +785,7 @@ index 55a786dd5bb66d2bb51568407bc942c8ac2e5fe4..c73e34868b03a11b44d195f419fb3fb2 pointerEvent.isSome() ? pointerEvent.ref() : mouseEvent.ref(); mouseOrPointerEvent.pointerId = aMouseEventData.mIdentifier; mouseOrPointerEvent.mModifiers = -@@ -9958,6 +9973,7 @@ Result nsContentUtils::SynthesizeMouseEvent( +@@ -10465,6 +10480,7 @@ Result nsContentUtils::SynthesizeMouseEvent( aOptions.mIsDOMEventSynthesized; mouseOrPointerEvent.mExitFrom = std::move(exitFrom); mouseOrPointerEvent.mCallbackId = notifier.SaveCallback(); @@ -774,10 +794,10 @@ index 55a786dd5bb66d2bb51568407bc942c8ac2e5fe4..c73e34868b03a11b44d195f419fb3fb2 nsPresContext* presContext = aPresShell->GetPresContext(); if (!presContext) { diff --git a/dom/base/nsFocusManager.cpp b/dom/base/nsFocusManager.cpp -index ed255ae7f0f3c3465f4d38a8af2a7899e0dbf3b9..aa60b5bdc059121ac7a9163a40f0d9d23afa5195 100644 +index c9a6ee9c507129ab6fdd1deca98b368eb48170d5..a312a2d1bfdc1d6551d78242b1ddf03975249408 100644 --- a/dom/base/nsFocusManager.cpp +++ b/dom/base/nsFocusManager.cpp -@@ -1875,6 +1875,10 @@ Maybe nsFocusManager::SetFocusInner(Element* aNewContent, +@@ -1867,6 +1867,10 @@ Maybe nsFocusManager::SetFocusInner(Element* aNewContent, (GetActiveBrowsingContext() == newRootBrowsingContext); } @@ -788,7 +808,7 @@ index ed255ae7f0f3c3465f4d38a8af2a7899e0dbf3b9..aa60b5bdc059121ac7a9163a40f0d9d2 // Exit fullscreen if a website focuses another window if (StaticPrefs::full_screen_api_exit_on_windowRaise() && !isElementInActiveWindow && (aFlags & FLAG_RAISE)) { -@@ -2436,6 +2440,7 @@ bool nsFocusManager::BlurImpl(BrowsingContext* aBrowsingContextToClear, +@@ -2428,6 +2432,7 @@ bool nsFocusManager::BlurImpl(BrowsingContext* aBrowsingContextToClear, bool aIsLeavingDocument, bool aAdjustWidget, bool aRemainActive, Element* aElementToFocus, uint64_t aActionId) { @@ -796,7 +816,7 @@ index ed255ae7f0f3c3465f4d38a8af2a7899e0dbf3b9..aa60b5bdc059121ac7a9163a40f0d9d2 LOGFOCUS(("<>", aActionId)); // hold a reference to the focused content, which may be null -@@ -2479,6 +2484,11 @@ bool nsFocusManager::BlurImpl(BrowsingContext* aBrowsingContextToClear, +@@ -2471,6 +2476,11 @@ bool nsFocusManager::BlurImpl(BrowsingContext* aBrowsingContextToClear, return true; } @@ -808,7 +828,7 @@ index ed255ae7f0f3c3465f4d38a8af2a7899e0dbf3b9..aa60b5bdc059121ac7a9163a40f0d9d2 // Keep a ref to presShell since dispatching the DOM event may cause // the document to be destroyed. RefPtr presShell = docShell->GetPresShell(); -@@ -3181,7 +3191,9 @@ void nsFocusManager::RaiseWindow(nsPIDOMWindowOuter* aWindow, +@@ -3180,7 +3190,9 @@ void nsFocusManager::RaiseWindow(nsPIDOMWindowOuter* aWindow, } } @@ -820,10 +840,10 @@ index ed255ae7f0f3c3465f4d38a8af2a7899e0dbf3b9..aa60b5bdc059121ac7a9163a40f0d9d2 // care of lowering the present active window. This happens in // a separate runnable to avoid touching multiple windows in diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp -index 0fbb9d9e11c72f862d5777a0554a142336f8f17f..80ce0fc7a1b42722eb55a4a2e03af4022126a374 100644 +index b9caf10ddbecb6c3ac5d5483403a09243befcde1..7a3d9923405cee8eb4936d028c2df2c933d3293e 100644 --- a/dom/base/nsGlobalWindowOuter.cpp +++ b/dom/base/nsGlobalWindowOuter.cpp -@@ -2535,10 +2535,16 @@ nsresult nsGlobalWindowOuter::SetNewDocument(Document* aDocument, +@@ -2534,10 +2534,16 @@ nsresult nsGlobalWindowOuter::SetNewDocument(Document* aDocument, }(); if (!isAboutBlankInChromeDocshell) { @@ -844,7 +864,7 @@ index 0fbb9d9e11c72f862d5777a0554a142336f8f17f..80ce0fc7a1b42722eb55a4a2e03af402 } } -@@ -2658,6 +2664,19 @@ void nsGlobalWindowOuter::DispatchDOMWindowCreated() { +@@ -2657,6 +2663,19 @@ void nsGlobalWindowOuter::DispatchDOMWindowCreated() { } } @@ -877,10 +897,10 @@ index fa95821e51984beb7e4672bb82038adb0f8e97d2..08b765dfee64001bde59f99704046246 // Outer windows only. virtual void EnsureSizeAndPositionUpToDate() override; diff --git a/dom/base/nsINode.cpp b/dom/base/nsINode.cpp -index 5b21525de8dc0cc86bd14e2b36fcd9f0059a030a..65c6027e614bb8566a7a24bac6a7b75aa93d4032 100644 +index d1564d960952c0c2b4caf9c485309f7ddfb52257..eff181a2fd215bd3e9d3d5703012e9fa3fdf4a4d 100644 --- a/dom/base/nsINode.cpp +++ b/dom/base/nsINode.cpp -@@ -1803,6 +1803,61 @@ void nsINode::GetBoxQuadsFromWindowOrigin(const BoxQuadOptions& aOptions, +@@ -1980,6 +1980,61 @@ void nsINode::GetBoxQuadsFromWindowOrigin(const BoxQuadOptions& aOptions, mozilla::GetBoxQuadsFromWindowOrigin(this, aOptions, aResult, aRv); } @@ -943,10 +963,10 @@ index 5b21525de8dc0cc86bd14e2b36fcd9f0059a030a..65c6027e614bb8566a7a24bac6a7b75a DOMQuad& aQuad, const GeometryNode& aFrom, const ConvertCoordinateOptions& aOptions, CallerType aCallerType, diff --git a/dom/base/nsINode.h b/dom/base/nsINode.h -index 4c882db32fb458c4e88dc35cdf6ea4c2ce6675f0..0b5ad5cb09b36bde91ddf5390e2e520c76bf6f49 100644 +index 271c98870e2ed923dfde62635de9557eda90864c..16c69324c83735792894130bf1a8728e16d8d0b0 100644 --- a/dom/base/nsINode.h +++ b/dom/base/nsINode.h -@@ -2551,6 +2551,10 @@ class nsINode : public mozilla::dom::EventTarget { +@@ -3057,6 +3057,10 @@ class nsINode : public mozilla::dom::EventTarget { nsTArray>& aResult, ErrorResult& aRv); @@ -958,7 +978,7 @@ index 4c882db32fb458c4e88dc35cdf6ea4c2ce6675f0..0b5ad5cb09b36bde91ddf5390e2e520c DOMQuad& aQuad, const TextOrElementOrDocument& aFrom, const ConvertCoordinateOptions& aOptions, CallerType aCallerType, diff --git a/dom/chrome-webidl/BrowsingContext.webidl b/dom/chrome-webidl/BrowsingContext.webidl -index d0508a8c29e5bd008213f781beb8eb1fcb458fb8..518580aa7a2eb355f690d904d091e80c34a98912 100644 +index 91b2d3ccaad070d887b609ef5a92147cc16a4d12..35c581dd07c3f6defc4ad87f27456189d057b4ff 100644 --- a/dom/chrome-webidl/BrowsingContext.webidl +++ b/dom/chrome-webidl/BrowsingContext.webidl @@ -72,6 +72,17 @@ enum PrefersReducedMotionOverride { @@ -990,10 +1010,10 @@ index d0508a8c29e5bd008213f781beb8eb1fcb458fb8..518580aa7a2eb355f690d904d091e80c * A unique identifier for the browser element that is hosting this * BrowsingContext tree. Every BrowsingContext in the element's tree will diff --git a/dom/events/EventStateManager.cpp b/dom/events/EventStateManager.cpp -index f95ba91a36f5b4ac906a2d37a814d6eb42b31151..8e373d2e6eee50f60aa12c2651bf8c3fc7c35b8a 100644 +index c1f865f86435f58b29c582005f60eab739548de5..552e5358951786d6d446cc5b0b0b7d39445409cd 100644 --- a/dom/events/EventStateManager.cpp +++ b/dom/events/EventStateManager.cpp -@@ -2104,6 +2104,25 @@ static BrowserParent* GetBrowserParentAncestor(BrowserParent* aBrowserParent) { +@@ -2110,6 +2110,25 @@ static BrowserParent* GetBrowserParentAncestor(BrowserParent* aBrowserParent) { return bbp->Manager(); } @@ -1019,7 +1039,7 @@ index f95ba91a36f5b4ac906a2d37a814d6eb42b31151..8e373d2e6eee50f60aa12c2651bf8c3f static void DispatchCrossProcessMouseExitEvents(WidgetMouseEvent* aMouseEvent, BrowserParent* aRemoteTarget, BrowserParent* aStopAncestor, -@@ -2227,7 +2246,7 @@ void EventStateManager::DispatchCrossProcessEvent(WidgetEvent* aEvent, +@@ -2233,7 +2252,7 @@ void EventStateManager::DispatchCrossProcessEvent(WidgetEvent* aEvent, if (mouseEvent->mReason == WidgetMouseEvent::eReal && remote != oldRemote) { MOZ_ASSERT(mouseEvent->mMessage != eMouseExitFromWidget); @@ -1048,15 +1068,15 @@ index 180065668131acf3738117c84b6a11117fcb6979..a4a77ca8e006fb298769270bb3342ff4 auto& args = mArgs.as(); mFetchDriver->SetWorkerScript(args.mWorkerScript); diff --git a/dom/geolocation/Geolocation.cpp b/dom/geolocation/Geolocation.cpp -index 87b78f8d13a1a118eedb032d8feaf7d371e13844..f768f0549659263bc714b6339f9df859a1e33f9b 100644 +index 51efcf97c062e106575b88232a14f1702470fddf..1c68b997e4efd8896ec182e962c9e085ab24e7c7 100644 --- a/dom/geolocation/Geolocation.cpp +++ b/dom/geolocation/Geolocation.cpp -@@ -120,8 +120,12 @@ class nsGeolocationRequest final : public ContentPermissionRequestBase, +@@ -103,8 +103,12 @@ class nsGeolocationRequest final : public ContentPermissionRequestBase, NS_IMETHOD GetIgnoreAllowSitePermission( bool* aIgnoreAllowSitePermission) override { -+ RefPtr gs = -+ nsGeolocationService::GetGeolocationService( ++ RefPtr gs = ++ GeolocationService::GetGeolocationService( + mLocator->GetBrowsingContext()); *aIgnoreAllowSitePermission = - mBehavior != geolocation::SystemGeolocationPermissionBehavior::NoPrompt; @@ -1065,99 +1085,30 @@ index 87b78f8d13a1a118eedb032d8feaf7d371e13844..f768f0549659263bc714b6339f9df859 return NS_OK; } -@@ -410,7 +414,9 @@ nsGeolocationRequest::Allow(JS::Handle aChoices) { +@@ -393,7 +397,11 @@ nsGeolocationRequest::Allow(JS::Handle aChoices) { self->Cancel(); }; - if (mBehavior != SystemGeolocationPermissionBehavior::NoPrompt) { -+ RefPtr gs = nsGeolocationService::GetGeolocationService( -+ mLocator->GetBrowsingContext()); -+ if (mBehavior != SystemGeolocationPermissionBehavior::NoPrompt && !gs->IsOverride()) { ++ RefPtr gs = ++ GeolocationService::GetGeolocationService( ++ mLocator->GetBrowsingContext()); ++ if (mBehavior != SystemGeolocationPermissionBehavior::NoPrompt && ++ !gs->IsOverride()) { // Asynchronously present the system dialog or open system preferences // (RequestGeolocationPermissionFromUser will know which to do), and wait // for the permission to change or the request to be canceled. If the -@@ -434,8 +440,6 @@ nsGeolocationRequest::Allow(JS::Handle aChoices) { +@@ -417,8 +425,6 @@ nsGeolocationRequest::Allow(JS::Handle aChoices) { return NS_OK; } -- RefPtr gs = nsGeolocationService::GetGeolocationService( -- mLocator->GetBrowsingContext()); +- RefPtr gs = +- GeolocationService::GetGeolocationService(mLocator->GetBrowsingContext()); bool canUseCache = false; CachedPositionAndAccuracy lastPosition = gs->GetCachedPosition(); if (lastPosition.position) { -@@ -722,11 +726,16 @@ NS_INTERFACE_MAP_END - NS_IMPL_ADDREF(nsGeolocationService) - NS_IMPL_RELEASE(nsGeolocationService) - --nsresult nsGeolocationService::Init() { -+nsresult nsGeolocationService::Init(bool isOverride) { - if (!StaticPrefs::geo_enabled()) { - return NS_ERROR_FAILURE; - } - -+ if (isOverride) { -+ mIsOverride = true; -+ mHigherAccuracy = true; -+ } -+ - if (XRE_IsContentProcess()) { - return NS_OK; - } -@@ -805,6 +814,10 @@ nsresult nsGeolocationService::Init() { - return NS_OK; - } - -+bool nsGeolocationService::IsOverride() { -+ return mIsOverride; -+} -+ - nsGeolocationService::~nsGeolocationService() = default; - - NS_IMETHODIMP -@@ -948,6 +961,10 @@ bool nsGeolocationService::HighAccuracyRequested() { - } - - void nsGeolocationService::UpdateAccuracy(bool aForceHigh) { -+ if (mIsOverride) { -+ return; -+ } -+ - bool highRequired = aForceHigh || HighAccuracyRequested(); - - if (XRE_IsContentProcess()) { -diff --git a/dom/geolocation/Geolocation.h b/dom/geolocation/Geolocation.h -index 0e1d90608d55db6a3d39c730d40efa2e708a4c4a..5d49528d5925b4f9ab55531efd694f355bf5a42d 100644 ---- a/dom/geolocation/Geolocation.h -+++ b/dom/geolocation/Geolocation.h -@@ -63,7 +63,7 @@ class nsGeolocationService final : public nsIGeolocationUpdate, - - nsGeolocationService() = default; - -- nsresult Init(); -+ nsresult Init(bool isOverride = false); - - // Management of the Geolocation objects - void AddLocator(mozilla::dom::Geolocation* aLocator); -@@ -88,6 +88,8 @@ class nsGeolocationService final : public nsIGeolocationUpdate, - void UpdateAccuracy(bool aForceHigh = false); - bool HighAccuracyRequested(); - -+ bool IsOverride(); -+ - private: - ~nsGeolocationService(); - -@@ -114,6 +116,8 @@ class nsGeolocationService final : public nsIGeolocationUpdate, - // Nothing() if not being started, or a boolean reflecting the requested - // accuracy. - mozilla::Maybe mStarting; -+ -+ bool mIsOverride = false; - }; - - namespace mozilla::dom { diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp -index 45fa91a01ee24014c33396b4e057a797c72377b2..5e44eea99d06d2688e196ba1eb62eb1766b3cb70 100644 +index 1e697e9ced01c1ac64348fcf375821b405ad3e2a..a23d9ac19eabe70affbf79835d3612cdcfdb6727 100644 --- a/dom/html/HTMLInputElement.cpp +++ b/dom/html/HTMLInputElement.cpp @@ -60,6 +60,7 @@ @@ -1477,10 +1428,10 @@ index 584d39da5f04f6d8fc6a87547557b0eeeb35d168..65eb7014356a90aa01ab517c9937ed65 * returned quads are further translated relative to the window * origin -- which is not the layout origin. Further translation diff --git a/dom/webidl/Window.webidl b/dom/webidl/Window.webidl -index ae2d14e105bc70dc4a5c7f1fa3481c550a74dd4d..83f090b0512dbf2fde42e152838d4bd8315e6616 100644 +index 87efe769a8e2d2f8d3502957127de50aed1e9971..b8545e4c788bbee7b1d53efff081289831abd1cf 100644 --- a/dom/webidl/Window.webidl +++ b/dom/webidl/Window.webidl -@@ -442,6 +442,8 @@ dictionary SynthesizeMouseEventOptions : SynthesizeEventOptions { +@@ -444,6 +444,8 @@ dictionary SynthesizeMouseEventOptions : SynthesizeEventOptions { boolean ignoreRootScrollFrame = false; // Controls WidgetMouseEvent.mReason value. boolean isWidgetEventSynthesized = false; @@ -1490,10 +1441,10 @@ index ae2d14e105bc70dc4a5c7f1fa3481c550a74dd4d..83f090b0512dbf2fde42e152838d4bd8 // Mozilla-specific stuff diff --git a/js/src/debugger/Object.cpp b/js/src/debugger/Object.cpp -index 57cd28a445025fb9f3a18ce7083ee683069015f0..398b82832bbf373d443b846b7b5cd4d10888d2a3 100644 +index 29e128ec723f557292f8ed0a2fc338503002f238..270ee326e169889ef5bb42dff4c3ac6eda6f0f01 100644 --- a/js/src/debugger/Object.cpp +++ b/js/src/debugger/Object.cpp -@@ -2510,7 +2510,11 @@ Maybe DebuggerObject::call(JSContext* cx, +@@ -2623,7 +2623,11 @@ Maybe DebuggerObject::call(JSContext* cx, invokeArgs[i].set(args2[i]); } @@ -1506,10 +1457,10 @@ index 57cd28a445025fb9f3a18ce7083ee683069015f0..398b82832bbf373d443b846b7b5cd4d1 } diff --git a/js/src/vm/DateTime.cpp b/js/src/vm/DateTime.cpp -index 50657afb4aaf2305b01b581ee09eb9ea712b2614..38384eea6216358b40e8440fa2a78a17fca83592 100644 +index 51d1d54ee7ed08739242913a607eac8d20655213..f5f9642ef3b4fe5c7c9b01ed88bb2666a1e20605 100644 --- a/js/src/vm/DateTime.cpp +++ b/js/src/vm/DateTime.cpp -@@ -810,7 +810,6 @@ void js::DateTimeInfo::internalResyncICUDefaultTimeZone() { +@@ -809,7 +809,6 @@ void js::DateTimeInfo::internalResyncICUDefaultTimeZone() { #if JS_HAS_INTL_API if (const char* tzenv = std::getenv("TZ")) { std::string_view tz(tzenv); @@ -1569,10 +1520,10 @@ index eaaf69687f669a4859a37f3f97b99e6ca519c420..6f965a110db3a1c86466d72f09ead23b // No boxes to return return; diff --git a/layout/base/PresShell.cpp b/layout/base/PresShell.cpp -index 5b52c35994ce5167fc07b31ed9adb9b2ecc1df32..906f8c6e7a7736eed6ba149fd4a142b53522dbc9 100644 +index 0be20c4ea8b91424a98659d991038b1e5c7f1e4a..5f497118bf3c2e4e7405d82732b49acadce45a8a 100644 --- a/layout/base/PresShell.cpp +++ b/layout/base/PresShell.cpp -@@ -11835,7 +11835,9 @@ bool PresShell::ComputeActiveness() const { +@@ -11948,7 +11948,9 @@ bool PresShell::ComputeActiveness() const { if (!browserChild->IsVisible()) { MOZ_LOG(gLog, LogLevel::Debug, (" > BrowserChild %p is not visible", browserChild)); @@ -1584,10 +1535,10 @@ index 5b52c35994ce5167fc07b31ed9adb9b2ecc1df32..906f8c6e7a7736eed6ba149fd4a142b5 // If the browser is visible but just due to be preserving layers diff --git a/layout/base/nsLayoutUtils.cpp b/layout/base/nsLayoutUtils.cpp -index aa6ef4435df02bad54e195607c5da02bd7ed315a..c7a3f02bf30704c53a3c6a619b2dfbee02edb0c2 100644 +index 22f2b63b9dc1a59bb6ca23f152faf42aa164f79c..17b22da2cce054b793780096fbfa10bb65b8afe8 100644 --- a/layout/base/nsLayoutUtils.cpp +++ b/layout/base/nsLayoutUtils.cpp -@@ -702,6 +702,7 @@ bool nsLayoutUtils::AllowZoomingForDocument( +@@ -701,6 +701,7 @@ bool nsLayoutUtils::AllowZoomingForDocument(const Document* aDocument) { !aDocument->GetPresShell()->AsyncPanZoomEnabled()) { return false; } @@ -1596,10 +1547,10 @@ index aa6ef4435df02bad54e195607c5da02bd7ed315a..c7a3f02bf30704c53a3c6a619b2dfbee // in RDM. BrowsingContext* bc = aDocument->GetBrowsingContext(); diff --git a/layout/style/GeckoBindings.h b/layout/style/GeckoBindings.h -index b8317e006298c1345a6f13d26ca20980875e5bd1..001fd4dbe5612588857924b902cf7ccb359885e5 100644 +index 574c2683cadf77b468ca9a4250bc7bc1fbb5c8f3..30c7a89a6ed08f526089a8ab40124a980fac3089 100644 --- a/layout/style/GeckoBindings.h +++ b/layout/style/GeckoBindings.h -@@ -623,6 +623,7 @@ bool Gecko_MediaFeatures_PrefersReducedMotion(const mozilla::dom::Document*); +@@ -607,6 +607,7 @@ bool Gecko_MediaFeatures_PrefersReducedMotion(const mozilla::dom::Document*); bool Gecko_MediaFeatures_PrefersReducedTransparency( const mozilla::dom::Document*); bool Gecko_MediaFeatures_MacRTL(const mozilla::dom::Document*); @@ -1633,10 +1584,10 @@ index 1f9613b8cd936fa9d884be010a8dd6167251faa6..0346bccc1c469446376c5bee3250e3dc return StylePrefersContrast::NoPreference; } diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml -index 3d33f9916a240af4ef5b0028b23e139dc50e9851..358b8365235dad23c08ad26d7d30cde3fd9b2bc8 100644 +index 1e0f2266c6ae3b00b44499031325a6689479069e..3e7e870cbc1884eedb41c776a9b46ab55f9a3f07 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml -@@ -13315,18 +13315,20 @@ +@@ -13496,18 +13496,20 @@ # Use the libwebrtc ScreenCaptureKit desktop capture backend on Mac by default. # When disabled, or on a host where not supported (< macOS 14), the older # CoreGraphics backend is used instead. @@ -1660,10 +1611,10 @@ index 3d33f9916a240af4ef5b0028b23e139dc50e9851..358b8365235dad23c08ad26d7d30cde3 # Use the libwebrtc ScreenCaptureKit desktop capture backend on Mac for screen diff --git a/netwerk/base/LoadInfo.cpp b/netwerk/base/LoadInfo.cpp -index a468108e7147e98f1037e751cb25bc5743a276c3..6b9fab4b6a07cdb9a54719bd7007e32368c485f6 100644 +index b37ab1f707c00065f4c7a688c3bf590bbfcd815b..cb888c7663a603744610a02e3e8cd4d680623c12 100644 --- a/netwerk/base/LoadInfo.cpp +++ b/netwerk/base/LoadInfo.cpp -@@ -752,7 +752,8 @@ LoadInfo::LoadInfo(const LoadInfo& rhs) +@@ -753,7 +753,8 @@ LoadInfo::LoadInfo(const LoadInfo& rhs) mInterceptionInfo(rhs.mInterceptionInfo), mSchemelessInput(rhs.mSchemelessInput), mUserNavigationInvolvement(rhs.mUserNavigationInvolvement), @@ -1673,7 +1624,7 @@ index a468108e7147e98f1037e751cb25bc5743a276c3..6b9fab4b6a07cdb9a54719bd7007e323 } LoadInfo::LoadInfo( -@@ -2116,4 +2117,16 @@ void LoadInfo::UpdateParentAddressSpaceInfo() { +@@ -2117,4 +2118,16 @@ void LoadInfo::UpdateParentAddressSpaceInfo() { } } @@ -1691,10 +1642,10 @@ index a468108e7147e98f1037e751cb25bc5743a276c3..6b9fab4b6a07cdb9a54719bd7007e323 + } // namespace mozilla::net diff --git a/netwerk/base/LoadInfo.h b/netwerk/base/LoadInfo.h -index 797517e695981ddddfef253cd5eb227fd08e686d..93927bb84e4a81068b1772a913c4b405605cc063 100644 +index 837efb44f66e2cbe39e563a44a2807e55bab9312..97bd9e9be52510192e260580a6b5638af97a8d5b 100644 --- a/netwerk/base/LoadInfo.h +++ b/netwerk/base/LoadInfo.h -@@ -550,6 +550,8 @@ class LoadInfo final : public nsILoadInfo { +@@ -542,6 +542,8 @@ class LoadInfo final : public nsILoadInfo { dom::UserNavigationInvolvement::None; bool mSkipHTTPSUpgrade = false; @@ -1704,10 +1655,10 @@ index 797517e695981ddddfef253cd5eb227fd08e686d..93927bb84e4a81068b1772a913c4b405 // This is exposed solely for testing purposes and should not be used outside of // LoadInfo diff --git a/netwerk/base/TRRLoadInfo.cpp b/netwerk/base/TRRLoadInfo.cpp -index 8acc7468ae67a75c53569c8d9498b0b27d6bcaa2..5cf23f8a3906f923c0368e8cf5b8fc99df4858f5 100644 +index 9a4cf0648708633b6b826312b262df9d3b6c163f..574aceb5700b47325ef9ceafeab647b2d166a609 100644 --- a/netwerk/base/TRRLoadInfo.cpp +++ b/netwerk/base/TRRLoadInfo.cpp -@@ -555,5 +555,15 @@ TRRLoadInfo::GetFetchDestination(nsACString& aDestination) { +@@ -556,5 +556,15 @@ TRRLoadInfo::GetFetchDestination(nsACString& aDestination) { return NS_ERROR_NOT_IMPLEMENTED; } @@ -1724,10 +1675,10 @@ index 8acc7468ae67a75c53569c8d9498b0b27d6bcaa2..5cf23f8a3906f923c0368e8cf5b8fc99 } // namespace net } // namespace mozilla diff --git a/netwerk/base/nsILoadInfo.idl b/netwerk/base/nsILoadInfo.idl -index f6a22a498d99ee75c7c9985e278b4331ec3eddc4..cbc8f92fabc7272a2f04b0e37214e7a51a9a0dda 100644 +index f5e892979d77d608fb499f963872076f7d122f76..f48a0d48d880453bb882d97114482dba2bf6bdbf 100644 --- a/netwerk/base/nsILoadInfo.idl +++ b/netwerk/base/nsILoadInfo.idl -@@ -1720,4 +1720,6 @@ interface nsILoadInfo : nsISupports +@@ -1704,4 +1704,6 @@ interface nsILoadInfo : nsISupports return static_cast(userNavigationInvolvement); } %} @@ -1756,10 +1707,10 @@ index 3654f3ed20f6b22d36c4238be40417e77e8f6867..f685e7668ad3310cac8bc8425124a6fe * Set the status and reason for the forthcoming synthesized response. * Multiple calls overwrite existing values. diff --git a/netwerk/ipc/DocumentLoadListener.cpp b/netwerk/ipc/DocumentLoadListener.cpp -index 1df2115be38c8e4c71635581f486c0efb0a48d2d..6a8e39d7b3504b05b463ba218b038455ff52bf2a 100644 +index 090ea90b4dbfcb47b3029142ceb9aaae04df3374..f0d161f4519841a85b38a34bb2c4c6fce893f4cc 100644 --- a/netwerk/ipc/DocumentLoadListener.cpp +++ b/netwerk/ipc/DocumentLoadListener.cpp -@@ -196,6 +196,7 @@ static auto CreateDocumentLoadInfo(CanonicalBrowsingContext* aBrowsingContext, +@@ -205,6 +205,7 @@ static auto CreateDocumentLoadInfo(CanonicalBrowsingContext* aBrowsingContext, aLoadState->GetTextDirectiveUserActivation() || aLoadState->HasLoadFlags(nsIWebNavigation::LOAD_FLAGS_FROM_EXTERNAL)); loadInfo->SetIsMetaRefresh(aLoadState->IsMetaRefresh()); @@ -1768,10 +1719,10 @@ index 1df2115be38c8e4c71635581f486c0efb0a48d2d..6a8e39d7b3504b05b463ba218b038455 return loadInfo.forget(); } diff --git a/netwerk/protocol/http/InterceptedHttpChannel.cpp b/netwerk/protocol/http/InterceptedHttpChannel.cpp -index 60626cc9b3f7495b3f0ffc13346d1319ffd86dab..99c85a483d9e325bc3f7fd86bf772968ece31724 100644 +index f094d87b789e0a9e1984aaf30718bff64f24ef94..d1f7fdaff06e62a7e96bd5c9dc6ea39d2ea593a5 100644 --- a/netwerk/protocol/http/InterceptedHttpChannel.cpp +++ b/netwerk/protocol/http/InterceptedHttpChannel.cpp -@@ -726,10 +726,33 @@ NS_IMPL_ISUPPORTS(ResetInterceptionHeaderVisitor, nsIHttpHeaderVisitor) +@@ -727,10 +727,33 @@ NS_IMPL_ISUPPORTS(ResetInterceptionHeaderVisitor, nsIHttpHeaderVisitor) } // anonymous namespace @@ -1805,7 +1756,7 @@ index 60626cc9b3f7495b3f0ffc13346d1319ffd86dab..99c85a483d9e325bc3f7fd86bf772968 if (mCanceled) { return mStatus; } -@@ -1143,11 +1166,18 @@ InterceptedHttpChannel::OnStartRequest(nsIRequest* aRequest) { +@@ -1148,11 +1171,18 @@ InterceptedHttpChannel::OnStartRequest(nsIRequest* aRequest) { GetCallback(mProgressSink); } @@ -1825,7 +1776,7 @@ index 60626cc9b3f7495b3f0ffc13346d1319ffd86dab..99c85a483d9e325bc3f7fd86bf772968 if (mPump && mLoadFlags & LOAD_CALL_CONTENT_SNIFFERS) { RefPtr pump(mPump); diff --git a/netwerk/protocol/http/InterceptedHttpChannel.h b/netwerk/protocol/http/InterceptedHttpChannel.h -index 430646881b927d2dddda1e0bcf5fd3427580224f..6b5bbb2a411794e9275d1ab83ea02d4cfca88e0e 100644 +index ab440756a6745ce3c4785f211db32173c6bd27c5..2ba8db70059b5143e72063ffb588c55dd2734c62 100644 --- a/netwerk/protocol/http/InterceptedHttpChannel.h +++ b/netwerk/protocol/http/InterceptedHttpChannel.h @@ -89,6 +89,11 @@ class InterceptedHttpChannel final @@ -1841,7 +1792,7 @@ index 430646881b927d2dddda1e0bcf5fd3427580224f..6b5bbb2a411794e9275d1ab83ea02d4c * InterceptionTimeStamps is used to record the time stamps of the * interception. diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp -index c6d926448e399e0cb3032180efad3cddaf785fc0..7148ba816bf0dab3224323736001237a99d3b2ba 100644 +index 0941cf9dc3933cd0c0e85647fd9531e18a3bcffb..10298f22fe713638b1c96f587b7d7be844f276e4 100644 --- a/netwerk/protocol/http/nsHttpChannel.cpp +++ b/netwerk/protocol/http/nsHttpChannel.cpp @@ -942,11 +942,9 @@ nsresult nsHttpChannel::OnBeforeConnect() { @@ -1868,7 +1819,7 @@ index c6d926448e399e0cb3032180efad3cddaf785fc0..7148ba816bf0dab3224323736001237a if (mURI->SchemeIs("https") || aShouldUpgrade || !LoadUseHTTPSSVC() || forceOffline) { -@@ -1531,15 +1527,14 @@ nsresult nsHttpChannel::ContinueConnect() { +@@ -1541,15 +1537,14 @@ nsresult nsHttpChannel::ContinueConnect() { "CORS preflight must have been finished by the time we " "do the rest of ContinueConnect"); @@ -1886,7 +1837,7 @@ index c6d926448e399e0cb3032180efad3cddaf785fc0..7148ba816bf0dab3224323736001237a BYPASS_LOCAL_CACHE(mLoadFlags, LoadPreferCacheLoadOverBypass())) { return NS_ERROR_OFFLINE; } -@@ -1581,7 +1576,7 @@ nsresult nsHttpChannel::ContinueConnect() { +@@ -1591,7 +1586,7 @@ nsresult nsHttpChannel::ContinueConnect() { } // We're about to hit the network. Don't if we're forced offline. @@ -1895,7 +1846,7 @@ index c6d926448e399e0cb3032180efad3cddaf785fc0..7148ba816bf0dab3224323736001237a return NS_ERROR_OFFLINE; } -@@ -1689,12 +1684,9 @@ void nsHttpChannel::SpeculativeConnect() { +@@ -1699,12 +1694,9 @@ void nsHttpChannel::SpeculativeConnect() { // don't speculate if we are offline, when doing http upgrade (i.e. // websockets bootstrap), or if we can't do keep-alive (because then we // couldn't reuse the speculative connection anyhow). @@ -1909,7 +1860,7 @@ index c6d926448e399e0cb3032180efad3cddaf785fc0..7148ba816bf0dab3224323736001237a return; } -@@ -5007,7 +4999,7 @@ nsresult nsHttpChannel::OpenCacheEntryInternal(bool isHttps) { +@@ -5060,7 +5052,7 @@ nsresult nsHttpChannel::OpenCacheEntryInternal(bool isHttps) { return NS_OK; } @@ -1918,7 +1869,7 @@ index c6d926448e399e0cb3032180efad3cddaf785fc0..7148ba816bf0dab3224323736001237a if (offline || (mLoadFlags & INHIBIT_CACHING) || forceOffline) { if (BYPASS_LOCAL_CACHE(mLoadFlags, LoadPreferCacheLoadOverBypass()) && !offline && !forceOffline) { -@@ -8306,6 +8298,20 @@ void nsHttpChannel::MaybeStartDNSPrefetch() { +@@ -8534,6 +8526,20 @@ void nsHttpChannel::MaybeStartDNSPrefetch() { } } @@ -1940,10 +1891,10 @@ index c6d926448e399e0cb3032180efad3cddaf785fc0..7148ba816bf0dab3224323736001237a nsHttpChannel::GetEncodedBodySize(uint64_t* aEncodedBodySize) { if (mCacheEntry && !LoadCacheEntryIsWriteOnly()) { diff --git a/netwerk/protocol/http/nsHttpChannel.h b/netwerk/protocol/http/nsHttpChannel.h -index 602a3def6eab873986afc17ec92073eb476a43fb..4b267ff4859ebc3eecb169b7f793e60ac8ba1728 100644 +index cb5da1a630eb49624078262917d34146c6367444..b826374a2d0dc2814b2fd85efaed941ec63edb54 100644 --- a/netwerk/protocol/http/nsHttpChannel.h +++ b/netwerk/protocol/http/nsHttpChannel.h -@@ -306,6 +306,10 @@ class nsHttpChannel final : public HttpBaseChannel, +@@ -317,6 +317,10 @@ class nsHttpChannel final : public HttpBaseChannel, void MaybeResolveProxyAndBeginConnect(); void MaybeStartDNSPrefetch(); @@ -1955,10 +1906,10 @@ index 602a3def6eab873986afc17ec92073eb476a43fb..4b267ff4859ebc3eecb169b7f793e60a // end server host name. nsIHttpChannelInternal::ProxyDNSStrategy ComputeProxyDNSStrategy(); diff --git a/parser/html/nsHtml5TreeOpExecutor.cpp b/parser/html/nsHtml5TreeOpExecutor.cpp -index ed63fe936c1c0fa38329c19c7adada56fd756f2d..b492aa20dd7ded36472b3816ea181d9a991b8d6f 100644 +index 8c376eb269cc152bebf4ddb24fee9bda26d4bac8..e52a4768d92c254f07196c903ee9355f464c91b1 100644 --- a/parser/html/nsHtml5TreeOpExecutor.cpp +++ b/parser/html/nsHtml5TreeOpExecutor.cpp -@@ -1449,6 +1449,10 @@ void nsHtml5TreeOpExecutor::UpdateReferrerInfoFromMeta( +@@ -1450,6 +1450,10 @@ void nsHtml5TreeOpExecutor::UpdateReferrerInfoFromMeta( void nsHtml5TreeOpExecutor::AddSpeculationCSP(const nsAString& aCSP) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); @@ -1970,10 +1921,10 @@ index ed63fe936c1c0fa38329c19c7adada56fd756f2d..b492aa20dd7ded36472b3816ea181d9a nsCOMPtr preloadCsp = mDocument->GetPreloadCsp(); if (!preloadCsp) { diff --git a/security/manager/ssl/nsCertOverrideService.cpp b/security/manager/ssl/nsCertOverrideService.cpp -index 068d9702fd015c974d698d2c8c741b5d2b0d8316..9ff49d6de9d712b7e5ab9711da7f22269dc2fe49 100644 +index 79a5989f6949505878cfbee21e6609cf2cdbaf37..fb2c56c9bb8047468773c421d3ccca37b873c035 100644 --- a/security/manager/ssl/nsCertOverrideService.cpp +++ b/security/manager/ssl/nsCertOverrideService.cpp -@@ -624,6 +624,8 @@ void nsCertOverrideService::CountPermanentOverrideTelemetry( +@@ -615,6 +615,8 @@ void nsCertOverrideService::CountPermanentOverrideTelemetry( } static bool IsDebugger() { @@ -1983,10 +1934,10 @@ index 068d9702fd015c974d698d2c8c741b5d2b0d8316..9ff49d6de9d712b7e5ab9711da7f2226 nsCOMPtr marionette = do_GetService(NS_MARIONETTE_CONTRACTID); if (marionette) { diff --git a/services/settings/Utils.sys.mjs b/services/settings/Utils.sys.mjs -index 76f69ab74eba9f704b95a7e3c7b27c39a887bf5a..cf0b9223baed50bff6a08497fe571ca153efaeb0 100644 +index 40e919a997a5e112ec9a83aa72680451d4739367..e0d91833af2392adc236f069a60808615ffb8287 100644 --- a/services/settings/Utils.sys.mjs +++ b/services/settings/Utils.sys.mjs -@@ -99,7 +99,7 @@ const _cdnURLs = {}; +@@ -105,7 +105,7 @@ function _isUndefined(value) { export var Utils = { get SERVER_URL() { @@ -1995,7 +1946,7 @@ index 76f69ab74eba9f704b95a7e3c7b27c39a887bf5a..cf0b9223baed50bff6a08497fe571ca1 ? // eslint-disable-next-line mozilla/valid-lazy lazy.gServerURL : AppConstants.REMOTE_SETTINGS_SERVER_URLS[0]; -@@ -113,6 +113,9 @@ export var Utils = { +@@ -119,6 +119,9 @@ export var Utils = { log, get shouldSkipRemoteActivity() { @@ -2006,35 +1957,37 @@ index 76f69ab74eba9f704b95a7e3c7b27c39a887bf5a..cf0b9223baed50bff6a08497fe571ca1 (lazy.isRunningTests || Cu.isInAutomation) && this.SERVER_URL == "data:,#remote-settings-dummy/v1" diff --git a/toolkit/components/browser/nsIWebBrowserChrome.idl b/toolkit/components/browser/nsIWebBrowserChrome.idl -index a665bd039d49aeeb6896e224080a7cc00b0eacbc..099c3b08d7de697cc8edcd5bb1d8000fc28aeefb 100644 +index 11f7f8a614bd9dc99ec23dec1d7b45527331bec0..e222d1be610db5e00ca7380c4f35259caaced091 100644 --- a/toolkit/components/browser/nsIWebBrowserChrome.idl +++ b/toolkit/components/browser/nsIWebBrowserChrome.idl -@@ -87,6 +87,9 @@ interface nsIWebBrowserChrome : nsISupports - // Whether this is a Document Picture-in-Picture window - const unsigned long CHROME_DOCUMENT_PIP = 1 << 22; +@@ -99,7 +99,10 @@ interface nsIWebBrowserChrome : nsISupports + // ignored for Linux. + const unsigned long CHROME_SUPPRESS_ANIMATION = 1 << 24; +- // Two bits are free here. + // Whether this window has "width" or "height" defined in features -+ const unsigned long JUGGLER_WINDOW_EXPLICIT_SIZE = 1 << 23; ++ const unsigned long JUGGLER_WINDOW_EXPLICIT_SIZE = 1 << 26; + - // Prevents new window animations on MacOS and Windows. Currently - // ignored for Linux. - const unsigned long CHROME_SUPPRESS_ANIMATION = 1 << 24; ++ // One bit is free here. + + const unsigned long CHROME_CENTER_SCREEN = 1 << 27; + diff --git a/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs b/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs -index 217d51f4c765e0aac2107246f7142099d21d984e..1b623f956c178904945cddeceec9f737a01c053f 100644 +index 7c5bde49c469a6645e4694bc523dab24302694ba..812311051343beb44883e48d8670545c5beb8d71 100644 --- a/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs +++ b/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs -@@ -115,7 +115,9 @@ EnterprisePoliciesManager.prototype = { +@@ -113,7 +113,9 @@ EnterprisePoliciesManager.prototype = { Services.prefs.clearUserPref(PREF_POLICIES_APPLIED); } -- let provider = this._chooseProvider(); +- let provider = this._buildProvider(); + // --- Playwright begin --- + let provider = new PlaywrightPoliciesProvider(); + // --- Playwright end --- if (provider.failed) { this.status = Ci.nsIEnterprisePolicies.FAILED; -@@ -736,6 +738,19 @@ class JSONPoliciesProvider { +@@ -760,6 +762,19 @@ class JSONPoliciesProvider extends PoliciesProvider { } } @@ -2051,11 +2004,74 @@ index 217d51f4c765e0aac2107246f7142099d21d984e..1b623f956c178904945cddeceec9f737 + } +} + - class WindowsGPOPoliciesProvider { + class WindowsGPOPoliciesProvider extends PoliciesProvider { constructor() { - this._policies = null; + super(); +diff --git a/toolkit/components/geolocation/GeolocationService.cpp b/toolkit/components/geolocation/GeolocationService.cpp +index 70af7ec0c23833766fdee173c1bcf35448e3af85..1e587ebb60f4591402566f6749426d296d0992b9 100644 +--- a/toolkit/components/geolocation/GeolocationService.cpp ++++ b/toolkit/components/geolocation/GeolocationService.cpp +@@ -52,11 +52,16 @@ NS_INTERFACE_MAP_END + NS_IMPL_ADDREF(GeolocationService) + NS_IMPL_RELEASE(GeolocationService) + +-nsresult GeolocationService::Init() { ++nsresult GeolocationService::Init(bool aIsOverride) { + if (!StaticPrefs::geo_enabled()) { + return NS_ERROR_FAILURE; + } + ++ if (aIsOverride) { ++ mIsOverride = true; ++ mHigherAccuracy = true; ++ } ++ + if (XRE_IsContentProcess()) { + return NS_OK; + } +@@ -277,6 +282,10 @@ bool GeolocationService::HighAccuracyRequested() { + } + + void GeolocationService::UpdateAccuracy(bool aForceHigh) { ++ if (mIsOverride) { ++ return; ++ } ++ + bool highRequired = aForceHigh || HighAccuracyRequested(); + + if (XRE_IsContentProcess()) { +diff --git a/toolkit/components/geolocation/GeolocationService.h b/toolkit/components/geolocation/GeolocationService.h +index ab90326ccabc727b72ac3b2b43ee354ef18e20a2..17ff228f0037687cf68c3c55ab56ab2b3d686d0c 100644 +--- a/toolkit/components/geolocation/GeolocationService.h ++++ b/toolkit/components/geolocation/GeolocationService.h +@@ -42,7 +42,7 @@ class GeolocationService final : public nsIGeolocationService, + + GeolocationService() = default; + +- nsresult Init(); ++ nsresult Init(bool aIsOverride = false); + + // Management of the Geolocation objects + void AddLocator(mozilla::dom::Geolocation* aLocator); +@@ -66,6 +66,7 @@ class GeolocationService final : public nsIGeolocationService, + // Update the accuracy and notify the provider if changed + void UpdateAccuracy(bool aForceHigh = false); + bool HighAccuracyRequested(); ++ bool IsOverride() const { return mIsOverride; } + + private: + ~GeolocationService(); +@@ -93,6 +94,8 @@ class GeolocationService final : public nsIGeolocationService, + // Nothing() if not being started, or a boolean reflecting the requested + // accuracy. + mozilla::Maybe mStarting; ++ ++ bool mIsOverride = false; + }; + + } // namespace mozilla diff --git a/toolkit/components/startup/nsAppStartup.cpp b/toolkit/components/startup/nsAppStartup.cpp -index d0f9c88dcdaca07c6e10139b50b092bf80fc8c3e..f6d1a5ff8f241abdd931f47508d0fffaadce0391 100644 +index b0f1e285ad6b89cf72c8a98fb564d474f5601c4e..52a080d8c16c8531aadf8714b30a31ef60537c46 100644 --- a/toolkit/components/startup/nsAppStartup.cpp +++ b/toolkit/components/startup/nsAppStartup.cpp @@ -377,7 +377,7 @@ nsAppStartup::Quit(uint32_t aMode, int aExitCode, bool* aUserAllowedQuit) { @@ -2083,24 +2099,28 @@ index efe8ff5541915c0fc632e75572d1e3968c60139d..e0157515ded811e47d21705f13f475d6 int32_t aMaxSelfProgress, int32_t aCurTotalProgress, diff --git a/toolkit/components/windowwatcher/nsWindowWatcher.cpp b/toolkit/components/windowwatcher/nsWindowWatcher.cpp -index d5ebdf4413568c89e93d89279510ae910bcdf9fd..a1dbe7916e39fd6bc0292d6262787d39a5c8015d 100644 +index d493fc20e19390e94e5609556f1c6878ee3f66d5..1df133fa362d6d1eb3cb35780403150138626aed 100644 --- a/toolkit/components/windowwatcher/nsWindowWatcher.cpp +++ b/toolkit/components/windowwatcher/nsWindowWatcher.cpp -@@ -1917,7 +1917,11 @@ uint32_t nsWindowWatcher::CalculateChromeFlagsForContent( - // behavior of other browsers and avoids breaking sites like Gmail that - // open a Compose popout via Shift+click. - *aIsPopupRequested = true; -- return nsIWebBrowserChrome::CHROME_MINIMAL_POPUP; -+ uint32_t chromeFlags = 0; +@@ -1902,8 +1902,14 @@ uint32_t nsWindowWatcher::CalculateChromeFlagsForContent( + return nsIWebBrowserChrome::CHROME_DOCUMENT_PICTURE_IN_PICTURE_FLAGS; + } + *aIsPopupRequested = ShouldOpenPopup(aFeatures); +- return *aIsPopupRequested ? nsIWebBrowserChrome::CHROME_MINIMAL_POPUP +- : nsIWebBrowserChrome::CHROME_ALL; ++ if (!*aIsPopupRequested) { ++ return nsIWebBrowserChrome::CHROME_ALL; ++ } ++ uint32_t chromeFlags = nsIWebBrowserChrome::CHROME_MINIMAL_POPUP; + if (aFeatures.Exists("width") || aFeatures.Exists("height")) { + chromeFlags |= nsIWebBrowserChrome::JUGGLER_WINDOW_EXPLICIT_SIZE; + } -+ return chromeFlags | nsIWebBrowserChrome::CHROME_MINIMAL_POPUP; ++ return chromeFlags; } /** diff --git a/toolkit/mozapps/update/UpdateService.sys.mjs b/toolkit/mozapps/update/UpdateService.sys.mjs -index db167b74f040cdb477f68c87c4dfe50ef1173c32..78690b35f701df5a683ea3e392b2f7757f66c73e 100644 +index 266f3ea153d578c20a8d96b3e0a0a319baab7468..9a2e98c5deb895fcacdd68bb1eaba39da22c4303 100644 --- a/toolkit/mozapps/update/UpdateService.sys.mjs +++ b/toolkit/mozapps/update/UpdateService.sys.mjs @@ -4024,6 +4024,8 @@ export class UpdateService { @@ -2177,10 +2197,10 @@ index 524451a83e03f8a9a83103b1f3d87850ad411515..a44b3c5da20f6a0bf9c892d4289a07ac // nsDocumentViewer::LoadComplete that doesn't do various things // that are not relevant here because this wasn't an actual diff --git a/uriloader/exthandler/nsExternalHelperAppService.cpp b/uriloader/exthandler/nsExternalHelperAppService.cpp -index 1eab401455890aafc73f1da9080415fc035f1c30..d7ce1bccb117cac5d95181f6ab0ca7c3e7e62808 100644 +index 566509f9ef17ac0a1d9830a5315b751266db6aee..fc659b901334b0ff63ad244bcbff779415bd3813 100644 --- a/uriloader/exthandler/nsExternalHelperAppService.cpp +++ b/uriloader/exthandler/nsExternalHelperAppService.cpp -@@ -111,6 +111,7 @@ +@@ -115,6 +115,7 @@ #include "mozilla/Components.h" #include "mozilla/ClearOnShutdown.h" @@ -2188,7 +2208,7 @@ index 1eab401455890aafc73f1da9080415fc035f1c30..d7ce1bccb117cac5d95181f6ab0ca7c3 #include "mozilla/Preferences.h" #include "mozilla/ipc/URIUtils.h" -@@ -879,6 +880,12 @@ NS_IMETHODIMP nsExternalHelperAppService::ApplyDecodingForExtension( +@@ -890,6 +891,12 @@ NS_IMETHODIMP nsExternalHelperAppService::ApplyDecodingForExtension( return NS_OK; } @@ -2201,7 +2221,7 @@ index 1eab401455890aafc73f1da9080415fc035f1c30..d7ce1bccb117cac5d95181f6ab0ca7c3 nsresult nsExternalHelperAppService::GetFileTokenForPath( const char16_t* aPlatformAppPath, nsIFile** aFile) { nsDependentString platformAppPath(aPlatformAppPath); -@@ -1514,7 +1521,12 @@ nsresult nsExternalAppHandler::SetUpTempFile(nsIChannel* aChannel) { +@@ -1568,7 +1575,12 @@ nsresult nsExternalAppHandler::SetUpTempFile(nsIChannel* aChannel) { // Strip off the ".part" from mTempLeafName mTempLeafName.Truncate(mTempLeafName.Length() - std::size(".part") + 1); @@ -2214,7 +2234,7 @@ index 1eab401455890aafc73f1da9080415fc035f1c30..d7ce1bccb117cac5d95181f6ab0ca7c3 mSaver = do_CreateInstance(NS_BACKGROUNDFILESAVERSTREAMLISTENER_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); -@@ -1698,7 +1710,36 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { +@@ -1752,7 +1764,36 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { return NS_OK; } @@ -2252,7 +2272,7 @@ index 1eab401455890aafc73f1da9080415fc035f1c30..d7ce1bccb117cac5d95181f6ab0ca7c3 if (NS_FAILED(rv)) { nsresult transferError = rv; -@@ -1760,6 +1801,9 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { +@@ -1814,6 +1855,9 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { bool alwaysAsk = true; mMimeInfo->GetAlwaysAskBeforeHandling(&alwaysAsk); @@ -2262,7 +2282,7 @@ index 1eab401455890aafc73f1da9080415fc035f1c30..d7ce1bccb117cac5d95181f6ab0ca7c3 if (alwaysAsk) { // But we *don't* ask if this mimeInfo didn't come from // our user configuration datastore and the user has said -@@ -2276,6 +2320,15 @@ nsExternalAppHandler::OnSaveComplete(nsIBackgroundFileSaver* aSaver, +@@ -2330,6 +2374,15 @@ nsExternalAppHandler::OnSaveComplete(nsIBackgroundFileSaver* aSaver, NotifyTransfer(aStatus); } @@ -2278,7 +2298,7 @@ index 1eab401455890aafc73f1da9080415fc035f1c30..d7ce1bccb117cac5d95181f6ab0ca7c3 return NS_OK; } -@@ -2761,6 +2814,14 @@ NS_IMETHODIMP nsExternalAppHandler::Cancel(nsresult aReason) { +@@ -2815,6 +2868,14 @@ NS_IMETHODIMP nsExternalAppHandler::Cancel(nsresult aReason) { } } @@ -2294,10 +2314,10 @@ index 1eab401455890aafc73f1da9080415fc035f1c30..d7ce1bccb117cac5d95181f6ab0ca7c3 // OnStartRequest) mDialog = nullptr; diff --git a/uriloader/exthandler/nsExternalHelperAppService.h b/uriloader/exthandler/nsExternalHelperAppService.h -index 3f8586ed7ee54af197a82b7a69651b4be2f84dad..929fe5b2b58ceef4055415cfc36a6698b73cba4c 100644 +index 477afd0a414b35af6b4acc13fa0321d826ae7d65..f2941eff522c0a06bb99759da375637c1196dfec 100644 --- a/uriloader/exthandler/nsExternalHelperAppService.h +++ b/uriloader/exthandler/nsExternalHelperAppService.h -@@ -269,6 +269,8 @@ class nsExternalHelperAppService : public nsIExternalHelperAppService, +@@ -302,6 +302,8 @@ class nsExternalHelperAppService : public nsIExternalHelperAppService, mozilla::dom::BrowsingContext* aContentContext, bool aForceSave, nsIInterfaceRequestor* aWindowContext, nsIStreamListener** aStreamListener); @@ -2306,7 +2326,7 @@ index 3f8586ed7ee54af197a82b7a69651b4be2f84dad..929fe5b2b58ceef4055415cfc36a6698 }; /** -@@ -467,6 +469,9 @@ class nsExternalAppHandler final : public nsIStreamListener, +@@ -500,6 +502,9 @@ class nsExternalAppHandler final : public nsIStreamListener, * Upon successful return, both mTempFile and mSaver will be valid. */ nsresult SetUpTempFile(nsIChannel* aChannel); @@ -2390,10 +2410,10 @@ index 777157e17e0db442262b1a9522b0b1b39058789a..54c4dde2ee4847e79b07fffe3ec57a34 } #endif diff --git a/widget/cocoa/NativeKeyBindings.mm b/widget/cocoa/NativeKeyBindings.mm -index 9711cfaf4e3e2974567ea7f7d4074b3541b0248f..34292be04676193f1b8efce45d4fa370c15e970f 100644 +index 4246b5ab669ecad8d2c24777c67470d1ece91d50..9b173d81b571d4aa25a57ca424e0ec6396ab5441 100644 --- a/widget/cocoa/NativeKeyBindings.mm +++ b/widget/cocoa/NativeKeyBindings.mm -@@ -618,6 +618,10 @@ +@@ -635,6 +635,10 @@ break; case KEY_NAME_INDEX_ArrowUp: if (aEvent.IsControl()) { @@ -2404,7 +2424,7 @@ index 9711cfaf4e3e2974567ea7f7d4074b3541b0248f..34292be04676193f1b8efce45d4fa370 break; } if (aEvent.IsMeta()) { -@@ -655,6 +659,10 @@ +@@ -672,6 +676,10 @@ break; case KEY_NAME_INDEX_ArrowDown: if (aEvent.IsControl()) { @@ -2416,7 +2436,7 @@ index 9711cfaf4e3e2974567ea7f7d4074b3541b0248f..34292be04676193f1b8efce45d4fa370 } if (aEvent.IsMeta()) { diff --git a/widget/headless/HeadlessCompositorWidget.cpp b/widget/headless/HeadlessCompositorWidget.cpp -index cbaa021c9d31cd57e253afd984c5076acab1941e..6cf72e6006e33eac7720958f7d0cd91983f2d782 100644 +index 8484851f674aa21d09e5a3ed77b9df4edaa89e12..2804831ec785a64c771946f095f855d9408851fe 100644 --- a/widget/headless/HeadlessCompositorWidget.cpp +++ b/widget/headless/HeadlessCompositorWidget.cpp @@ -2,6 +2,8 @@ @@ -2425,10 +2445,10 @@ index cbaa021c9d31cd57e253afd984c5076acab1941e..6cf72e6006e33eac7720958f7d0cd919 +#include "mozilla/gfx/2D.h" +#include "mozilla/layers/CompositorThread.h" - #include "mozilla/widget/PlatformWidgetTypes.h" #include "HeadlessCompositorWidget.h" + #include "VsyncDispatcher.h" -@@ -14,9 +16,30 @@ HeadlessCompositorWidget::HeadlessCompositorWidget( +@@ -15,9 +17,30 @@ HeadlessCompositorWidget::HeadlessCompositorWidget( const layers::CompositorOptions& aOptions, HeadlessWidget* aWindow) : CompositorWidget(aOptions), mWidget(aWindow), @@ -2459,7 +2479,7 @@ index cbaa021c9d31cd57e253afd984c5076acab1941e..6cf72e6006e33eac7720958f7d0cd919 void HeadlessCompositorWidget::ObserveVsync(VsyncObserver* aObserver) { if (RefPtr cvd = mWidget->GetCompositorVsyncDispatcher()) { -@@ -30,6 +53,59 @@ void HeadlessCompositorWidget::NotifyClientSizeChanged( +@@ -31,6 +54,59 @@ void HeadlessCompositorWidget::NotifyClientSizeChanged( const LayoutDeviceIntSize& aClientSize) { auto size = mClientSize.Lock(); *size = aClientSize; @@ -2520,7 +2540,7 @@ index cbaa021c9d31cd57e253afd984c5076acab1941e..6cf72e6006e33eac7720958f7d0cd919 LayoutDeviceIntSize HeadlessCompositorWidget::GetClientSize() { diff --git a/widget/headless/HeadlessCompositorWidget.h b/widget/headless/HeadlessCompositorWidget.h -index 8374cd00944eddad27563624cd33bb046b6cf143..0345d19d1da862ccd31afa49b123e9f16755c886 100644 +index f9454a79f3e3ff4d9013db99cf9bb98413633330..68e7b94c3a9c75dc9fceae0d5881971497e4fc7c 100644 --- a/widget/headless/HeadlessCompositorWidget.h +++ b/widget/headless/HeadlessCompositorWidget.h @@ -5,6 +5,7 @@ @@ -2528,10 +2548,10 @@ index 8374cd00944eddad27563624cd33bb046b6cf143..0345d19d1da862ccd31afa49b123e9f1 #define widget_headless_HeadlessCompositorWidget_h +#include "mozilla/ReentrantMonitor.h" + #include "HeadlessWidget.h" #include "mozilla/widget/CompositorWidget.h" - #include "HeadlessWidget.h" -@@ -22,8 +23,11 @@ class HeadlessCompositorWidget final : public CompositorWidget, +@@ -21,8 +22,11 @@ class HeadlessCompositorWidget final : public CompositorWidget, HeadlessWidget* aWindow); void NotifyClientSizeChanged(const LayoutDeviceIntSize& aClientSize); @@ -2543,7 +2563,7 @@ index 8374cd00944eddad27563624cd33bb046b6cf143..0345d19d1da862ccd31afa49b123e9f1 uintptr_t GetWidgetKey() override; -@@ -41,10 +45,18 @@ class HeadlessCompositorWidget final : public CompositorWidget, +@@ -40,10 +44,18 @@ class HeadlessCompositorWidget final : public CompositorWidget, } private: @@ -2563,7 +2583,7 @@ index 8374cd00944eddad27563624cd33bb046b6cf143..0345d19d1da862ccd31afa49b123e9f1 } // namespace widget diff --git a/widget/headless/HeadlessLookAndFeelGTK.cpp b/widget/headless/HeadlessLookAndFeelGTK.cpp -index 34ef4bf32bc4c348bd226f1c3ea5a4f03ad4fac1..394e892cdf35c351b9d8536dcb82c1a130cb2095 100644 +index d6e94f053c22d9ed5df36b1c20cd408c2605bdc5..31fdf23775544cf1ee9e89e8dd09bcc2166b067b 100644 --- a/widget/headless/HeadlessLookAndFeelGTK.cpp +++ b/widget/headless/HeadlessLookAndFeelGTK.cpp @@ -3,6 +3,7 @@ @@ -2588,10 +2608,10 @@ index 34ef4bf32bc4c348bd226f1c3ea5a4f03ad4fac1..394e892cdf35c351b9d8536dcb82c1a1 default: aResult = 0; diff --git a/widget/headless/HeadlessWidget.cpp b/widget/headless/HeadlessWidget.cpp -index 0841e299ea23692310db27acfa78f7e2050bb002..3285e16397b14400127c81c0d1a13711e536b073 100644 +index 0c28540ee6aca7f888ab505f6aeb80e61b9981d6..23458a37b6993996e43138bd0e547e9fc7df6614 100644 --- a/widget/headless/HeadlessWidget.cpp +++ b/widget/headless/HeadlessWidget.cpp -@@ -112,6 +112,8 @@ void HeadlessWidget::Destroy() { +@@ -113,6 +113,8 @@ void HeadlessWidget::Destroy() { } } @@ -2600,7 +2620,7 @@ index 0841e299ea23692310db27acfa78f7e2050bb002..3285e16397b14400127c81c0d1a13711 nsIWidget::OnDestroy(); nsIWidget::Destroy(); -@@ -573,5 +575,14 @@ nsresult HeadlessWidget::SynthesizeNativeTouchpadPan( +@@ -574,5 +576,14 @@ nsresult HeadlessWidget::SynthesizeNativeTouchpadPan( return NS_OK; } @@ -2616,7 +2636,7 @@ index 0841e299ea23692310db27acfa78f7e2050bb002..3285e16397b14400127c81c0d1a13711 } // namespace widget } // namespace mozilla diff --git a/widget/headless/HeadlessWidget.h b/widget/headless/HeadlessWidget.h -index 9daa3334e7ec6aee433ee7a28e6b86a8548f6226..0aa21a1b0a3b07548d832bfb41f73fe37a174fdc 100644 +index 05ae8d02e81e65ec22729173995968367fb026e7..236d2991b75fb613af90d699b5a8f7dcca99a7aa 100644 --- a/widget/headless/HeadlessWidget.h +++ b/widget/headless/HeadlessWidget.h @@ -127,6 +127,9 @@ class HeadlessWidget final : public nsIWidget { @@ -2630,10 +2650,10 @@ index 9daa3334e7ec6aee433ee7a28e6b86a8548f6226..0aa21a1b0a3b07548d832bfb41f73fe3 ~HeadlessWidget(); bool mEnabled; diff --git a/xpcom/reflect/xptinfo/xptinfo.h b/xpcom/reflect/xptinfo/xptinfo.h -index 2888ffaf432e16d67d348ff008372fbb96c06991..cd4cce73f69bee86d4fb7cb510cd60de0cbcd0b8 100644 +index 7b1d918e86af55b5f961d839ca5009335b56d88b..3d2bc9339173a7358137fc28f0441932061d82bc 100644 --- a/xpcom/reflect/xptinfo/xptinfo.h +++ b/xpcom/reflect/xptinfo/xptinfo.h -@@ -503,7 +503,7 @@ static_assert(sizeof(nsXPTMethodInfo) == 8, "wrong size"); +@@ -504,7 +504,7 @@ static_assert(sizeof(nsXPTMethodInfo) == 8, "wrong size"); #if defined(MOZ_THUNDERBIRD) || defined(MOZ_SUITE) # define PARAM_BUFFER_COUNT 18 #else diff --git a/browser_patches/firefox/preferences/playwright.cfg b/browser_patches/firefox/preferences/playwright.cfg index f6a3cd845c1f9..f635ae6c8f4dd 100644 --- a/browser_patches/firefox/preferences/playwright.cfg +++ b/browser_patches/firefox/preferences/playwright.cfg @@ -20,6 +20,11 @@ pref("dom.security.https_first", false); pref("datareporting.policy.dataSubmissionEnabled", false); pref("datareporting.policy.dataSubmissionPolicyAccepted", false); pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true); +// Do not show the "Terms of Use" first-run notification (Firefox 154+). +pref("termsofuse.bypassNotification", true); +// Do not show the pre-onboarding splash modal in the first window; it waits +// for Nimbus experiments to load and blocks all mouse input (Firefox 154+). +pref("browser.preonboarding.enabled", false); // Force pdfs into downloads. pref("pdfjs.disabled", true); @@ -148,7 +153,7 @@ pref("ui.use_standins_for_native_colors", true); // Turn off the Push service. pref("dom.push.serverURL", ""); // Prevent Remote Settings (firefox.settings.services.mozilla.com) to issue non local connections. -pref("services.settings.server", ""); +pref("services.settings.server", "data:,#remote-settings-dummy/v1"); // Prevent location.services.mozilla.com to issue non local connections. pref("browser.region.network.url", ""); pref("browser.pocket.enabled", false); diff --git a/browser_patches/webkit/UPSTREAM_CONFIG.sh b/browser_patches/webkit/UPSTREAM_CONFIG.sh index 949eaeae6398a..1b73874e1bc76 100644 --- a/browser_patches/webkit/UPSTREAM_CONFIG.sh +++ b/browser_patches/webkit/UPSTREAM_CONFIG.sh @@ -1,3 +1,3 @@ REMOTE_URL="https://github.com/WebKit/WebKit.git" BASE_BRANCH="main" -BASE_REVISION="343e13bf22dca9d0ec227801419aab0f9001a32f" +BASE_REVISION="4d05d732e5a84f32675bef4cc135a2e7a9269a87" diff --git a/browser_patches/webkit/embedder/Playwright/win/WinMain.cpp b/browser_patches/webkit/embedder/Playwright/win/WinMain.cpp index 8dde328f19415..585e1778b0a6d 100644 --- a/browser_patches/webkit/embedder/Playwright/win/WinMain.cpp +++ b/browser_patches/webkit/embedder/Playwright/win/WinMain.cpp @@ -40,6 +40,7 @@ #include #include "WebKitBrowserWindow.h" #include +#include #include SOFT_LINK_LIBRARY(user32); @@ -76,7 +77,6 @@ int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE); #endif - MSG msg { }; HACCEL hAccelTable, hPreAccelTable; INITCOMMONCONTROLSEX InitCtrlEx; @@ -143,17 +143,15 @@ int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, // Main message loop: __try { - while (GetMessage(&msg, nullptr, 0, 0)) { + RunLoop::setWindowsMessageHandler([hAccelTable, hPreAccelTable] (MSG& msg) { if (TranslateAccelerator(msg.hwnd, hPreAccelTable, &msg)) - continue; + return true; bool processed = false; if (MainWindow::isInstance(msg.hwnd)) processed = TranslateAccelerator(msg.hwnd, hAccelTable, &msg); - if (!processed) { - TranslateMessage(&msg); - DispatchMessage(&msg); - } - } + return processed; + }); + RunLoop::run(); } __except(createCrashReport(GetExceptionInformation()), EXCEPTION_EXECUTE_HANDLER) { } exit: @@ -164,5 +162,5 @@ int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, // Shut down COM. OleUninitialize(); - return static_cast(msg.wParam); + return 0; } diff --git a/browser_patches/webkit/patches/bootstrap.diff b/browser_patches/webkit/patches/bootstrap.diff index f6496e94a03d3..c6fb0a9e5f31b 100644 --- a/browser_patches/webkit/patches/bootstrap.diff +++ b/browser_patches/webkit/patches/bootstrap.diff @@ -1,8 +1,8 @@ diff --git a/Source/JavaScriptCore/CMakeLists.txt b/Source/JavaScriptCore/CMakeLists.txt -index c257f6a5a523ce12f67857e0d344aadffea6601a..f8c46e22eb4fa79c6b9f56846828391c3bf4de21 100644 +index 161c7b52986e742e8a503a1055defc973f1b4a5c..66a6a9e43c39f4098f4dcf0aaa06dc131d1ec775 100644 --- a/Source/JavaScriptCore/CMakeLists.txt +++ b/Source/JavaScriptCore/CMakeLists.txt -@@ -1615,21 +1615,26 @@ set(JavaScriptCore_INSPECTOR_DOMAINS +@@ -1687,21 +1687,26 @@ set(JavaScriptCore_INSPECTOR_DOMAINS ${JAVASCRIPTCORE_DIR}/inspector/protocol/CSS.json ${JAVASCRIPTCORE_DIR}/inspector/protocol/Canvas.json ${JAVASCRIPTCORE_DIR}/inspector/protocol/Console.json @@ -30,10 +30,10 @@ index c257f6a5a523ce12f67857e0d344aadffea6601a..f8c46e22eb4fa79c6b9f56846828391c ${JAVASCRIPTCORE_DIR}/inspector/protocol/ServiceWorker.json ${JAVASCRIPTCORE_DIR}/inspector/protocol/Target.json diff --git a/Source/JavaScriptCore/DerivedSources-input.xcfilelist b/Source/JavaScriptCore/DerivedSources-input.xcfilelist -index 55eacb0d378fddea15ece56db612655ac5d64061..cb6cfa7c542e82671f68984ff9373e1bf58da2b8 100644 +index 726fff0b42d9c68f6bc2264e80ffaba5ecdc9c19..e0467722a0a84a7bd97ee1cf83792e3b21dc21e1 100644 --- a/Source/JavaScriptCore/DerivedSources-input.xcfilelist +++ b/Source/JavaScriptCore/DerivedSources-input.xcfilelist -@@ -101,20 +101,25 @@ $(PROJECT_DIR)/inspector/protocol/CPUProfiler.json +@@ -100,20 +100,25 @@ $(PROJECT_DIR)/inspector/protocol/CPUProfiler.json $(PROJECT_DIR)/inspector/protocol/CSS.json $(PROJECT_DIR)/inspector/protocol/Canvas.json $(PROJECT_DIR)/inspector/protocol/Console.json @@ -60,10 +60,10 @@ index 55eacb0d378fddea15ece56db612655ac5d64061..cb6cfa7c542e82671f68984ff9373e1b $(PROJECT_DIR)/inspector/protocol/Security.json $(PROJECT_DIR)/inspector/protocol/ServiceWorker.json diff --git a/Source/JavaScriptCore/DerivedSources.make b/Source/JavaScriptCore/DerivedSources.make -index 7eb4f1c1e0cd3abdf61203d33cd2f53d77d4264d..01adf210b42055612dc02aba0ed52e6f6ed1d10f 100644 +index f578bae225764bc744094224bbb26135b65fbe95..6aa5de1e3e410dbd261c4eefd7d1b6939ede60bd 100644 --- a/Source/JavaScriptCore/DerivedSources.make +++ b/Source/JavaScriptCore/DerivedSources.make -@@ -298,21 +298,26 @@ INSPECTOR_DOMAINS := \ +@@ -294,21 +294,26 @@ INSPECTOR_DOMAINS := \ $(JavaScriptCore)/inspector/protocol/CSS.json \ $(JavaScriptCore)/inspector/protocol/Canvas.json \ $(JavaScriptCore)/inspector/protocol/Console.json \ @@ -146,7 +146,7 @@ index 4d9152423abf35dfbdf338435b567a822b2f6904..4e6b551494e1d124655c8cc54f121ac4 return nullptr; inspectorObject->setValue(name.string(), inspectorValue.releaseNonNull()); diff --git a/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.cpp b/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.cpp -index e20e61989609a48b3cdb0492f3c5801a3ada3319..9e91d3ca5c92e71b79ec58570c05380e16f410ef 100644 +index 5131f15fbcd16365883508bdb77160c97f0e359a..8862b68b20d9a5610d1a1f7792e68ca6e9b27e04 100644 --- a/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.cpp +++ b/Source/JavaScriptCore/inspector/InspectorBackendDispatcher.cpp @@ -104,7 +104,7 @@ void BackendDispatcher::registerDispatcherForDomain(const String& domain, Supple @@ -380,7 +380,7 @@ index 0b7d732f036fce96b01bd06f4c47f5d471a0ab38..a9efea4bec66aa68966c99d9e77f8a36 // FrontendChannel FrontendChannel::ConnectionType connectionType() const; diff --git a/Source/JavaScriptCore/inspector/protocol/DOM.json b/Source/JavaScriptCore/inspector/protocol/DOM.json -index 0b41f31605a2407fd068e28eaec60dbeabefd4d8..80fc7f1351949447c1f988641a26bcf695d589f2 100644 +index 6e8fdd27aca49bfc534ea3c3f960b419dbb0dc9f..eca174d4507a70ef4066babaa83c992009a00a88 100644 --- a/Source/JavaScriptCore/inspector/protocol/DOM.json +++ b/Source/JavaScriptCore/inspector/protocol/DOM.json @@ -80,6 +80,16 @@ @@ -837,10 +837,10 @@ index 0000000000000000000000000000000000000000..1c43b476603325fa412bcfded9163e7a + ] +} diff --git a/Source/JavaScriptCore/inspector/protocol/Network.json b/Source/JavaScriptCore/inspector/protocol/Network.json -index 29a3ef3294dcdf9e9559b665e9f1bb9a5727cf4b..b6b90f356dd6b7de7752ab4296069c8f07b4de38 100644 +index a535818daee8ec8bea89658d12a04109f2841567..26f19ac2846b72dd71b5defe877b93c47f72b23a 100644 --- a/Source/JavaScriptCore/inspector/protocol/Network.json +++ b/Source/JavaScriptCore/inspector/protocol/Network.json -@@ -360,6 +360,13 @@ +@@ -361,6 +361,13 @@ "parameters": [ { "name": "bytesPerSecondLimit", "type": "integer", "optional": true, "description": "Limits the bytes per second of requests if positive. Removes any limits if zero or not provided." } ] @@ -855,7 +855,7 @@ index 29a3ef3294dcdf9e9559b665e9f1bb9a5727cf4b..b6b90f356dd6b7de7752ab4296069c8f ], "events": [ diff --git a/Source/JavaScriptCore/inspector/protocol/Page.json b/Source/JavaScriptCore/inspector/protocol/Page.json -index 374b26c3ee9acf39aeb23fb60821b39d0afd971c..8843241e3e669c2d7537adb11e83325f216febba 100644 +index 8113d39cf01d78e6d5ba1e0c8bcbdea9b7ff667f..1cec5a9de917834f2671ed0000129bb1435403e7 100644 --- a/Source/JavaScriptCore/inspector/protocol/Page.json +++ b/Source/JavaScriptCore/inspector/protocol/Page.json @@ -20,7 +20,15 @@ @@ -875,7 +875,19 @@ index 374b26c3ee9acf39aeb23fb60821b39d0afd971c..8843241e3e669c2d7537adb11e83325f ] }, { -@@ -62,6 +70,12 @@ +@@ -56,12 +64,24 @@ + "enum": ["Viewport", "Page"], + "description": "Coordinate system used by supplied coordinates." + }, ++ { ++ "id": "ImageFormat", ++ "type": "string", ++ "enum": ["png", "jpeg", "webp"], ++ "description": "Image format used to encode a captured snapshot." ++ }, + { + "id": "CookieSameSitePolicy", + "type": "string", "enum": ["None", "Lax", "Strict"], "description": "Same-Site policy of a cookie." }, @@ -888,7 +900,7 @@ index 374b26c3ee9acf39aeb23fb60821b39d0afd971c..8843241e3e669c2d7537adb11e83325f { "id": "Frame", "type": "object", -@@ -126,6 +140,16 @@ +@@ -126,6 +146,16 @@ { "name": "sameSite", "$ref": "CookieSameSitePolicy", "description": "Cookie Same-Site policy." }, { "name": "partitionKey", "type": "string", "optional": true, "description": "Cookie partition key. If null and partitioned property is true, then key must be computed." } ] @@ -905,7 +917,7 @@ index 374b26c3ee9acf39aeb23fb60821b39d0afd971c..8843241e3e669c2d7537adb11e83325f } ], "commands": [ -@@ -145,6 +169,14 @@ +@@ -145,6 +175,14 @@ { "name": "revalidateAllResources", "type": "boolean", "optional": true, "description": "If true, all cached subresources will be revalidated when the main resource loads. Otherwise, only expired cached subresources will be revalidated (the default behavior for most WebKit clients)." } ] }, @@ -920,7 +932,7 @@ index 374b26c3ee9acf39aeb23fb60821b39d0afd971c..8843241e3e669c2d7537adb11e83325f { "name": "overrideUserAgent", "description": "Override's the user agent of the inspected page", -@@ -153,6 +185,14 @@ +@@ -153,6 +191,14 @@ { "name": "value", "type": "string", "optional": true, "description": "Value to override the user agent with. If this value is not provided, the override is removed. Overrides are removed when Web Inspector closes/disconnects." } ] }, @@ -935,7 +947,7 @@ index 374b26c3ee9acf39aeb23fb60821b39d0afd971c..8843241e3e669c2d7537adb11e83325f { "name": "overrideSetting", "description": "Allows the frontend to override the inspected page's settings.", -@@ -277,6 +317,28 @@ +@@ -277,6 +323,28 @@ { "name": "media", "type": "string", "description": "Media type to emulate. Empty string disables the override." } ] }, @@ -964,17 +976,23 @@ index 374b26c3ee9acf39aeb23fb60821b39d0afd971c..8843241e3e669c2d7537adb11e83325f { "name": "snapshotNode", "description": "Capture a snapshot of the specified node that does not include unrelated layers.", -@@ -297,7 +359,8 @@ +@@ -297,10 +365,13 @@ { "name": "y", "type": "integer", "description": "Y coordinate" }, { "name": "width", "type": "integer", "description": "Rectangle width" }, { "name": "height", "type": "integer", "description": "Rectangle height" }, - { "name": "coordinateSystem", "$ref": "CoordinateSystem", "description": "Indicates the coordinate system of the supplied rectangle." } + { "name": "coordinateSystem", "$ref": "CoordinateSystem", "description": "Indicates the coordinate system of the supplied rectangle." }, -+ { "name": "omitDeviceScaleFactor", "type": "boolean", "optional": true, "description": "By default, screenshot is inflated by device scale factor to avoid blurry image. This flag disables it." } ++ { "name": "omitDeviceScaleFactor", "type": "boolean", "optional": true, "description": "By default, screenshot is inflated by device scale factor to avoid blurry image. This flag disables it." }, ++ { "name": "format", "$ref": "ImageFormat", "optional": true, "description": "Image format of the resulting snapshot. Defaults to \"png\"." }, ++ { "name": "quality", "type": "integer", "optional": true, "description": "Compression quality from 0 to 100 (ignored for the \"png\" format). Defaults to 80." } ], "returns": [ - { "name": "dataURL", "type": "string", "description": "Base64-encoded image data (PNG)." } -@@ -315,12 +378,54 @@ +- { "name": "dataURL", "type": "string", "description": "Base64-encoded image data (PNG)." } ++ { "name": "dataURL", "type": "string", "description": "Base64-encoded image data." } + ] + }, + { +@@ -315,12 +386,54 @@ { "name": "setScreenSizeOverride", "description": "Overrides screen size exposed to DOM and used in media queries for testing with provided values.", @@ -1030,7 +1048,7 @@ index 374b26c3ee9acf39aeb23fb60821b39d0afd971c..8843241e3e669c2d7537adb11e83325f } ], "events": [ -@@ -328,14 +433,16 @@ +@@ -328,14 +441,16 @@ "name": "domContentEventFired", "targetTypes": ["page"], "parameters": [ @@ -1049,7 +1067,7 @@ index 374b26c3ee9acf39aeb23fb60821b39d0afd971c..8843241e3e669c2d7537adb11e83325f ] }, { -@@ -345,6 +452,14 @@ +@@ -345,6 +460,14 @@ { "name": "frame", "$ref": "Frame", "description": "Frame object." } ] }, @@ -1064,7 +1082,7 @@ index 374b26c3ee9acf39aeb23fb60821b39d0afd971c..8843241e3e669c2d7537adb11e83325f { "name": "frameDetached", "description": "Fired when frame has been detached from its parent.", -@@ -353,6 +468,22 @@ +@@ -353,6 +476,22 @@ { "name": "frameId", "$ref": "Network.FrameId", "description": "Id of the frame that has been detached." } ] }, @@ -1087,7 +1105,7 @@ index 374b26c3ee9acf39aeb23fb60821b39d0afd971c..8843241e3e669c2d7537adb11e83325f { "name": "defaultUserPreferencesDidChange", "description": "Fired when the default value of a user preference changes at the system level.", -@@ -360,6 +491,42 @@ +@@ -360,6 +499,42 @@ "parameters": [ { "name": "preferences", "type": "array", "items": { "$ref": "UserPreference" }, "description": "List of user preferences that can be overriden and their new system (default) values." } ] @@ -1613,10 +1631,10 @@ index 607a4b0f761b0aadd3ca8b83e080519ad6f97715..609f5b23e6ebd2629a1313673d9d8ea5 WEBKIT_ADD_TARGET_CXX_FLAGS(Skia diff --git a/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml b/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml -index 991cbe56d742a6cdd31d5a142754675272d4abd8..ce7e004a533d87765976b46e1d538799d2226af5 100644 +index aa2cc398647a1ded38c509a379922ce6ed6017cc..106efd19dfcbcabe1c702afe3b5bb8c179422f42 100644 --- a/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml +++ b/Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml -@@ -605,6 +605,7 @@ ApplePayEnabled: +@@ -619,6 +619,7 @@ ApplePayEnabled: richJavaScript: true # FIXME: This is on by default in WebKit2 PLATFORM(COCOA). Perhaps we should consider turning it on for WebKitLegacy as well. @@ -1624,7 +1642,7 @@ index 991cbe56d742a6cdd31d5a142754675272d4abd8..ce7e004a533d87765976b46e1d538799 AsyncClipboardAPIEnabled: type: bool status: mature -@@ -615,7 +616,7 @@ AsyncClipboardAPIEnabled: +@@ -629,7 +630,7 @@ AsyncClipboardAPIEnabled: default: false WebKit: "PLATFORM(COCOA) || PLATFORM(GTK) || PLATFORM(WPE)" : true @@ -1633,7 +1651,7 @@ index 991cbe56d742a6cdd31d5a142754675272d4abd8..ce7e004a533d87765976b46e1d538799 WebCore: default: false -@@ -871,13 +872,10 @@ BlobFileAccessEnforcementEnabled: +@@ -885,13 +886,10 @@ BlobFileAccessEnforcementEnabled: sharedPreferenceForWebProcess: true defaultValue: WebKitLegacy: @@ -1646,8 +1664,8 @@ index 991cbe56d742a6cdd31d5a142754675272d4abd8..ce7e004a533d87765976b46e1d538799 - "PLATFORM(COCOA)": true default: false - BlockIOKitInWebContentSandbox: -@@ -2073,6 +2071,7 @@ CrossOriginEmbedderPolicyEnabled: + BlockMediaLayerRehostingInWebContentProcess: +@@ -2116,6 +2114,7 @@ CrossOriginEmbedderPolicyEnabled: WebCore: default: false @@ -1655,7 +1673,7 @@ index 991cbe56d742a6cdd31d5a142754675272d4abd8..ce7e004a533d87765976b46e1d538799 CrossOriginOpenerPolicyEnabled: type: bool status: stable -@@ -2146,6 +2145,7 @@ DOMAudioSessionFullEnabled: +@@ -2189,6 +2188,7 @@ DOMAudioSessionFullEnabled: WebCore: default: false @@ -1663,7 +1681,7 @@ index 991cbe56d742a6cdd31d5a142754675272d4abd8..ce7e004a533d87765976b46e1d538799 DOMPasteAccessRequestsEnabled: type: bool status: internal -@@ -2157,7 +2157,7 @@ DOMPasteAccessRequestsEnabled: +@@ -2200,7 +2200,7 @@ DOMPasteAccessRequestsEnabled: default: false WebKit: "PLATFORM(IOS) || PLATFORM(MAC) || PLATFORM(GTK) || PLATFORM(WPE) || PLATFORM(VISION)": true @@ -1672,7 +1690,7 @@ index 991cbe56d742a6cdd31d5a142754675272d4abd8..ce7e004a533d87765976b46e1d538799 WebCore: default: false -@@ -2223,10 +2223,10 @@ DataListElementEnabled: +@@ -2281,10 +2281,10 @@ DataListElementEnabled: WebKitLegacy: default: false WebKit: @@ -1685,7 +1703,7 @@ index 991cbe56d742a6cdd31d5a142754675272d4abd8..ce7e004a533d87765976b46e1d538799 default: false sharedPreferenceForWebProcess: true -@@ -2239,7 +2239,7 @@ DataTransferItemsEnabled: +@@ -2297,7 +2297,7 @@ DataTransferItemsEnabled: WebKitLegacy: default: true WebKit: @@ -1694,7 +1712,7 @@ index 991cbe56d742a6cdd31d5a142754675272d4abd8..ce7e004a533d87765976b46e1d538799 default: false WebCore: default: false -@@ -2482,7 +2482,7 @@ DirectoryUploadEnabled: +@@ -2540,7 +2540,7 @@ DirectoryUploadEnabled: WebKitLegacy: default: false WebKit: @@ -1703,7 +1721,7 @@ index 991cbe56d742a6cdd31d5a142754675272d4abd8..ce7e004a533d87765976b46e1d538799 default: false WebCore: default: false -@@ -3188,10 +3188,10 @@ FullScreenEnabled: +@@ -3262,10 +3262,10 @@ FullScreenEnabled: WebKitLegacy: default: false WebKit: @@ -1716,7 +1734,7 @@ index 991cbe56d742a6cdd31d5a142754675272d4abd8..ce7e004a533d87765976b46e1d538799 default: false sharedPreferenceForWebProcess: true -@@ -3503,7 +3503,7 @@ HardwareAccelerationEnabled: +@@ -3606,7 +3606,7 @@ HardwareAccelerationEnabled: status: internal humanReadableName: "Hardware acceleration" humanReadableDescription: "Enable hardware acceleration" @@ -1725,7 +1743,7 @@ index 991cbe56d742a6cdd31d5a142754675272d4abd8..ce7e004a533d87765976b46e1d538799 defaultValue: WebKitLegacy: default: true -@@ -3910,7 +3910,7 @@ InputTypeColorEnabled: +@@ -4044,7 +4044,7 @@ InputTypeColorEnabled: WebKitLegacy: default: false WebKit: @@ -1734,7 +1752,7 @@ index 991cbe56d742a6cdd31d5a142754675272d4abd8..ce7e004a533d87765976b46e1d538799 default: false WebCore: default: false -@@ -3943,7 +3943,7 @@ InputTypeDateEnabled: +@@ -4077,7 +4077,7 @@ InputTypeDateEnabled: "PLATFORM(IOS_FAMILY)": true default: false WebKit: @@ -1743,7 +1761,7 @@ index 991cbe56d742a6cdd31d5a142754675272d4abd8..ce7e004a533d87765976b46e1d538799 default: false WebCore: default: false -@@ -3959,7 +3959,7 @@ InputTypeDateTimeLocalEnabled: +@@ -4093,7 +4093,7 @@ InputTypeDateTimeLocalEnabled: "PLATFORM(IOS_FAMILY)": true default: false WebKit: @@ -1752,7 +1770,7 @@ index 991cbe56d742a6cdd31d5a142754675272d4abd8..ce7e004a533d87765976b46e1d538799 default: false WebCore: default: false -@@ -3991,7 +3991,7 @@ InputTypeTimeEnabled: +@@ -4125,7 +4125,7 @@ InputTypeTimeEnabled: "PLATFORM(IOS_FAMILY)": true default: false WebKit: @@ -1761,7 +1779,7 @@ index 991cbe56d742a6cdd31d5a142754675272d4abd8..ce7e004a533d87765976b46e1d538799 default: false WebCore: default: false -@@ -4052,6 +4052,7 @@ InspectorMaximumResourcesContentSize: +@@ -4186,6 +4186,7 @@ InspectorMaximumResourcesContentSize: "PLATFORM(WPE)": 50 default: 200 @@ -1769,7 +1787,7 @@ index 991cbe56d742a6cdd31d5a142754675272d4abd8..ce7e004a533d87765976b46e1d538799 InspectorStartsAttached: type: bool status: embedder -@@ -4059,7 +4060,7 @@ InspectorStartsAttached: +@@ -4193,7 +4194,7 @@ InspectorStartsAttached: exposed: [ WebKit ] defaultValue: WebKit: @@ -1778,7 +1796,28 @@ index 991cbe56d742a6cdd31d5a142754675272d4abd8..ce7e004a533d87765976b46e1d538799 InspectorSupportsShowingCertificate: type: bool -@@ -6266,7 +6267,7 @@ PointerLockEnabled: +@@ -5830,6 +5831,7 @@ MuteCameraOnMicrophoneInterruptionEnabled: + WebCore: + default: false + ++# Playwright: also enable on Windows to align with other platforms. + NavigationAPIEnabled: + type: bool + status: stable +@@ -5840,10 +5842,10 @@ NavigationAPIEnabled: + WebKitLegacy: + default: false + WebKit: +- "PLATFORM(COCOA) || PLATFORM(GTK) || PLATFORM(WPE)": true ++ "PLATFORM(COCOA) || PLATFORM(GTK) || PLATFORM(WPE) || PLATFORM(WIN)": true + default: false + WebCore: +- "PLATFORM(COCOA) || PLATFORM(GTK) || PLATFORM(WPE)": true ++ "PLATFORM(COCOA) || PLATFORM(GTK) || PLATFORM(WPE) || PLATFORM(WIN)": true + default: false + + NavigatorUserAgentDataJavaScriptAPIEnabled: +@@ -6428,7 +6430,7 @@ PointerLockEnabled: "PLATFORM(IOS_FAMILY)": false default: true WebCore: @@ -1787,7 +1826,7 @@ index 991cbe56d742a6cdd31d5a142754675272d4abd8..ce7e004a533d87765976b46e1d538799 PopoverAttributeEnabled: type: bool -@@ -6896,7 +6897,7 @@ ScreenOrientationAPIEnabled: +@@ -7058,7 +7060,7 @@ ScreenOrientationAPIEnabled: WebKitLegacy: default: false WebKit: @@ -1797,10 +1836,10 @@ index 991cbe56d742a6cdd31d5a142754675272d4abd8..ce7e004a533d87765976b46e1d538799 default: false sharedPreferenceForWebProcess: true diff --git a/Source/WTF/wtf/PlatformEnable.h b/Source/WTF/wtf/PlatformEnable.h -index 61ba5001a3813448c611289b1fba181bc97d6f80..b85131232297784d8db7e7399da8f09504fd2352 100644 +index e6257d7388b973f803175cac7fd1dfa8aa4a8951..20a65d120f0c2f78b3580bc4b6709e992a9da615 100644 --- a/Source/WTF/wtf/PlatformEnable.h +++ b/Source/WTF/wtf/PlatformEnable.h -@@ -455,7 +455,7 @@ +@@ -467,7 +467,7 @@ // ORIENTATION_EVENTS should never get enabled on Desktop, only Mobile. #if !defined(ENABLE_ORIENTATION_EVENTS) @@ -1809,7 +1848,7 @@ index 61ba5001a3813448c611289b1fba181bc97d6f80..b85131232297784d8db7e7399da8f095 #endif #if OS(WINDOWS) -@@ -568,7 +568,7 @@ +@@ -580,7 +580,7 @@ #endif #if !defined(ENABLE_TOUCH_EVENTS) @@ -1819,10 +1858,10 @@ index 61ba5001a3813448c611289b1fba181bc97d6f80..b85131232297784d8db7e7399da8f095 #if !defined(ENABLE_CSS_TAP_HIGHLIGHT_COLOR) && ENABLE(TOUCH_EVENTS) diff --git a/Source/WTF/wtf/PlatformEnableCocoa.h b/Source/WTF/wtf/PlatformEnableCocoa.h -index 2d9da141c5d8ac413d2ed0aa7a6295ae3d817aff..9c0c99590182a7c6d3bcc707a610d434efd645cd 100644 +index 80a068153aa38fb686ea117fe13d2e32f460cc0b..643c12615ced8ab7f97f893428d95806b6b457d7 100644 --- a/Source/WTF/wtf/PlatformEnableCocoa.h +++ b/Source/WTF/wtf/PlatformEnableCocoa.h -@@ -822,7 +822,7 @@ +@@ -832,7 +832,7 @@ #endif #if !defined(ENABLE_SEC_ITEM_SHIM) @@ -1832,10 +1871,10 @@ index 2d9da141c5d8ac413d2ed0aa7a6295ae3d817aff..9c0c99590182a7c6d3bcc707a610d434 #if !defined(ENABLE_SERVER_PRECONNECT) diff --git a/Source/WTF/wtf/StdLibExtras.h b/Source/WTF/wtf/StdLibExtras.h -index ebdcb2707223f582f378d83ccbf31aa115724047..cc2d26b056618c847d6e184f73fff1701d5b921d 100644 +index 0b20a1ae6f61a574689e77b606dabb2589b99ad4..cbd4a349a20e27a590323f458a7ce39c0c688c80 100644 --- a/Source/WTF/wtf/StdLibExtras.h +++ b/Source/WTF/wtf/StdLibExtras.h -@@ -1607,6 +1607,39 @@ template constexpr auto forward_like(U&& value) -> detai +@@ -1621,6 +1621,39 @@ template constexpr auto forward_like(U&& value) -> detai template constexpr auto forward_like_preserving_const(U&& value) -> detail::forward_like_preserving_const_impl { return static_cast>(value); } } // namespace WTF @@ -1892,7 +1931,7 @@ index 8f551b2abcca06a623c432ac52db75de16849e8e..92bbd90bd032b9a9290ec741e7b38f9e namespace Unicode { diff --git a/Source/WebCore/DerivedSources.make b/Source/WebCore/DerivedSources.make -index aeb32c84bdd2c3351a57268b0e8a62d925e603e4..3c53cbdcfe3d0c1826765e8b4c3b1d877300bb35 100644 +index ceaf5b2c47117bfcaa93cf247af87c17de6f81cc..5e59987eb86ba02ad3089ca96274ed325d3d2433 100644 --- a/Source/WebCore/DerivedSources.make +++ b/Source/WebCore/DerivedSources.make @@ -1260,6 +1260,10 @@ JS_BINDING_IDLS := \ @@ -1906,7 +1945,7 @@ index aeb32c84bdd2c3351a57268b0e8a62d925e603e4..3c53cbdcfe3d0c1826765e8b4c3b1d87 $(WebCore)/dom/Text.idl \ $(WebCore)/dom/TextDecoder.idl \ $(WebCore)/dom/TextDecoderStream.idl \ -@@ -1884,9 +1888,6 @@ JS_BINDING_IDLS := \ +@@ -1886,9 +1890,6 @@ JS_BINDING_IDLS := \ ADDITIONAL_BINDING_IDLS = \ DocumentTouch.idl \ GestureEvent.idl \ @@ -1968,10 +2007,10 @@ index de8ce576a7e7156460680e5ea11f10a84cda83ec..3b502fa4663dc528cb892d28e5217c4e [self sendSpeechEndIfNeeded]; diff --git a/Source/WebCore/PlatformWin.cmake b/Source/WebCore/PlatformWin.cmake -index 72b2846f2c82818fc9a64fd90b7cba0c0601e15f..22277ab6c3233f040852d9daf9becf7ba81d12ca 100644 +index f86ea769e299e1f892a777282ac863f4d37769c7..45c5ec2270555eea7b025c318945225f95ac8883 100644 --- a/Source/WebCore/PlatformWin.cmake +++ b/Source/WebCore/PlatformWin.cmake -@@ -217,6 +217,7 @@ if (USE_CAIRO) +@@ -218,6 +218,7 @@ if (USE_CAIRO) platform/graphics/win/cairo/MediaPlayerPrivateMediaFoundationCairo.cpp platform/win/cairo/DragImageWinCairo.cpp @@ -1980,13 +2019,13 @@ index 72b2846f2c82818fc9a64fd90b7cba0c0601e15f..22277ab6c3233f040852d9daf9becf7b elseif (USE_SKIA) list(APPEND WebCore_SOURCES diff --git a/Source/WebCore/SourcesCocoa.txt b/Source/WebCore/SourcesCocoa.txt -index f3882f2579d2053b7ef773d1e786809d8756adaf..a2f7ed7c8172111432a119711bbe858c761d20d8 100644 +index e368c8b033bcf2d8c897d39a4f9d7af45309dbab..5aefd845f429f9a864e0470a3b443a2afdda8c85 100644 --- a/Source/WebCore/SourcesCocoa.txt +++ b/Source/WebCore/SourcesCocoa.txt -@@ -746,3 +746,9 @@ testing/cocoa/WebViewVisualIdentificationOverlay.mm @nonARC - platform/graphics/angle/GraphicsContextGLANGLE.cpp @no-unify - platform/graphics/cocoa/GraphicsContextGLCocoa.mm @nonARC @no-unify - platform/graphics/cv/GraphicsContextGLCVCocoa.mm @nonARC @no-unify +@@ -748,3 +748,9 @@ testing/cocoa/WebViewVisualIdentificationOverlay.mm @nonARC + platform/graphics/angle/GraphicsContextGLANGLE.cpp @no-unify // ANGLE headers redefine GL types/macros that collide with system OpenGL. + platform/graphics/cocoa/GraphicsContextGLCocoa.mm @nonARC @no-unify // ANGLE headers redefine GL types/macros that collide with system OpenGL. + platform/graphics/cv/GraphicsContextGLCVCocoa.mm @nonARC @no-unify // ANGLE headers redefine GL types/macros that collide with system OpenGL. + +// Playwright begin +JSTouch.cpp @@ -1994,10 +2033,10 @@ index f3882f2579d2053b7ef773d1e786809d8756adaf..a2f7ed7c8172111432a119711bbe858c +JSTouchList.cpp +// Playwright end diff --git a/Source/WebCore/SourcesGTK.txt b/Source/WebCore/SourcesGTK.txt -index 8c4a4c5e75fc792adb0c0801b3e81fd220df777c..99da4150c59018176142f34ebfccd4053c55fc49 100644 +index 30fc9d0f848c5d6d2a60a545ac8507e174837ed6..4427441fa947324fc67512ebfffb770068afb89b 100644 --- a/Source/WebCore/SourcesGTK.txt +++ b/Source/WebCore/SourcesGTK.txt -@@ -107,3 +107,10 @@ platform/unix/LoggingUnix.cpp +@@ -110,3 +110,10 @@ platform/unix/LoggingUnix.cpp platform/unix/SharedMemoryUnix.cpp platform/xdg/MIMETypeRegistryXdg.cpp @@ -2009,10 +2048,10 @@ index 8c4a4c5e75fc792adb0c0801b3e81fd220df777c..99da4150c59018176142f34ebfccd405 +JSSpeechSynthesisEventInit.cpp +// Playwright: end. diff --git a/Source/WebCore/SourcesWPE.txt b/Source/WebCore/SourcesWPE.txt -index eb48da502311408b4772385e51b4143da28fc5d0..0cbc5941fa1c659f5a76f77cc4b22ee5842e77af 100644 +index 39f5f71efa4e15ee6a07709bb21aa18c1a9c8389..16ec8acbdaf6d1a465280f592c7fb43edc2870eb 100644 --- a/Source/WebCore/SourcesWPE.txt +++ b/Source/WebCore/SourcesWPE.txt -@@ -114,3 +114,8 @@ platform/wpe/PasteboardWPE.cpp +@@ -119,3 +119,8 @@ platform/wpe/PasteboardWPE.cpp platform/wpe/PlatformScreenWPE.cpp platform/xdg/MIMETypeRegistryXdg.cpp @@ -2022,13 +2061,13 @@ index eb48da502311408b4772385e51b4143da28fc5d0..0cbc5941fa1c659f5a76f77cc4b22ee5 +JSSpeechSynthesisErrorEventInit.cpp +JSSpeechSynthesisEventInit.cpp diff --git a/Source/WebCore/WebCore.xcodeproj/project.pbxproj b/Source/WebCore/WebCore.xcodeproj/project.pbxproj -index a40df707691eec03783ba07d5102ec84ecda1911..e6f7e081d8e608d28e28a367c0cf731a4e649a58 100644 +index 073d4bbe83549ad1ed3f9e5056c66d4a3da15396..2a16a55fe8feefc28fe397946e26283825585f97 100644 --- a/Source/WebCore/WebCore.xcodeproj/project.pbxproj +++ b/Source/WebCore/WebCore.xcodeproj/project.pbxproj -@@ -7070,6 +7070,13 @@ - EE6C530F2F8831FF00C7B706 /* RenderTreeOrder.h in Headers */ = {isa = PBXBuildFile; fileRef = EE6C530D2F8830BD00C7B706 /* RenderTreeOrder.h */; settings = {ATTRIBUTES = (Private, ); }; }; - EEE349082DE0061C00A7D4BB /* StyleScopeIdentifier.h in Headers */ = {isa = PBXBuildFile; fileRef = EEE349072DE005FC00A7D4BB /* StyleScopeIdentifier.h */; settings = {ATTRIBUTES = (Private, ); }; }; - EFCC6C8F20FE914400A2321B /* CanvasActivityRecord.h in Headers */ = {isa = PBXBuildFile; fileRef = EFCC6C8D20FE914000A2321B /* CanvasActivityRecord.h */; settings = {ATTRIBUTES = (Private, ); }; }; +@@ -7076,6 +7076,13 @@ + E5F06AF724D4BB5600BBC4F8 /* DateTimeEditElement.h in Headers */ = {isa = PBXBuildFile; fileRef = E5F06AF524D4BB5600BBC4F8 /* DateTimeEditElement.h */; }; + E5F67D962B65CDBE00CC30DE /* AttachmentAssociatedElement.h in Headers */ = {isa = PBXBuildFile; fileRef = E5F67D932B65CC2400CC30DE /* AttachmentAssociatedElement.h */; settings = {ATTRIBUTES = (Private, ); }; }; + E71467B324ABAEF200FB2F50 /* AudioNodeOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = E71467B124ABAEF100FB2F50 /* AudioNodeOptions.h */; }; + F050E16823AC9C080011CE47 /* PlatformTouchEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = F050E16623AC9C070011CE47 /* PlatformTouchEvent.h */; settings = {ATTRIBUTES = (Private, ); }; }; + F050E16A23AD660C0011CE47 /* Touch.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F050E16923AD660C0011CE47 /* Touch.cpp */; }; + F050E16D23AD66630011CE47 /* TouchList.h in Headers */ = {isa = PBXBuildFile; fileRef = F050E16B23AD66620011CE47 /* TouchList.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -2036,10 +2075,10 @@ index a40df707691eec03783ba07d5102ec84ecda1911..e6f7e081d8e608d28e28a367c0cf731a + F050E17123AD669F0011CE47 /* TouchEvent.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F050E16F23AD669E0011CE47 /* TouchEvent.cpp */; }; + F050E17423AD6A800011CE47 /* DocumentTouch.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F050E17323AD6A800011CE47 /* DocumentTouch.cpp */; }; + F050E17823AD70C50011CE47 /* PlatformTouchPoint.h in Headers */ = {isa = PBXBuildFile; fileRef = F050E17623AD70C40011CE47 /* PlatformTouchPoint.h */; settings = {ATTRIBUTES = (Private, ); }; }; - F12171F616A8CF0B000053CA /* WebVTTElement.h in Headers */ = {isa = PBXBuildFile; fileRef = F12171F416A8BC63000053CA /* WebVTTElement.h */; }; - F30EE46B2E721BA800935B60 /* FrameInspectorController.h in Headers */ = {isa = PBXBuildFile; fileRef = F30EE46A2E721B9D00935B60 /* FrameInspectorController.h */; settings = {ATTRIBUTES = (Private, ); }; }; - F32BDCD92363AACA0073B6AE /* UserGestureEmulationScope.h in Headers */ = {isa = PBXBuildFile; fileRef = F32BDCD72363AACA0073B6AE /* UserGestureEmulationScope.h */; }; -@@ -22865,6 +22872,14 @@ + E71467B624ABAF1D00FB2F50 /* PannerOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = E71467B524ABAF1D00FB2F50 /* PannerOptions.h */; }; + E755E89224C7434B009F7C23 /* PeriodicWaveConstraints.h in Headers */ = {isa = PBXBuildFile; fileRef = E755E88F24C7434B009F7C23 /* PeriodicWaveConstraints.h */; }; + E755E89924C7461D009F7C23 /* PeriodicWaveOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = E755E89624C7461D009F7C23 /* PeriodicWaveOptions.h */; }; +@@ -23189,6 +23196,14 @@ EFCC6C8D20FE914000A2321B /* CanvasActivityRecord.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CanvasActivityRecord.h; sourceTree = ""; }; F088343F2E721B29001B2348 /* AXLocalFrame.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AXLocalFrame.h; sourceTree = ""; }; F08834402E721B33001B2348 /* AXLocalFrame.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = AXLocalFrame.cpp; sourceTree = ""; }; @@ -2053,8 +2092,8 @@ index a40df707691eec03783ba07d5102ec84ecda1911..e6f7e081d8e608d28e28a367c0cf731a + F050E17623AD70C40011CE47 /* PlatformTouchPoint.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PlatformTouchPoint.h; sourceTree = ""; }; F12171F316A8BC63000053CA /* WebVTTElement.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = WebVTTElement.cpp; sourceTree = ""; }; F12171F416A8BC63000053CA /* WebVTTElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebVTTElement.h; sourceTree = ""; }; - F30EE46A2E721B9D00935B60 /* FrameInspectorController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FrameInspectorController.h; sourceTree = ""; }; -@@ -30981,6 +30996,11 @@ + F2462F05A319D5DE971FCFBC /* UnifiedSource60-header-RenderStyleGetters.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "UnifiedSource60-header-RenderStyleGetters.cpp"; sourceTree = ""; }; +@@ -31328,6 +31343,11 @@ BC4A5324256055590028C592 /* TextDirectionSubmenuInclusionBehavior.h */, 2D4F96F11A1ECC240098BF88 /* TextIndicator.cpp */, 2D4F96F21A1ECC240098BF88 /* TextIndicator.h */, @@ -2066,7 +2105,7 @@ index a40df707691eec03783ba07d5102ec84ecda1911..e6f7e081d8e608d28e28a367c0cf731a F48570A42644C76D00C05F71 /* TranslationContextMenuInfo.h */, D640B24C2E3058C800EB6C49 /* UADataValues.h */, D640B24E2E3058C800EB6C49 /* UADataValues.idl */, -@@ -38987,6 +39007,8 @@ +@@ -39532,6 +39552,8 @@ 29E4D8DF16B0940F00C84704 /* PlatformSpeechSynthesizer.h */, 1AD8F81A11CAB9E900E93E54 /* PlatformStrategies.cpp */, 1AD8F81911CAB9E900E93E54 /* PlatformStrategies.h */, @@ -2075,7 +2114,7 @@ index a40df707691eec03783ba07d5102ec84ecda1911..e6f7e081d8e608d28e28a367c0cf731a FE3DC9932D0C063C0021B6FC /* PlatformTZoneImpls.cpp */, 0FD7C21D23CE41E30096D102 /* PlatformWheelEvent.cpp */, 935C476A09AC4D4F00A6AAB4 /* PlatformWheelEvent.h */, -@@ -41998,6 +42020,7 @@ +@@ -42549,6 +42571,7 @@ AD6E71AB1668899D00320C13 /* DocumentSharedObjectPool.h */, 6BDB5DC1227BD3B800919770 /* DocumentStorageAccess.cpp */, 6BDB5DC0227BD3B800919770 /* DocumentStorageAccess.h */, @@ -2083,7 +2122,7 @@ index a40df707691eec03783ba07d5102ec84ecda1911..e6f7e081d8e608d28e28a367c0cf731a 7CE7FA5B1EF882300060C9D6 /* DocumentTouch.cpp */, 7CE7FA591EF882300060C9D6 /* DocumentTouch.h */, A8185F3209765765005826D9 /* DocumentType.cpp */, -@@ -47190,6 +47213,8 @@ +@@ -47774,6 +47797,8 @@ F4E90A3C2B52038E002DA469 /* PlatformTextAlternatives.h in Headers */, 0F7D07331884C56C00B4AF86 /* PlatformTextTrack.h in Headers */, 074E82BB18A69F0E007EF54C /* PlatformTimeRanges.h in Headers */, @@ -2092,7 +2131,7 @@ index a40df707691eec03783ba07d5102ec84ecda1911..e6f7e081d8e608d28e28a367c0cf731a CDD08ABD277E542600EA3755 /* PlatformTrackConfiguration.h in Headers */, CD1F9B022700323D00617EB6 /* PlatformVideoColorPrimaries.h in Headers */, CD1F9B01270020B700617EB6 /* PlatformVideoColorSpace.h in Headers */, -@@ -48863,6 +48888,7 @@ +@@ -49450,6 +49475,7 @@ 0F54DD081881D5F5003EEDBB /* Touch.h in Headers */, 71B7EE0D21B5C6870031C1EF /* TouchAction.h in Headers */, 0F54DD091881D5F5003EEDBB /* TouchEvent.h in Headers */, @@ -2100,7 +2139,7 @@ index a40df707691eec03783ba07d5102ec84ecda1911..e6f7e081d8e608d28e28a367c0cf731a 0F54DD0A1881D5F5003EEDBB /* TouchList.h in Headers */, 070334D71459FFD5008D8D45 /* TrackBase.h in Headers */, 513C7B6E2E7C2E7A00079881 /* TrackInfo.h in Headers */, -@@ -50137,7 +50163,9 @@ +@@ -50724,7 +50750,9 @@ 2D22830323A8470700364B7E /* CursorMac.mm in Sources */, 5CBD59592280E926002B22AA /* CustomHeaderFields.cpp in Sources */, 07E4BDBF2A3A5FAB000D5509 /* DictationCaretAnimator.cpp in Sources */, @@ -2110,7 +2149,7 @@ index a40df707691eec03783ba07d5102ec84ecda1911..e6f7e081d8e608d28e28a367c0cf731a 7CE6CBFD187F394900D46BF5 /* FormatConverter.cpp in Sources */, 4667EA3E2968D9DA00BAB1E2 /* GameControllerHapticEffect.mm in Sources */, 46FE73D32968E52000B8064C /* GameControllerHapticEngines.mm in Sources */, -@@ -50233,6 +50261,9 @@ +@@ -50821,6 +50849,9 @@ 072F696F2E755BFA00281FC5 /* TextListParser.cpp in Sources */, BE39137129B267F500FA5D4F /* TextTransformCocoa.cpp in Sources */, 51DF6D800B92A18E00C2DC85 /* ThreadCheck.mm in Sources */, @@ -2121,7 +2160,7 @@ index a40df707691eec03783ba07d5102ec84ecda1911..e6f7e081d8e608d28e28a367c0cf731a 0B2AA9B7DC40569673FC9868 /* UnifiedSource1-header-RenderStyleGetters.cpp in Sources */, 538EC8031F96AF81004D22A8 /* UnifiedSource1-nonARC.mm in Sources */, diff --git a/Source/WebCore/css/query/MediaQueryFeatures.cpp b/Source/WebCore/css/query/MediaQueryFeatures.cpp -index ab1b8b7d0e7baff7d7564609736a6145472c2e3a..00921097a9f2b5d96937785e62f5a3533afafe55 100644 +index e3d2c06a8c561c7489d7315d80976c8f6feff8c4..349e0486dfe266c8edadc9073d4ed37f5f75c461 100644 --- a/Source/WebCore/css/query/MediaQueryFeatures.cpp +++ b/Source/WebCore/css/query/MediaQueryFeatures.cpp @@ -404,7 +404,11 @@ static const IdentifierSchema& forcedColorsFeatureSchema() @@ -2212,7 +2251,7 @@ index 2feeeac677ee3f36cb13c2bac1afbc57fbe70e12..6ed92e45b7659c48b3bb1e24f7f00751 SecureContext ] interface DeviceOrientationEvent : Event { diff --git a/Source/WebCore/dom/PointerEvent.cpp b/Source/WebCore/dom/PointerEvent.cpp -index dd8c59fe17c70a72f03df3c884b5a92f8f655e61..9c2b847b15f4361ad8199729c2b808a0253b77c2 100644 +index dd8c59fe17c70a72f03df3c884b5a92f8f655e61..97dc2a0d19f856090041cc3b1b23218c35fbcca4 100644 --- a/Source/WebCore/dom/PointerEvent.cpp +++ b/Source/WebCore/dom/PointerEvent.cpp @@ -20,7 +20,7 @@ @@ -2278,7 +2317,7 @@ index dd8c59fe17c70a72f03df3c884b5a92f8f655e61..9c2b847b15f4361ad8199729c2b808a0 +} + +PointerEvent::PointerEvent(const AtomString& type, const PlatformTouchEvent& event, const Vector>& coalescedEvents, const Vector>& predictedEvents, CanBubble canBubble, IsCancelable isCancelable, unsigned touchIndex, bool isPrimary, Ref&& view, const DoublePoint& touchDelta) -+ : MouseEvent(EventInterfaceType::PointerEvent, type, canBubble, isCancelable, typeIsComposed(type), event.timestamp().approximateMonotonicTime(), WTF::move(view), 0, ++ : MouseEvent(EventInterfaceType::PointerEvent, type, canBubble, isCancelable, typeIsComposed(type), event.timestamp().approximate(), WTF::move(view), 0, + event.touchPoints().at(touchIndex).pos(), event.touchPoints().at(touchIndex).pos(), touchDelta.x(), touchDelta.y(), event.modifiers(), buttonForType(type), buttonsForType(type), nullptr, 0, SyntheticClickType::NoTap, { }, { }, std::nullopt, IsSimulated::No, IsTrusted::Yes) + , m_pointerId(event.touchPoints().at(touchIndex).id()) + , m_width(2 * event.touchPoints().at(touchIndex).radius().width()) @@ -2382,7 +2421,7 @@ index d0a3d5c048647b07772e1581c76c4eb60ecf41b0..bec324636991079264e620c0dfdaf984 #endif // USE(LIBWPE) diff --git a/Source/WebCore/html/FileInputType.cpp b/Source/WebCore/html/FileInputType.cpp -index 70293a7486130c23eda9198e399f4fc7a998c2b3..1ff1dc564dbe7f2ed2d35000d82bac6e2fb2cad0 100644 +index ac3447184b6d42244da543184702f5d500bb1a63..e23c649494d88d4610a7c9ac9f142a0f5757c7f3 100644 --- a/Source/WebCore/html/FileInputType.cpp +++ b/Source/WebCore/html/FileInputType.cpp @@ -39,6 +39,7 @@ @@ -2406,10 +2445,10 @@ index 70293a7486130c23eda9198e399f4fc7a998c2b3..1ff1dc564dbe7f2ed2d35000d82bac6e return; diff --git a/Source/WebCore/inspector/FrameInspectorController.cpp b/Source/WebCore/inspector/FrameInspectorController.cpp -index 12650ec508ee1acef286c9b862e37a73f652409f..5721af8def5099347a80e7ad31c7551dbc0be0d5 100644 +index 2af3d6bb16c29ed5f2f09c34fdf8ebf2a173e768..7a36bf7ac7e93f16e9505c3b6df15446bab84a6d 100644 --- a/Source/WebCore/inspector/FrameInspectorController.cpp +++ b/Source/WebCore/inspector/FrameInspectorController.cpp -@@ -170,6 +170,12 @@ void FrameInspectorController::connectFrontend(Inspector::FrontendChannel& front +@@ -161,6 +161,12 @@ void FrameInspectorController::connectFrontend(Inspector::FrontendChannel& front UNUSED_PARAM(isAutomaticInspection); UNUSED_PARAM(immediatelyPause); @@ -2422,7 +2461,7 @@ index 12650ec508ee1acef286c9b862e37a73f652409f..5721af8def5099347a80e7ad31c7551d if (auto* page = m_frame->page()) page->settings().setDeveloperExtrasEnabled(true); -@@ -184,6 +190,19 @@ void FrameInspectorController::connectFrontend(Inspector::FrontendChannel& front +@@ -175,6 +181,19 @@ void FrameInspectorController::connectFrontend(Inspector::FrontendChannel& front m_injectedScriptManager->addClient(); m_agents.didCreateFrontendAndBackend(); } @@ -2443,10 +2482,10 @@ index 12650ec508ee1acef286c9b862e37a73f652409f..5721af8def5099347a80e7ad31c7551d void FrameInspectorController::disconnectFrontend(Inspector::FrontendChannel& frontendChannel) diff --git a/Source/WebCore/inspector/InspectorIdentifierRegistry.cpp b/Source/WebCore/inspector/InspectorIdentifierRegistry.cpp -index 1585d249dafdc13f4258ff1dde95268eeb329f93..24d21908c7a79e744aadf37a6d8ba0918f2dd7b7 100644 +index 5e93584ff771f47495036f43d1c40eb052d1db6d..4ace85cb400111278cc4bdd3ce7d04c7df9e89ca 100644 --- a/Source/WebCore/inspector/InspectorIdentifierRegistry.cpp +++ b/Source/WebCore/inspector/InspectorIdentifierRegistry.cpp -@@ -48,20 +48,22 @@ Protocol::Network::FrameId LegacyIdentifierRegistry::frameId(const WebCore::Fram +@@ -50,20 +50,22 @@ Protocol::Network::FrameId LegacyIdentifierRegistry::frameId(const WebCore::Fram { if (!frame) return emptyString(); @@ -2477,7 +2516,7 @@ index 1585d249dafdc13f4258ff1dde95268eeb329f93..24d21908c7a79e744aadf37a6d8ba091 } WebCore::LocalFrame* LegacyIdentifierRegistry::assertFrame(Protocol::ErrorString& errorString, const Protocol::Network::FrameId& frameId) -@@ -74,15 +76,19 @@ WebCore::LocalFrame* LegacyIdentifierRegistry::assertFrame(Protocol::ErrorString +@@ -76,15 +78,19 @@ WebCore::LocalFrame* LegacyIdentifierRegistry::assertFrame(Protocol::ErrorString Protocol::Network::FrameId LegacyIdentifierRegistry::takeFrame(const WebCore::Frame& frame) { @@ -2500,12 +2539,12 @@ index 1585d249dafdc13f4258ff1dde95268eeb329f93..24d21908c7a79e744aadf37a6d8ba091 + return String::number(navigationID->toUInt64()); } - } // namespace Inspector + // --- BackendIdentifierRegistry --- diff --git a/Source/WebCore/inspector/InspectorInstrumentation.cpp b/Source/WebCore/inspector/InspectorInstrumentation.cpp -index 68b5996d7ae96ce0c3f5eda869e353b448823da7..5227faf95b488a3f67e2e63e30089ba9b9cd0d5c 100644 +index 2a83aecdbc7a799517ea311a26407e72c82443f9..8d36e185ae04ba5c0f79462bbed57baa4608d921 100644 --- a/Source/WebCore/inspector/InspectorInstrumentation.cpp +++ b/Source/WebCore/inspector/InspectorInstrumentation.cpp -@@ -617,6 +617,12 @@ void InspectorInstrumentation::applyUserAgentOverrideImpl(InstrumentingAgents& i +@@ -667,6 +667,12 @@ void InspectorInstrumentation::applyUserAgentOverrideImpl(InstrumentingAgents& i pageAgent->applyUserAgentOverride(userAgent); } @@ -2518,7 +2557,7 @@ index 68b5996d7ae96ce0c3f5eda869e353b448823da7..5227faf95b488a3f67e2e63e30089ba9 void InspectorInstrumentation::applyEmulatedMediaImpl(InstrumentingAgents& instrumentingAgents, AtomString& media) { if (CheckedPtr pageAgent = instrumentingAgents.enabledPageAgent()) -@@ -700,6 +706,12 @@ void InspectorInstrumentation::didFailLoadingImpl(InstrumentingAgents& instrumen +@@ -764,6 +770,12 @@ void InspectorInstrumentation::didFailLoadingImpl(InstrumentingAgents& instrumen consoleAgent->didFailLoading(identifier, error); // This should come AFTER resource notification, front-end relies on this. } @@ -2530,8 +2569,8 @@ index 68b5996d7ae96ce0c3f5eda869e353b448823da7..5227faf95b488a3f67e2e63e30089ba9 + void InspectorInstrumentation::willLoadXHRSynchronouslyImpl(InstrumentingAgents& instrumentingAgents) { - if (auto* networkAgent = instrumentingAgents.enabledNetworkAgent()) -@@ -732,20 +744,17 @@ void InspectorInstrumentation::didReceiveScriptResponseImpl(InstrumentingAgents& + if (CheckedPtr networkAgent = instrumentingAgents.enabledNetworkAgent()) +@@ -796,20 +808,17 @@ void InspectorInstrumentation::didReceiveScriptResponseImpl(InstrumentingAgents& void InspectorInstrumentation::domContentLoadedEventFiredImpl(InstrumentingAgents& instrumentingAgents, LocalFrame& frame) { @@ -2555,7 +2594,7 @@ index 68b5996d7ae96ce0c3f5eda869e353b448823da7..5227faf95b488a3f67e2e63e30089ba9 } void InspectorInstrumentation::frameDetachedFromParentImpl(InstrumentingAgents& instrumentingAgents, LocalFrame& frame) -@@ -825,12 +834,6 @@ void InspectorInstrumentation::frameDocumentUpdatedImpl(InstrumentingAgents& ins +@@ -895,12 +904,6 @@ void InspectorInstrumentation::frameDocumentUpdatedImpl(InstrumentingAgents& ins pageDOMDebuggerAgent->frameDocumentUpdated(frame); } @@ -2568,7 +2607,7 @@ index 68b5996d7ae96ce0c3f5eda869e353b448823da7..5227faf95b488a3f67e2e63e30089ba9 void InspectorInstrumentation::frameStartedLoadingImpl(InstrumentingAgents& instrumentingAgents, LocalFrame& frame) { if (frame.isMainFrame()) { -@@ -861,6 +864,12 @@ void InspectorInstrumentation::accessibilitySettingsDidChangeImpl(InstrumentingA +@@ -931,6 +934,12 @@ void InspectorInstrumentation::accessibilitySettingsDidChangeImpl(InstrumentingA inspectorPageAgent->accessibilitySettingsDidChange(); } @@ -2581,7 +2620,7 @@ index 68b5996d7ae96ce0c3f5eda869e353b448823da7..5227faf95b488a3f67e2e63e30089ba9 #if ENABLE(DARK_MODE_CSS) void InspectorInstrumentation::defaultAppearanceDidChangeImpl(InstrumentingAgents& instrumentingAgents) { -@@ -913,6 +922,12 @@ void InspectorInstrumentation::interceptResponseImpl(InstrumentingAgents& instru +@@ -983,6 +992,12 @@ void InspectorInstrumentation::interceptResponseImpl(InstrumentingAgents& instru networkAgent->interceptResponse(response, identifier, WTF::move(handler)); } @@ -2594,7 +2633,7 @@ index 68b5996d7ae96ce0c3f5eda869e353b448823da7..5227faf95b488a3f67e2e63e30089ba9 // JavaScriptCore InspectorDebuggerAgent should know Console MessageTypes. static bool NODELETE isConsoleAssertMessage(MessageSource source, MessageType type) { -@@ -1050,6 +1065,12 @@ void InspectorInstrumentation::consoleStopRecordingCanvasImpl(InstrumentingAgent +@@ -1120,6 +1135,12 @@ void InspectorInstrumentation::consoleStopRecordingCanvasImpl(InstrumentingAgent canvasAgent->consoleStopRecordingCanvas(context); } @@ -2607,7 +2646,7 @@ index 68b5996d7ae96ce0c3f5eda869e353b448823da7..5227faf95b488a3f67e2e63e30089ba9 void InspectorInstrumentation::didDispatchDOMStorageEventImpl(InstrumentingAgents& instrumentingAgents, const String& key, const String& oldValue, const String& newValue, StorageType storageType, const SecurityOrigin& securityOrigin) { if (auto* domStorageAgent = instrumentingAgents.enabledDOMStorageAgent()) -@@ -1347,6 +1368,36 @@ void InspectorInstrumentation::renderLayerDestroyedImpl(InstrumentingAgents& ins +@@ -1417,6 +1438,36 @@ void InspectorInstrumentation::renderLayerDestroyedImpl(InstrumentingAgents& ins layerTreeAgent->renderLayerDestroyed(renderLayer); } @@ -2645,7 +2684,7 @@ index 68b5996d7ae96ce0c3f5eda869e353b448823da7..5227faf95b488a3f67e2e63e30089ba9 { return globalScope.inspectorController().m_instrumentingAgents; diff --git a/Source/WebCore/inspector/InspectorInstrumentation.h b/Source/WebCore/inspector/InspectorInstrumentation.h -index d3c1a73fd1eaa95dad8e582f8e52ed894a53a8be..24791c5f62638513dd507ff0886ae4d593523919 100644 +index 3eb556054eb2ff2e5c4c484ce35aae1d82ea117a..0d387908225a54788fd2587bc9fde0cbe8a43f4b 100644 --- a/Source/WebCore/inspector/InspectorInstrumentation.h +++ b/Source/WebCore/inspector/InspectorInstrumentation.h @@ -45,6 +45,7 @@ @@ -2656,7 +2695,7 @@ index d3c1a73fd1eaa95dad8e582f8e52ed894a53a8be..24791c5f62638513dd507ff0886ae4d5 #include "ResourceLoader.h" #include "ResourceLoaderIdentifier.h" #include "StorageArea.h" -@@ -79,6 +80,7 @@ class Document; +@@ -80,6 +81,7 @@ class Document; class DocumentLoader; class DocumentThreadableLoader; class EventListener; @@ -2664,7 +2703,7 @@ index d3c1a73fd1eaa95dad8e582f8e52ed894a53a8be..24791c5f62638513dd507ff0886ae4d5 class HTTPHeaderMap; class InspectorTimelineAgent; class InstrumentingAgents; -@@ -203,6 +205,7 @@ public: +@@ -204,6 +206,7 @@ public: static void didRecalculateStyle(Document&); static void didScheduleStyleRecalculation(Document&); static void applyUserAgentOverride(LocalFrame&, String&); @@ -2672,7 +2711,7 @@ index d3c1a73fd1eaa95dad8e582f8e52ed894a53a8be..24791c5f62638513dd507ff0886ae4d5 static void applyEmulatedMedia(LocalFrame&, AtomString&); static void flexibleBoxRendererBeganLayout(const RenderObject&); -@@ -215,6 +218,7 @@ public: +@@ -216,6 +219,7 @@ public: static void didReceiveData(LocalFrame*, ResourceLoaderIdentifier, const SharedBuffer*, int encodedDataLength); static void didFinishLoading(LocalFrame*, DocumentLoader*, ResourceLoaderIdentifier, const NetworkLoadMetrics&, ResourceLoader*); static void didFailLoading(LocalFrame*, DocumentLoader*, ResourceLoaderIdentifier, const ResourceError&); @@ -2723,7 +2762,7 @@ index d3c1a73fd1eaa95dad8e582f8e52ed894a53a8be..24791c5f62638513dd507ff0886ae4d5 static void frontendDeleted(); static bool hasFrontends() { return InspectorInstrumentationPublic::hasFrontends(); } @@ -434,6 +446,7 @@ private: - static void didRecalculateStyleImpl(InstrumentingAgents&); + static void didRecalculateStyleImpl(InstrumentingAgents&, Document&); static void didScheduleStyleRecalculationImpl(InstrumentingAgents&, Document&); static void applyUserAgentOverrideImpl(InstrumentingAgents&, String&); + static void applyPlatformOverrideImpl(InstrumentingAgents&, String&); @@ -2744,7 +2783,7 @@ index d3c1a73fd1eaa95dad8e582f8e52ed894a53a8be..24791c5f62638513dd507ff0886ae4d5 static void frameDocumentUpdatedImpl(InstrumentingAgents&, LocalFrame&); - static void loaderDetachedFromFrameImpl(InstrumentingAgents&, DocumentLoader&); static void frameStartedLoadingImpl(InstrumentingAgents&, LocalFrame&); - static void didCompleteRenderingFrameImpl(InstrumentingAgents&); + static void didCompleteRenderingFrameImpl(InstrumentingAgents&, LocalFrame&); static void frameStoppedLoadingImpl(InstrumentingAgents&, LocalFrame&); static void accessibilitySettingsDidChangeImpl(InstrumentingAgents&); + static void didNavigateWithinPageImpl(InstrumentingAgents&, LocalFrame&); @@ -2951,10 +2990,10 @@ index 01ad196099f09a89717830cd70dded3b495cf5f5..c9d2e97c6f5f5e6b0a8f70c31af947ef + } diff --git a/Source/WebCore/inspector/PageInspectorController.cpp b/Source/WebCore/inspector/PageInspectorController.cpp -index 4fa92ae7d7c2f6e501eecb90691c3dca65e4482e..21dbf683927dec650570a540f0fcd5be6538afa5 100644 +index f5a5196bce1339186a045872f59a18adaffff83e..b476dc81f398f60591d320e66a4cf99d15cd57f6 100644 --- a/Source/WebCore/inspector/PageInspectorController.cpp +++ b/Source/WebCore/inspector/PageInspectorController.cpp -@@ -291,6 +291,8 @@ void PageInspectorController::disconnectFrontend(FrontendChannel& frontendChanne +@@ -295,6 +295,8 @@ void PageInspectorController::disconnectFrontend(FrontendChannel& frontendChanne // Unplug all instrumentations since they aren't needed now. InspectorInstrumentation::unregisterInstrumentingAgents(m_instrumentingAgents.get()); @@ -2963,7 +3002,7 @@ index 4fa92ae7d7c2f6e501eecb90691c3dca65e4482e..21dbf683927dec650570a540f0fcd5be } m_inspectorBackendClient->frontendCountChanged(m_frontendRouter->frontendCount()); -@@ -305,6 +307,8 @@ void PageInspectorController::disconnectAllFrontends() +@@ -309,6 +311,8 @@ void PageInspectorController::disconnectAllFrontends() // The frontend should call setInspectorFrontendClient(nullptr) under closeWindow(). ASSERT(!m_inspectorFrontendClient); @@ -2972,7 +3011,7 @@ index 4fa92ae7d7c2f6e501eecb90691c3dca65e4482e..21dbf683927dec650570a540f0fcd5be if (!m_frontendRouter->hasFrontends()) return; -@@ -384,8 +388,8 @@ void PageInspectorController::inspect(Node* node) +@@ -405,8 +409,8 @@ void PageInspectorController::inspect(Node* node) if (!enabled()) return; @@ -2983,7 +3022,7 @@ index 4fa92ae7d7c2f6e501eecb90691c3dca65e4482e..21dbf683927dec650570a540f0fcd5be CheckedRef { ensureDOMAgent() }->inspect(node); } -@@ -523,4 +527,34 @@ void PageInspectorController::didComposite(LocalFrame& frame) +@@ -544,4 +548,34 @@ void PageInspectorController::didComposite(LocalFrame& frame) InspectorInstrumentation::didComposite(frame); } @@ -3019,10 +3058,10 @@ index 4fa92ae7d7c2f6e501eecb90691c3dca65e4482e..21dbf683927dec650570a540f0fcd5be + } // namespace WebCore diff --git a/Source/WebCore/inspector/PageInspectorController.h b/Source/WebCore/inspector/PageInspectorController.h -index 1418886b19d5310d76deef334ae994ff8afd0562..6217b778b6c0bd73d7da6f9b8ef1454dda156806 100644 +index ebe3fd57747f494efcbe4d62211850ab4e18220b..c864facba5ceec1402b250385d0ec5d5bf9364ec 100644 --- a/Source/WebCore/inspector/PageInspectorController.h +++ b/Source/WebCore/inspector/PageInspectorController.h -@@ -117,6 +117,12 @@ public: +@@ -120,6 +120,12 @@ public: WEBCORE_EXPORT void willComposite(LocalFrame&); WEBCORE_EXPORT void didComposite(LocalFrame&); @@ -3035,7 +3074,7 @@ index 1418886b19d5310d76deef334ae994ff8afd0562..6217b778b6c0bd73d7da6f9b8ef1454d // Testing support. bool isUnderTest() const { return m_isUnderTest; } void setIsUnderTest(bool isUnderTest) { m_isUnderTest = isUnderTest; } -@@ -153,6 +159,7 @@ private: +@@ -158,6 +164,7 @@ private: PageAgentContext pageAgentContext(); void createLazyAgents(); @@ -3043,7 +3082,7 @@ index 1418886b19d5310d76deef334ae994ff8afd0562..6217b778b6c0bd73d7da6f9b8ef1454d WeakRef m_page; const Ref m_instrumentingAgents; -@@ -177,6 +184,7 @@ private: +@@ -182,6 +189,7 @@ private: bool m_isAutomaticInspection { false }; bool m_pauseAfterInitialization = { false }; bool m_didCreateLazyAgents { false }; @@ -3052,7 +3091,7 @@ index 1418886b19d5310d76deef334ae994ff8afd0562..6217b778b6c0bd73d7da6f9b8ef1454d } // namespace WebCore diff --git a/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp b/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp -index 077b6857627c916d6b8d6cfcf0b1018dd597a7af..67d109a50be0a05dd1179c32e20897883558cb09 100644 +index ae136036740e893a6d3755f66af4bf82ce2980fb..85e8afab72349ba6a66a94ae0d42968ac6e8564f 100644 --- a/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp +++ b/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp @@ -54,6 +54,7 @@ @@ -3079,22 +3118,21 @@ index 077b6857627c916d6b8d6cfcf0b1018dd597a7af..67d109a50be0a05dd1179c32e2089788 #include "HTMLMediaElement.h" #include "HTMLNames.h" #include "HTMLScriptElement.h" -@@ -107,12 +113,14 @@ +@@ -106,11 +112,13 @@ #include "Pasteboard.h" #include "PseudoElement.h" #include "RenderGrid.h" +#include "RenderLayer.h" #include "RenderObject.h" - #include "RenderStyle.h" #include "RenderStyleConstants.h" #include "ScriptController.h" #include "SelectorChecker.h" #include "ShadowRoot.h" +#include "SharedBuffer.h" #include "StaticNodeList.h" + #include "StyleComputedStyle.h" #include "StyleProperties.h" - #include "StyleResolver.h" -@@ -157,7 +165,8 @@ using namespace HTMLNames; +@@ -156,7 +164,8 @@ using namespace HTMLNames; static const size_t maxTextSize = 10000; static const char16_t horizontalEllipsisUTF16[] = { horizontalEllipsis, 0 }; @@ -3104,7 +3142,7 @@ index 077b6857627c916d6b8d6cfcf0b1018dd597a7af..67d109a50be0a05dd1179c32e2089788 { if (!colorObject) return std::nullopt; -@@ -176,7 +185,7 @@ static std::optional parseColor(RefPtr&& colorObject) +@@ -175,7 +184,7 @@ static std::optional parseColor(RefPtr&& colorObject) static std::optional parseRequiredConfigColor(const String& fieldName, JSON::Object& configObject) { @@ -3113,7 +3151,7 @@ index 077b6857627c916d6b8d6cfcf0b1018dd597a7af..67d109a50be0a05dd1179c32e2089788 } static Color parseOptionalConfigColor(const String& fieldName, JSON::Object& configObject) -@@ -203,6 +212,20 @@ static bool parseQuad(Ref&& quadArray, FloatQuad* quad) +@@ -202,6 +211,20 @@ static bool parseQuad(Ref&& quadArray, FloatQuad* quad) return true; } @@ -3134,7 +3172,7 @@ index 077b6857627c916d6b8d6cfcf0b1018dd597a7af..67d109a50be0a05dd1179c32e2089788 class RevalidateStyleAttributeTask final : public CanMakeCheckedPtr { WTF_MAKE_TZONE_ALLOCATED(RevalidateStyleAttributeTask); WTF_OVERRIDE_DELETE_FOR_CHECKED_PTR(RevalidateStyleAttributeTask); -@@ -486,6 +509,20 @@ Node* InspectorDOMAgent::assertNode(Inspector::Protocol::ErrorString& errorStrin +@@ -485,6 +508,20 @@ Node* InspectorDOMAgent::assertNode(Inspector::Protocol::ErrorString& errorStrin return node.unsafeGet(); } @@ -3155,7 +3193,7 @@ index 077b6857627c916d6b8d6cfcf0b1018dd597a7af..67d109a50be0a05dd1179c32e2089788 Document* InspectorDOMAgent::assertDocument(Inspector::Protocol::ErrorString& errorString, Inspector::Protocol::DOM::NodeId nodeId) { RefPtr node = assertNode(errorString, nodeId); -@@ -1603,15 +1640,7 @@ Inspector::Protocol::ErrorStringOr InspectorDOMAgent::highlightNode(std::o +@@ -1602,15 +1639,7 @@ Inspector::Protocol::ErrorStringOr InspectorDOMAgent::highlightNode(std::o { Inspector::Protocol::ErrorString errorString; @@ -3172,7 +3210,7 @@ index 077b6857627c916d6b8d6cfcf0b1018dd597a7af..67d109a50be0a05dd1179c32e2089788 if (!node) return makeUnexpected(errorString); -@@ -1862,15 +1891,159 @@ Inspector::Protocol::ErrorStringOr InspectorDOMAgent::setInspectedNode(Ins +@@ -1861,15 +1890,159 @@ Inspector::Protocol::ErrorStringOr InspectorDOMAgent::setInspectedNode(Ins return { }; } @@ -3335,7 +3373,7 @@ index 077b6857627c916d6b8d6cfcf0b1018dd597a7af..67d109a50be0a05dd1179c32e2089788 if (!object) return makeUnexpected("Missing injected script for given nodeId"_s); -@@ -3134,7 +3307,7 @@ Inspector::Protocol::ErrorStringOr InspectorDO +@@ -3133,7 +3306,7 @@ Inspector::Protocol::ErrorStringOr InspectorDO return makeUnexpected("Missing node for given path"_s); } @@ -3344,7 +3382,7 @@ index 077b6857627c916d6b8d6cfcf0b1018dd597a7af..67d109a50be0a05dd1179c32e2089788 { RefPtr document = &node->document(); if (auto* templateHost = document->templateDocumentHost()) -@@ -3143,12 +3316,18 @@ RefPtr InspectorDOMAgent::resolveNod +@@ -3142,12 +3315,18 @@ RefPtr InspectorDOMAgent::resolveNod if (!frame) return nullptr; @@ -3366,7 +3404,7 @@ index 077b6857627c916d6b8d6cfcf0b1018dd597a7af..67d109a50be0a05dd1179c32e2089788 } Node* InspectorDOMAgent::scriptValueAsNode(JSC::JSValue value) -@@ -3302,4 +3481,53 @@ Inspector::Protocol::ErrorStringOr> In +@@ -3301,4 +3480,53 @@ Inspector::Protocol::ErrorStringOr> In #endif } @@ -3494,7 +3532,7 @@ index 8f83cc5eaca550bbcea167c98b944f704d9a9cb1..2f655b6534f1428a23907e1cada99c24 void discardBindings(); diff --git a/Source/WebCore/inspector/agents/InspectorNetworkAgent.cpp b/Source/WebCore/inspector/agents/InspectorNetworkAgent.cpp -index dd164a0c7161396ee21bc3e1e4f7889f5ddbb770..58a528c6ac0efea4b383688d5369606013282075 100644 +index 51f9c0efda1011e93a2c85bd44b85e4e0db5e8f3..991231e85c4ceb3a8f62a6c2fc3529472d66087f 100644 --- a/Source/WebCore/inspector/agents/InspectorNetworkAgent.cpp +++ b/Source/WebCore/inspector/agents/InspectorNetworkAgent.cpp @@ -62,6 +62,7 @@ @@ -3505,7 +3543,7 @@ index dd164a0c7161396ee21bc3e1e4f7889f5ddbb770..58a528c6ac0efea4b383688d53696060 #include "Page.h" #include "PlatformStrategies.h" #include "ProgressTracker.h" -@@ -275,8 +276,8 @@ static Ref buildObjectForResourceRequest( +@@ -276,8 +277,8 @@ static Ref buildObjectForResourceRequest( .release(); if (request.httpBody() && !request.httpBody()->isEmpty()) { @@ -3516,7 +3554,7 @@ index dd164a0c7161396ee21bc3e1e4f7889f5ddbb770..58a528c6ac0efea4b383688d53696060 } if (resourceLoader) { -@@ -328,6 +329,8 @@ RefPtr InspectorNetworkAgent::buildObjec +@@ -329,6 +330,8 @@ RefPtr InspectorNetworkAgent::buildObjec .setSource(responseSource(response.source())) .release(); @@ -3525,7 +3563,7 @@ index dd164a0c7161396ee21bc3e1e4f7889f5ddbb770..58a528c6ac0efea4b383688d53696060 if (resourceLoader) { auto* metrics = response.deprecatedNetworkLoadMetricsOrNull(); responseObject->setTiming(buildObjectForTiming(metrics ? *metrics : NetworkLoadMetrics::emptyMetrics(), *resourceLoader)); -@@ -522,7 +525,7 @@ void InspectorNetworkAgent::didReceiveResponse(ResourceLoaderIdentifier identifi +@@ -523,7 +526,7 @@ void InspectorNetworkAgent::didReceiveResponse(ResourceLoaderIdentifier identifi // 'Raw' is used for loading worker scripts, and those should stay as 'Script' and not change to 'XHR' type. if (type != newType && newType != ResourceType::XHR && newType != ResourceType::Other) type = newType; @@ -3534,7 +3572,7 @@ index dd164a0c7161396ee21bc3e1e4f7889f5ddbb770..58a528c6ac0efea4b383688d53696060 // FIXME: 304 Not Modified responses for XHR/Fetch do not have all their information from the cache. if (isNotModified && (type == ResourceType::XHR || type == ResourceType::Fetch) && (!cachedResource || !cachedResource->encodedSize())) { if (auto previousResourceData = m_resourcesData->dataForURL(response.url().string())) { -@@ -533,12 +536,12 @@ void InspectorNetworkAgent::didReceiveResponse(ResourceLoaderIdentifier identifi +@@ -534,12 +537,12 @@ void InspectorNetworkAgent::didReceiveResponse(ResourceLoaderIdentifier identifi m_resourcesData->maybeAddResourceData(requestId, buffer); }); } @@ -3550,7 +3588,7 @@ index dd164a0c7161396ee21bc3e1e4f7889f5ddbb770..58a528c6ac0efea4b383688d53696060 resourceResponse->setString("source"_s, Inspector::Protocol::Helpers::getEnumConstantValue(Inspector::Protocol::Network::Response::Source::DiskCache)); } } -@@ -617,6 +620,9 @@ void InspectorNetworkAgent::didFailLoading(ResourceLoaderIdentifier identifier, +@@ -618,6 +621,9 @@ void InspectorNetworkAgent::didFailLoading(ResourceLoaderIdentifier identifier, String requestId = IdentifiersFactory::requestId(identifier.toUInt64()); if (loader && m_resourcesData->resourceType(requestId) == ResourceType::Document) { @@ -3560,7 +3598,7 @@ index dd164a0c7161396ee21bc3e1e4f7889f5ddbb770..58a528c6ac0efea4b383688d53696060 auto* frame = loader->frame(); if (frame && frame->loader().documentLoader() && frame->document()) { m_resourcesData->addResourceSharedBuffer(requestId, -@@ -846,6 +852,7 @@ Inspector::Protocol::ErrorStringOr InspectorNetworkAgent::disable() +@@ -847,6 +853,7 @@ Inspector::Protocol::ErrorStringOr InspectorNetworkAgent::disable() Ref { m_instrumentingAgents.get() }->setEnabledNetworkAgent(nullptr); m_resourcesData->clear(); m_extraRequestHeaders.clear(); @@ -3568,7 +3606,7 @@ index dd164a0c7161396ee21bc3e1e4f7889f5ddbb770..58a528c6ac0efea4b383688d53696060 continuePendingRequests(); continuePendingResponses(); -@@ -904,6 +911,7 @@ void InspectorNetworkAgent::continuePendingResponses() +@@ -905,6 +912,7 @@ void InspectorNetworkAgent::continuePendingResponses() Inspector::Protocol::ErrorStringOr InspectorNetworkAgent::setExtraHTTPHeaders(Ref&& headers) { @@ -3576,7 +3614,7 @@ index dd164a0c7161396ee21bc3e1e4f7889f5ddbb770..58a528c6ac0efea4b383688d53696060 for (auto& entry : headers.get()) { auto stringValue = entry.value->asString(); if (!!stringValue) -@@ -1158,6 +1166,11 @@ void InspectorNetworkAgent::interceptResponse(const ResourceResponse& response, +@@ -1169,6 +1177,11 @@ void InspectorNetworkAgent::interceptResponse(const ResourceResponse& response, m_frontendDispatcher->responseIntercepted(requestId, resourceResponse.releaseNonNull()); } @@ -3588,7 +3626,7 @@ index dd164a0c7161396ee21bc3e1e4f7889f5ddbb770..58a528c6ac0efea4b383688d53696060 Inspector::Protocol::ErrorStringOr InspectorNetworkAgent::interceptContinue(const Inspector::Protocol::Network::RequestId& requestId, Inspector::Protocol::Network::NetworkStage networkStage) { switch (networkStage) { -@@ -1187,6 +1200,9 @@ Inspector::Protocol::ErrorStringOr InspectorNetworkAgent::interceptWithReq +@@ -1198,6 +1211,9 @@ Inspector::Protocol::ErrorStringOr InspectorNetworkAgent::interceptWithReq return makeUnexpected("Missing pending intercept request for given requestId"_s); auto& loader = *pendingRequest->m_loader; @@ -3598,7 +3636,7 @@ index dd164a0c7161396ee21bc3e1e4f7889f5ddbb770..58a528c6ac0efea4b383688d53696060 ResourceRequest request = loader.request(); if (!!url) request.setURL(URL({ }, url)); -@@ -1282,13 +1298,22 @@ Inspector::Protocol::ErrorStringOr InspectorNetworkAgent::interceptRequest +@@ -1293,13 +1309,22 @@ Inspector::Protocol::ErrorStringOr InspectorNetworkAgent::interceptRequest response.setHTTPStatusCode(status); response.setHTTPStatusText(String { statusText }); HTTPHeaderMap explicitHeaders; @@ -3622,7 +3660,7 @@ index dd164a0c7161396ee21bc3e1e4f7889f5ddbb770..58a528c6ac0efea4b383688d53696060 loader->didReceiveResponse(WTF::move(response), [loader, buffer = data.releaseNonNull()]() { if (loader->reachedTerminalState()) return; -@@ -1352,6 +1377,12 @@ Inspector::Protocol::ErrorStringOr InspectorNetworkAgent::setEmulatedCondi +@@ -1363,6 +1388,12 @@ Inspector::Protocol::ErrorStringOr InspectorNetworkAgent::setEmulatedCondi #endif // ENABLE(INSPECTOR_NETWORK_THROTTLING) @@ -3636,19 +3674,19 @@ index dd164a0c7161396ee21bc3e1e4f7889f5ddbb770..58a528c6ac0efea4b383688d53696060 { auto searchResult = Inspector::Protocol::Page::SearchResult::create() diff --git a/Source/WebCore/inspector/agents/InspectorNetworkAgent.h b/Source/WebCore/inspector/agents/InspectorNetworkAgent.h -index 5875ac43836d8cbd9b40e82ebf964a34eb65254f..175bd8649c496232fed546ccbb12102722d8608b 100644 +index 306305d101151c4e1e1ac710add6e95dc237d935..848177b164a308d6916df22e022e14c1752a5f14 100644 --- a/Source/WebCore/inspector/agents/InspectorNetworkAgent.h +++ b/Source/WebCore/inspector/agents/InspectorNetworkAgent.h -@@ -35,6 +35,8 @@ - #include "InspectorPageAgent.h" +@@ -36,6 +36,8 @@ #include "InspectorWebAgentBase.h" + #include "NetworkAgentInstrumentation.h" #include "NetworkResourcesData.h" +#include "ResourceError.h" +#include "SharedBuffer.h" #include "WebSocket.h" #include #include -@@ -105,6 +107,7 @@ public: +@@ -106,6 +108,7 @@ public: #if ENABLE(INSPECTOR_NETWORK_THROTTLING) Inspector::Protocol::ErrorStringOr setEmulatedConditions(std::optional&& bytesPerSecondLimit) final; #endif @@ -3656,7 +3694,7 @@ index 5875ac43836d8cbd9b40e82ebf964a34eb65254f..175bd8649c496232fed546ccbb121027 // InspectorInstrumentation void NODELETE willRecalculateStyle(); -@@ -136,6 +139,7 @@ public: +@@ -137,6 +140,7 @@ public: bool shouldInterceptResponse(const ResourceResponse&); void interceptResponse(const ResourceResponse&, ResourceLoaderIdentifier, CompletionHandler)>&&); void interceptRequest(ResourceLoader&, Function&&); @@ -3664,7 +3702,7 @@ index 5875ac43836d8cbd9b40e82ebf964a34eb65254f..175bd8649c496232fed546ccbb121027 void searchOtherRequests(const JSC::Yarr::RegularExpression&, Ref>&); void searchInRequest(Inspector::Protocol::ErrorString&, const Inspector::Protocol::Network::RequestId&, const String& query, bool caseSensitive, bool isRegex, RefPtr>&); -@@ -192,6 +196,7 @@ private: +@@ -193,6 +197,7 @@ private: bool m_loadingXHRSynchronously { false }; bool m_interceptionEnabled { false }; bool m_clearResourceDataOnNavigate { true }; @@ -3673,7 +3711,7 @@ index 5875ac43836d8cbd9b40e82ebf964a34eb65254f..175bd8649c496232fed546ccbb121027 } // namespace WebCore diff --git a/Source/WebCore/inspector/agents/InspectorPageAgent.cpp b/Source/WebCore/inspector/agents/InspectorPageAgent.cpp -index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11d5e1fb03 100644 +index 3f9caf323ad1fb608aab868e20dce4cdb0ef1749..b79f19f291d2546fc517529168e1ecf8a7494827 100644 --- a/Source/WebCore/inspector/agents/InspectorPageAgent.cpp +++ b/Source/WebCore/inspector/agents/InspectorPageAgent.cpp @@ -32,6 +32,7 @@ @@ -3684,13 +3722,12 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 #include "CachedResource.h" #include "Cookie.h" #include "CookieJar.h" -@@ -39,14 +40,17 @@ +@@ -39,13 +40,16 @@ #include "DocumentLoader.h" #include "DocumentResourceLoader.h" #include "DocumentView.h" +#include "Editor.h" #include "ElementInlines.h" - #include "EventTargetInlines.h" +#include "FocusController.h" #include "ForcedAccessibilityValue.h" #include "FrameInlines.h" @@ -3702,7 +3739,7 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 #include "HTMLNames.h" #include "ImageBuffer.h" #include "ImageUtilities.h" -@@ -57,30 +61,38 @@ +@@ -56,6 +60,7 @@ #include "InspectorOverlay.h" #include "InspectorResourceUtilities.h" #include "InstrumentingAgents.h" @@ -3710,9 +3747,10 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 #include "LocalFrame.h" #include "LocalFrameView.h" #include "MIMETypeRegistry.h" - #include "MemoryCache.h" +@@ -63,24 +68,32 @@ #include "Page.h" #include "PageInspectorController.h" + #include "RemoteFrame.h" +#include "PlatformScreen.h" #include "RenderObjectInlines.h" #include "RenderTheme.h" @@ -3730,6 +3768,7 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 #include #include +#include ++#include #include +#include #include @@ -3741,7 +3780,7 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 #include #if ENABLE(APPLICATION_MANIFEST) -@@ -102,6 +114,11 @@ using namespace Inspector; +@@ -102,6 +115,11 @@ using namespace Inspector; WTF_MAKE_TZONE_ALLOCATED_IMPL(InspectorPageAgent); @@ -3753,7 +3792,7 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 InspectorOverlay& InspectorPageAgent::overlay() const { return m_overlay.get(); -@@ -112,6 +129,7 @@ InspectorPageAgent::InspectorPageAgent(PageAgentContext& context, InspectorBacke +@@ -112,6 +130,7 @@ InspectorPageAgent::InspectorPageAgent(PageAgentContext& context, InspectorBacke , m_frontendDispatcher(makeUniqueRef(context.frontendRouter)) , m_backendDispatcher(Inspector::PageBackendDispatcher::create(context.backendDispatcher, this)) , m_inspectedPage(context.inspectedPage) @@ -3761,7 +3800,7 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 , m_client(client) , m_overlay(overlay) { -@@ -142,12 +160,20 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::enable() +@@ -142,12 +161,20 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::enable() defaultUserPreferencesDidChange(); @@ -3782,7 +3821,7 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 std::ignore = setShowPaintRects(false); #if !PLATFORM(IOS_FAMILY) -@@ -200,6 +226,22 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::reload(std::optiona +@@ -200,6 +227,22 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::reload(std::optiona return { }; } @@ -3805,7 +3844,7 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 Inspector::Protocol::ErrorStringOr InspectorPageAgent::overrideUserAgent(const String& value) { m_userAgentOverride = value; -@@ -207,6 +249,13 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::overrideUserAgent(c +@@ -207,6 +250,13 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::overrideUserAgent(c return { }; } @@ -3819,7 +3858,7 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 Inspector::Protocol::ErrorStringOr InspectorPageAgent::overrideSetting(Inspector::Protocol::Page::Setting setting, std::optional&& value) { auto& inspectedPageSettings = m_inspectedPage->settings(); -@@ -220,6 +269,12 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::overrideSetting(Ins +@@ -220,6 +270,12 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::overrideSetting(Ins inspectedPageSettings.setAuthorAndUserStylesEnabledInspectorOverride(value); return { }; @@ -3832,7 +3871,7 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 case Inspector::Protocol::Page::Setting::ICECandidateFilteringEnabled: inspectedPageSettings.setICECandidateFilteringEnabledInspectorOverride(value); return { }; -@@ -246,6 +301,39 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::overrideSetting(Ins +@@ -246,6 +302,39 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::overrideSetting(Ins m_client->setDeveloperPreferenceOverride(InspectorBackendClient::DeveloperPreference::NeedsSiteSpecificQuirks, value); return { }; @@ -3872,7 +3911,7 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 case Inspector::Protocol::Page::Setting::ScriptEnabled: inspectedPageSettings.setScriptEnabledInspectorOverride(value); return { }; -@@ -258,6 +346,12 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::overrideSetting(Ins +@@ -258,6 +347,12 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::overrideSetting(Ins inspectedPageSettings.setShowRepaintCounterInspectorOverride(value); return { }; @@ -3885,7 +3924,7 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 case Inspector::Protocol::Page::Setting::WebSecurityEnabled: inspectedPageSettings.setWebSecurityEnabledInspectorOverride(value); return { }; -@@ -670,15 +764,16 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::setShowPaintRects(b +@@ -670,15 +765,16 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::setShowPaintRects(b return { }; } @@ -3907,7 +3946,7 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 } void InspectorPageAgent::frameNavigated(LocalFrame& frame) -@@ -686,6 +781,22 @@ void InspectorPageAgent::frameNavigated(LocalFrame& frame) +@@ -686,6 +782,22 @@ void InspectorPageAgent::frameNavigated(LocalFrame& frame) m_frontendDispatcher->frameNavigated(buildObjectForFrame(&frame)); } @@ -3930,7 +3969,7 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 void InspectorPageAgent::frameDetached(LocalFrame& frame) { auto identifier = m_inspectedPage->inspectorController().identifierRegistry().takeFrame(frame); -@@ -758,6 +869,12 @@ void InspectorPageAgent::defaultUserPreferencesDidChange() +@@ -758,6 +870,12 @@ void InspectorPageAgent::defaultUserPreferencesDidChange() m_frontendDispatcher->defaultUserPreferencesDidChange(WTF::move(defaultUserPreferences)); } @@ -3943,7 +3982,7 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 #if ENABLE(DARK_MODE_CSS) void InspectorPageAgent::defaultAppearanceDidChange() { -@@ -771,6 +888,9 @@ void InspectorPageAgent::didClearWindowObjectInWorld(LocalFrame& frame, DOMWrapp +@@ -771,6 +889,9 @@ void InspectorPageAgent::didClearWindowObjectInWorld(LocalFrame& frame, DOMWrapp return; if (m_bootstrapScript.isEmpty()) @@ -3953,7 +3992,7 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 return; frame.script().evaluateIgnoringException(ScriptSourceCode(m_bootstrapScript, JSC::SourceTaintedOrigin::Untainted, URL { "web-inspector://bootstrap.js"_str })); -@@ -818,6 +938,51 @@ void InspectorPageAgent::didRecalculateStyle() +@@ -818,6 +939,51 @@ void InspectorPageAgent::didRecalculateStyle() protect(overlay())->update(); } @@ -4005,7 +4044,7 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 Ref InspectorPageAgent::buildObjectForFrame(LocalFrame* frame) { ASSERT_ARG(frame, frame); -@@ -911,6 +1076,12 @@ void InspectorPageAgent::applyUserAgentOverride(String& userAgent) +@@ -933,6 +1099,12 @@ void InspectorPageAgent::applyUserAgentOverride(String& userAgent) userAgent = m_userAgentOverride; } @@ -4018,7 +4057,7 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 void InspectorPageAgent::applyEmulatedMedia(AtomString& media) { if (!m_emulatedMedia.isEmpty()) -@@ -926,7 +1097,7 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::snapshotNode(Insp +@@ -948,7 +1120,7 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::snapshotNode(Insp RefPtr node = domAgent->assertNode(errorString, nodeId); if (!node) return makeUnexpected(errorString); @@ -4027,13 +4066,16 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 RefPtr localMainFrame = m_inspectedPage->localMainFrame(); if (!localMainFrame) return makeUnexpected("Main frame isn't local"_s); -@@ -937,11 +1108,13 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::snapshotNode(Insp +@@ -959,11 +1131,16 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::snapshotNode(Insp return encodeDataURL(WTF::move(snapshot), "image/png"_s); } -Inspector::Protocol::ErrorStringOr InspectorPageAgent::snapshotRect(int x, int y, int width, int height, Inspector::Protocol::Page::CoordinateSystem coordinateSystem) -+Inspector::Protocol::ErrorStringOr InspectorPageAgent::snapshotRect(int x, int y, int width, int height, Inspector::Protocol::Page::CoordinateSystem coordinateSystem, std::optional&& omitDeviceScaleFactor) ++Inspector::Protocol::ErrorStringOr InspectorPageAgent::snapshotRect(int x, int y, int width, int height, Inspector::Protocol::Page::CoordinateSystem coordinateSystem, std::optional&& omitDeviceScaleFactor, std::optional&& format, std::optional&& quality) { ++ if (quality && (*quality < 0 || *quality > 100)) ++ return makeUnexpected("Quality must be between 0 and 100"_s); ++ SnapshotOptions options { { }, PixelFormat::BGRA8, DestinationColorSpace::SRGB() }; if (coordinateSystem == Inspector::Protocol::Page::CoordinateSystem::Viewport) options.flags.add(SnapshotFlags::InViewCoordinates); @@ -4042,10 +4084,30 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 IntRect rectangle(x, y, width, height); RefPtr localMainFrame = m_inspectedPage->localMainFrame(); -@@ -954,6 +1127,43 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::snapshotRect(int - return encodeDataURL(WTF::move(snapshot), "image/png"_s); - } +@@ -973,9 +1150,68 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::snapshotRect(int + if (!snapshot) + return makeUnexpected("Could not capture snapshot"_s); +- return encodeDataURL(WTF::move(snapshot), "image/png"_s); ++ ++ String mimeType; ++ std::optional encodingQuality; ++ switch (format.value_or(Inspector::Protocol::Page::ImageFormat::Png)) { ++ case Inspector::Protocol::Page::ImageFormat::Png: ++ mimeType = "image/png"_s; ++ break; ++ case Inspector::Protocol::Page::ImageFormat::Jpeg: ++ mimeType = "image/jpeg"_s; ++ encodingQuality = quality.value_or(80) / 100.0; ++ break; ++ case Inspector::Protocol::Page::ImageFormat::Webp: ++ mimeType = "image/webp"_s; ++ encodingQuality = quality.value_or(80) / 100.0; ++ break; ++ } ++ return encodeDataURL(WTF::move(snapshot), mimeType, encodingQuality); ++} ++ +Inspector::Protocol::ErrorStringOr InspectorPageAgent::setForcedColors(std::optional&& forcedColors) +{ + if (!forcedColors) { @@ -4072,9 +4134,15 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 + if (!success) + return makeUnexpected(makeString("Invalid time zone "_s, timeZone)); + ++#if PLATFORM(COCOA) ++ // JSC caches time zone information process-wide using lastTimeZoneID as the cache ++ // key. The cache is normally invalidated only by the system time zone change ++ // notification, so bump the counter explicitly to force re-reading the override. ++ ++JSC::lastTimeZoneID; ++#endif + return { }; -+} -+ + } + +Inspector::Protocol::ErrorStringOr InspectorPageAgent::setTouchEmulationEnabled(bool enabled) +{ + setScreenHasTouchDeviceOverride(enabled); @@ -4086,7 +4154,7 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 #if ENABLE(WEB_ARCHIVE) && USE(CF) Inspector::Protocol::ErrorStringOr InspectorPageAgent::archive() { -@@ -970,7 +1180,6 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::archive() +@@ -992,7 +1228,6 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::archive() } #endif @@ -4094,7 +4162,7 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 Inspector::Protocol::ErrorStringOr InspectorPageAgent::setScreenSizeOverride(std::optional&& width, std::optional&& height) { if (width.has_value() != height.has_value()) -@@ -988,6 +1197,86 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::setScreenSizeOverri +@@ -1010,6 +1245,86 @@ Inspector::Protocol::ErrorStringOr InspectorPageAgent::setScreenSizeOverri localMainFrame->setOverrideScreenSize(FloatSize(width.value_or(0), height.value_or(0))); return { }; } @@ -4183,7 +4251,7 @@ index 6bb8dd192ae2fedac06e2b35bb9c6711950d5b23..5fb689a1a62b8fd687b73c590236ac11 } // namespace WebCore diff --git a/Source/WebCore/inspector/agents/InspectorPageAgent.h b/Source/WebCore/inspector/agents/InspectorPageAgent.h -index 97c3cf09961f5c5ea7ede70d35755639fc66a3df..d5aabfbb3062f6037415bb3e136d3d7cebdfbbfd 100644 +index 0af80809258326e52818831166b3ff901e4b9663..ed85e4d92b1c2a5aa56fd390d8711322575e8050 100644 --- a/Source/WebCore/inspector/agents/InspectorPageAgent.h +++ b/Source/WebCore/inspector/agents/InspectorPageAgent.h @@ -43,10 +43,12 @@ @@ -4194,7 +4262,7 @@ index 97c3cf09961f5c5ea7ede70d35755639fc66a3df..d5aabfbb3062f6037415bb3e136d3d7c #include namespace Inspector { - enum class ResourceType; + enum class ResourceType : uint8_t; +class InjectedScriptManager; } @@ -4207,7 +4275,7 @@ index 97c3cf09961f5c5ea7ede70d35755639fc66a3df..d5aabfbb3062f6037415bb3e136d3d7c class InspectorOverlay; class LocalFrame; class Page; -@@ -68,6 +71,9 @@ public: +@@ -69,6 +72,9 @@ public: InspectorPageAgent(PageAgentContext&, InspectorBackendClient*, InspectorOverlay&); ~InspectorPageAgent(); @@ -4217,7 +4285,7 @@ index 97c3cf09961f5c5ea7ede70d35755639fc66a3df..d5aabfbb3062f6037415bb3e136d3d7c // InspectorAgentBase void didCreateFrontendAndBackend(); void willDestroyFrontendAndBackend(Inspector::DisconnectReason); -@@ -76,7 +82,10 @@ public: +@@ -77,7 +83,10 @@ public: Inspector::Protocol::ErrorStringOr enable(); Inspector::Protocol::ErrorStringOr disable(); Inspector::Protocol::ErrorStringOr reload(std::optional&& ignoreCache, std::optional&& revalidateAllResources); @@ -4228,7 +4296,7 @@ index 97c3cf09961f5c5ea7ede70d35755639fc66a3df..d5aabfbb3062f6037415bb3e136d3d7c Inspector::Protocol::ErrorStringOr overrideSetting(Inspector::Protocol::Page::Setting, std::optional&& value); Inspector::Protocol::ErrorStringOr overrideUserPreference(Inspector::Protocol::Page::UserPreferenceName, std::optional&&); Inspector::Protocol::ErrorStringOr>> getCookies(); -@@ -92,41 +101,60 @@ public: +@@ -93,41 +102,60 @@ public: #endif Inspector::Protocol::ErrorStringOr setShowPaintRects(bool); Inspector::Protocol::ErrorStringOr setEmulatedMedia(const String&); @@ -4237,7 +4305,7 @@ index 97c3cf09961f5c5ea7ede70d35755639fc66a3df..d5aabfbb3062f6037415bb3e136d3d7c + Inspector::Protocol::ErrorStringOr setTouchEmulationEnabled(bool); Inspector::Protocol::ErrorStringOr snapshotNode(Inspector::Protocol::DOM::NodeId); - Inspector::Protocol::ErrorStringOr snapshotRect(int x, int y, int width, int height, Inspector::Protocol::Page::CoordinateSystem); -+ Inspector::Protocol::ErrorStringOr snapshotRect(int x, int y, int width, int height, Inspector::Protocol::Page::CoordinateSystem, std::optional&& omitDeviceScaleFactor); ++ Inspector::Protocol::ErrorStringOr snapshotRect(int x, int y, int width, int height, Inspector::Protocol::Page::CoordinateSystem, std::optional&& omitDeviceScaleFactor, std::optional&& format, std::optional&& quality); #if ENABLE(WEB_ARCHIVE) && USE(CF) Inspector::Protocol::ErrorStringOr archive(); #endif @@ -4294,7 +4362,7 @@ index 97c3cf09961f5c5ea7ede70d35755639fc66a3df..d5aabfbb3062f6037415bb3e136d3d7c InspectorOverlay& NODELETE overlay() const; -@@ -141,14 +169,19 @@ private: +@@ -142,14 +170,19 @@ private: const Ref m_backendDispatcher; WeakRef m_inspectedPage; @@ -4313,9 +4381,70 @@ index 97c3cf09961f5c5ea7ede70d35755639fc66a3df..d5aabfbb3062f6037415bb3e136d3d7c + bool m_ignoreDidClearWindowObject { false }; }; + } // namespace WebCore +diff --git a/Source/WebCore/inspector/agents/frame/FrameDOMAgent.h b/Source/WebCore/inspector/agents/frame/FrameDOMAgent.h +index 5f424acadd1ed82e70947aec894134feedfc36b7..cb3e406cf2893c998d10d80a56b21752f0fabec1 100644 +--- a/Source/WebCore/inspector/agents/frame/FrameDOMAgent.h ++++ b/Source/WebCore/inspector/agents/frame/FrameDOMAgent.h +@@ -115,7 +115,7 @@ public: + Inspector::CommandResult showFlexOverlay(int nodeId, Ref&& flexOverlayConfig) override; + Inspector::CommandResult hideFlexOverlay(std::optional&& nodeId) override; + Inspector::CommandResult pushNodeByPathToFrontend(const String& path) override; +- Inspector::CommandResult> resolveNode(int nodeId, const String& objectGroup) override; ++ Inspector::CommandResult> resolveNode(std::optional&&, const String&, const String&, std::optional&&, const String&) override; + Inspector::CommandResult moveTo(int nodeId, int targetNodeId, std::optional&& insertBeforeNodeId) override; + Inspector::CommandResult undo() override; + Inspector::CommandResult redo() override; +@@ -124,6 +124,10 @@ public: + Inspector::CommandResult setInspectedNode(int nodeId) override; + Inspector::CommandResult setAllowEditingUserAgentShadowTrees(bool) override; + Inspector::CommandResult> getMediaStats(int nodeId) override; ++ Inspector::CommandResultOf describeNode(const String&) override; ++ Inspector::CommandResult scrollIntoViewIfNeeded(const String&, RefPtr&&) override; ++ Inspector::CommandResult>> getContentQuads(const String&) override; ++ void setInputFiles(const String&, Ref&&, Ref&&) override; + + // InspectorInstrumentation hooks + void didInsertDOMNode(Node&); +diff --git a/Source/WebCore/inspector/agents/frame/FrameDOMAgentStubs.cpp b/Source/WebCore/inspector/agents/frame/FrameDOMAgentStubs.cpp +index fbf7088fd151b7744e2e6f424b65b0bd513fb4f6..386232b2c6ad0e1bb7f894d91dae2547670b3e79 100644 +--- a/Source/WebCore/inspector/agents/frame/FrameDOMAgentStubs.cpp ++++ b/Source/WebCore/inspector/agents/frame/FrameDOMAgentStubs.cpp +@@ -196,7 +196,7 @@ Inspector::CommandResult FrameDOMAgent::hideFlexOverlay(std::optional + return makeUnexpected("Not supported for frame targets"_s); + } + +-Inspector::CommandResult> FrameDOMAgent::resolveNode(int, const String&) ++Inspector::CommandResult> FrameDOMAgent::resolveNode(std::optional&&, const String&, const String&, std::optional&&, const String&) + { + return makeUnexpected("Not yet implemented for frame targets"_s); + } +@@ -241,4 +241,23 @@ Inspector::CommandResult> FrameDOMAgen + return makeUnexpected("Not supported for frame targets"_s); + } + ++Inspector::CommandResultOf FrameDOMAgent::describeNode(const String&) ++{ ++ return makeUnexpected("Not supported for frame targets"_s); ++} ++ ++Inspector::CommandResult FrameDOMAgent::scrollIntoViewIfNeeded(const String&, RefPtr&&) ++{ ++ return makeUnexpected("Not supported for frame targets"_s); ++} ++ ++Inspector::CommandResult>> FrameDOMAgent::getContentQuads(const String&) ++{ ++ return makeUnexpected("Not supported for frame targets"_s); ++} ++ ++void FrameDOMAgent::setInputFiles(const String&, Ref&&, Ref&&) ++{ ++} ++ } // namespace WebCore diff --git a/Source/WebCore/inspector/agents/page/PageRuntimeAgent.cpp b/Source/WebCore/inspector/agents/page/PageRuntimeAgent.cpp -index e93e93df33a77cf1995340ff1375488efee0a9df..002c160eec98597f97d547f6f2722b7edb58be72 100644 +index d0c409f4832ea16ace7ee6c9f35ee06b0348c764..6d47d17cc4508d84bc3e4c5fa0885b8275732ca3 100644 --- a/Source/WebCore/inspector/agents/page/PageRuntimeAgent.cpp +++ b/Source/WebCore/inspector/agents/page/PageRuntimeAgent.cpp @@ -35,7 +35,9 @@ @@ -4328,15 +4457,15 @@ index e93e93df33a77cf1995340ff1375488efee0a9df..002c160eec98597f97d547f6f2722b7e #include "InstrumentingAgents.h" #include "JSDOMWindowCustom.h" #include "JSExecState.h" -@@ -43,6 +45,7 @@ - #include "Page.h" +@@ -44,6 +46,7 @@ #include "PageInspectorController.h" + #include "RuntimeAgentUtilities.h" #include "ScriptController.h" +#include "ScriptSourceCode.h" #include "SecurityOrigin.h" #include "UserGestureEmulationScope.h" #include -@@ -90,13 +93,74 @@ Inspector::Protocol::ErrorStringOr PageRuntimeAgent::disable() +@@ -91,13 +94,74 @@ Inspector::Protocol::ErrorStringOr PageRuntimeAgent::disable() { Ref { m_instrumentingAgents.get() }->setEnabledPageRuntimeAgent(nullptr); @@ -4411,7 +4540,7 @@ index e93e93df33a77cf1995340ff1375488efee0a9df..002c160eec98597f97d547f6f2722b7e } void PageRuntimeAgent::didClearWindowObjectInWorld(LocalFrame& frame, DOMWrapperWorld& world) -@@ -105,7 +169,29 @@ void PageRuntimeAgent::didClearWindowObjectInWorld(LocalFrame& frame, DOMWrapper +@@ -106,7 +170,29 @@ void PageRuntimeAgent::didClearWindowObjectInWorld(LocalFrame& frame, DOMWrapper if (frameId.isEmpty()) return; @@ -4441,7 +4570,7 @@ index e93e93df33a77cf1995340ff1375488efee0a9df..002c160eec98597f97d547f6f2722b7e } InjectedScript PageRuntimeAgent::injectedScriptForEval(Inspector::Protocol::ErrorString& errorString, std::optional&& executionContextId) -@@ -142,9 +228,6 @@ void PageRuntimeAgent::reportExecutionContextCreation() +@@ -143,9 +229,6 @@ void PageRuntimeAgent::reportExecutionContextCreation() Ref identifierRegistry = m_inspectedPage->inspectorController().identifierRegistry(); m_inspectedPage->forEachLocalFrame([&](LocalFrame& frame) { @@ -4508,10 +4637,10 @@ index c2f9d5d90ff590154ef39132453194c162be0cc5..ff36e34f9e5963f1deaa41341174a7fd protected: static SameSiteInfo sameSiteInfo(const Document&, IsForDOMCookieAccess = IsForDOMCookieAccess::No); diff --git a/Source/WebCore/loader/DocumentLoader.cpp b/Source/WebCore/loader/DocumentLoader.cpp -index d4925cee087ccd79875945bb2b222401cc390a53..c595fb4793b5b8d33a3ad0c659bdc27770d55c0c 100644 +index bd624699105ee090f2fbe6143a80f9682ee355a3..528d8f7c93dfe0a5b0c8603d4e538061f1f0d349 100644 --- a/Source/WebCore/loader/DocumentLoader.cpp +++ b/Source/WebCore/loader/DocumentLoader.cpp -@@ -787,8 +787,10 @@ void DocumentLoader::willSendRequest(ResourceRequest&& newRequest, const Resourc +@@ -775,8 +775,10 @@ void DocumentLoader::willSendRequest(ResourceRequest&& newRequest, const Resourc if (!didReceiveRedirectResponse) return completionHandler(WTF::move(newRequest)); @@ -4522,7 +4651,7 @@ index d4925cee087ccd79875945bb2b222401cc390a53..c595fb4793b5b8d33a3ad0c659bdc277 switch (navigationPolicyDecision) { case NavigationPolicyDecision::IgnoreLoad: case NavigationPolicyDecision::LoadWillContinueInAnotherProcess: -@@ -1597,11 +1599,17 @@ void DocumentLoader::detachFromFrame(LoadWillContinueInAnotherProcess loadWillCo +@@ -1586,11 +1588,17 @@ void DocumentLoader::detachFromFrame(LoadWillContinueInAnotherProcess loadWillCo if (auto navigationID = std::exchange(m_navigationID, { })) frame->loader().client().documentLoaderDetached(*navigationID, loadWillContinueInAnotherProcess); @@ -4543,10 +4672,10 @@ index d4925cee087ccd79875945bb2b222401cc390a53..c595fb4793b5b8d33a3ad0c659bdc277 { m_navigationID = navigationID; diff --git a/Source/WebCore/loader/DocumentLoader.h b/Source/WebCore/loader/DocumentLoader.h -index ffebadc8882ffd9a519fdaec8d42ac2909c5fe65..145e7ee21440d14da048d59f7c177c8d334e0ee3 100644 +index df3efd07ebcadc1eb790d1e909031407152902d1..33fd4709e5b3b3d9453d951e72c7907b273b8c76 100644 --- a/Source/WebCore/loader/DocumentLoader.h +++ b/Source/WebCore/loader/DocumentLoader.h -@@ -209,6 +209,8 @@ public: +@@ -210,6 +210,8 @@ public: WEBCORE_EXPORT virtual void detachFromFrame(LoadWillContinueInAnotherProcess); @@ -4556,10 +4685,10 @@ index ffebadc8882ffd9a519fdaec8d42ac2909c5fe65..145e7ee21440d14da048d59f7c177c8d WEBCORE_EXPORT SubresourceLoader* NODELETE mainResourceLoader() const; WEBCORE_EXPORT RefPtr mainResourceData() const; diff --git a/Source/WebCore/loader/FrameLoader.cpp b/Source/WebCore/loader/FrameLoader.cpp -index 994f22fa6190e271f9f9927caa6a331c355cc43e..3f469ef6abebeea6f7df6b9406c8c0e9cd5fe386 100644 +index d09aaee884faad69924de5db1e5515f8d21bb6d2..afa48d41bb19b9b3eeccaddbf3f1f88ee15e4bb0 100644 --- a/Source/WebCore/loader/FrameLoader.cpp +++ b/Source/WebCore/loader/FrameLoader.cpp -@@ -1385,6 +1385,7 @@ void FrameLoader::loadInSameDocument(URL url, RefPtr stat +@@ -1409,6 +1409,7 @@ void FrameLoader::loadInSameDocument(URL url, RefPtr stat } m_client->dispatchDidNavigateWithinPage(); @@ -4567,7 +4696,7 @@ index 994f22fa6190e271f9f9927caa6a331c355cc43e..3f469ef6abebeea6f7df6b9406c8c0e9 document->statePopped(stateObject ? stateObject.releaseNonNull() : SerializedScriptValue::nullValue()); m_client->dispatchDidPopStateWithinPage(); -@@ -1938,6 +1939,7 @@ void FrameLoader::loadWithDocumentLoader(DocumentLoader* loader, FrameLoadType t +@@ -1967,6 +1968,7 @@ void FrameLoader::loadWithDocumentLoader(DocumentLoader* loader, FrameLoadType t const String& httpMethod = loader->request().httpMethod(); if (shouldPerformFragmentNavigation(isFormSubmission, httpMethod, policyChecker().loadType(), newURL) && !loader->substituteData().isValid()) { @@ -4575,17 +4704,17 @@ index 994f22fa6190e271f9f9927caa6a331c355cc43e..3f469ef6abebeea6f7df6b9406c8c0e9 RefPtr oldDocumentLoader = m_documentLoader; NavigationAction action { protect(frame->document()).releaseNonNull(), loader->request(), InitiatedByMainFrame::Unknown, loader->isRequestFromClientOrUserInput(), policyChecker().loadType(), isFormSubmission }; -@@ -1977,7 +1979,9 @@ void FrameLoader::loadWithDocumentLoader(DocumentLoader* loader, FrameLoadType t +@@ -2008,7 +2010,9 @@ void FrameLoader::loadWithDocumentLoader(DocumentLoader* loader, FrameLoadType t auto policyDecisionMode = loader->triggeringAction().isFromNavigationAPI() ? PolicyDecisionMode::Synchronous : PolicyDecisionMode::Asynchronous; RELEASE_ASSERT(!isBackForwardLoadType(policyChecker().loadType()) || history().provisionalItem()); + InspectorInstrumentation::willCheckNavigationPolicy(m_frame); - policyChecker().checkNavigationPolicy(ResourceRequest(loader->request()), ResourceResponse { } /* redirectResponse */, loader, WTF::move(formSubmission), [this, protectedThis = Ref { *this }, allowNavigationToInvalidURL, completionHandler = completionHandlerCaller.release()] (const ResourceRequest& request, WeakPtr&& weakFormSubmission, NavigationPolicyDecision navigationPolicyDecision) mutable { + policyChecker().checkNavigationPolicy(ResourceRequest(loader->request()), ResourceResponse { } /* redirectResponse */, loader, WTF::move(formSubmission), [this, protectedThis = Ref { *this }, allowNavigationToInvalidURL, shouldRestoreFromBackForwardCache, completionHandler = completionHandlerCaller.release()] (const ResourceRequest& request, WeakPtr&& weakFormSubmission, NavigationPolicyDecision navigationPolicyDecision) mutable { + InspectorInstrumentation::didCheckNavigationPolicy(m_frame, navigationPolicyDecision != NavigationPolicyDecision::ContinueLoad); - continueLoadAfterNavigationPolicy(request, RefPtr { weakFormSubmission.get() }.get(), navigationPolicyDecision, allowNavigationToInvalidURL); + continueLoadAfterNavigationPolicy(request, RefPtr { weakFormSubmission.get() }.get(), navigationPolicyDecision, allowNavigationToInvalidURL, shouldRestoreFromBackForwardCache); completionHandler(); }, policyDecisionMode, determineNavigationType(type, NavigationHistoryBehavior::Auto)); -@@ -3324,10 +3328,15 @@ String FrameLoader::userAgent(const URL& url) const +@@ -3383,10 +3387,15 @@ String FrameLoader::userAgent(const URL& url) const String FrameLoader::navigatorPlatform() const { @@ -4603,7 +4732,7 @@ index 994f22fa6190e271f9f9927caa6a331c355cc43e..3f469ef6abebeea6f7df6b9406c8c0e9 } void FrameLoader::dispatchOnloadEvents() -@@ -3785,6 +3794,8 @@ void FrameLoader::receivedMainResourceError(const ResourceError& error, LoadWill +@@ -3844,6 +3853,8 @@ void FrameLoader::receivedMainResourceError(const ResourceError& error, LoadWill } if (frame->page()) checkLoadComplete(loadWillContinueInAnotherProcess); @@ -4612,7 +4741,7 @@ index 994f22fa6190e271f9f9927caa6a331c355cc43e..3f469ef6abebeea6f7df6b9406c8c0e9 } void FrameLoader::continueFragmentScrollAfterNavigationPolicy(const ResourceRequest& request, const SecurityOrigin* requesterOrigin, bool shouldContinue, NavigationHistoryBehavior historyHandling) -@@ -4789,9 +4800,6 @@ String FrameLoader::referrer() const +@@ -4853,9 +4864,6 @@ String FrameLoader::referrer() const void FrameLoader::dispatchDidClearWindowObjectsInAllWorlds() { @@ -4622,7 +4751,7 @@ index 994f22fa6190e271f9f9927caa6a331c355cc43e..3f469ef6abebeea6f7df6b9406c8c0e9 Vector> worlds; ScriptController::getAllWorlds(worlds); for (auto& world : worlds) -@@ -4801,13 +4809,12 @@ void FrameLoader::dispatchDidClearWindowObjectsInAllWorlds() +@@ -4865,13 +4873,12 @@ void FrameLoader::dispatchDidClearWindowObjectsInAllWorlds() void FrameLoader::dispatchDidClearWindowObjectInWorld(DOMWrapperWorld& world) { Ref frame = m_frame.get(); @@ -4677,10 +4806,10 @@ index 3acb026789c46395be3f8cd6b837ebf207e215f7..a4e3c1e69fbbb108badd23f32e879423 } diff --git a/Source/WebCore/loader/cache/CachedResourceLoader.cpp b/Source/WebCore/loader/cache/CachedResourceLoader.cpp -index 01466dae0f43c53e7e50dad34c73731fbc8240d6..f37b4130add559d50a869ab494a3f7fb99b275d1 100644 +index 7be02354989d0b0d52732318c676313d01ca83a4..c568730f75b62fdeff861a443c8a9cc5557ac9c4 100644 --- a/Source/WebCore/loader/cache/CachedResourceLoader.cpp +++ b/Source/WebCore/loader/cache/CachedResourceLoader.cpp -@@ -1159,8 +1159,11 @@ ResourceErrorOr> CachedResourceLoader::requestResource(Cache +@@ -1172,8 +1172,11 @@ ResourceErrorOr> CachedResourceLoader::requestResource(Cache request.updateReferrerPolicy(document ? document->referrerPolicy() : ReferrerPolicy::Default); @@ -4694,7 +4823,7 @@ index 01466dae0f43c53e7e50dad34c73731fbc8240d6..f37b4130add559d50a869ab494a3f7fb if (RefPtr documentLoader = m_documentLoader) { bool madeHTTPS { request.resourceRequest().wasSchemeOptimisticallyUpgraded() }; -@@ -1817,8 +1820,9 @@ Vector> CachedResourceLoader::allCachedSVGImages() const +@@ -1830,8 +1833,9 @@ Vector> CachedResourceLoader::allCachedSVGImages() const ResourceErrorOr> CachedResourceLoader::preload(CachedResource::Type type, CachedResourceRequest&& request) { @@ -4707,10 +4836,10 @@ index 01466dae0f43c53e7e50dad34c73731fbc8240d6..f37b4130add559d50a869ab494a3f7fb RefPtr document = m_document; ASSERT(document); diff --git a/Source/WebCore/page/ChromeClient.h b/Source/WebCore/page/ChromeClient.h -index 1a4fae919b725be107c91cde64f8ccfa30b1e839..2687ebc237823715ebfa06440d54e9dd6dbddd8e 100644 +index 86a65aa2b510255e130013985c3c1de48c4ddf8e..8868f815f521424b47b349b2d65856b58ed9aabc 100644 --- a/Source/WebCore/page/ChromeClient.h +++ b/Source/WebCore/page/ChromeClient.h -@@ -408,7 +408,7 @@ public: +@@ -409,7 +409,7 @@ public: #endif #if ENABLE(ORIENTATION_EVENTS) @@ -4720,10 +4849,10 @@ index 1a4fae919b725be107c91cde64f8ccfa30b1e839..2687ebc237823715ebfa06440d54e9dd virtual RefPtr createColorChooser(ColorChooserClient&, const Color&) = 0; diff --git a/Source/WebCore/page/EventHandler.cpp b/Source/WebCore/page/EventHandler.cpp -index c3b1e5711e275b22c4237f46fa307072f34edefb..28f2f4ea96febba6bf4c6c8b729e18d58cb2e141 100644 +index 91ce2cf2c5d4da9b816d73a5132c80536946a9d2..8966fd4fda62a813c79d31231d8a7963aca05cde 100644 --- a/Source/WebCore/page/EventHandler.cpp +++ b/Source/WebCore/page/EventHandler.cpp -@@ -4836,6 +4836,12 @@ bool EventHandler::handleDrag(const MouseEventWithHitTestResults& event, CheckDr +@@ -4832,6 +4832,12 @@ bool EventHandler::handleDrag(const MouseEventWithHitTestResults& event, CheckDr if (!document) return false; @@ -4736,7 +4865,7 @@ index c3b1e5711e275b22c4237f46fa307072f34edefb..28f2f4ea96febba6bf4c6c8b729e18d5 dragState().dataTransfer = DataTransfer::createForDrag(*document); auto hasNonDefaultPasteboardData = HasNonDefaultPasteboardData::No; -@@ -5440,6 +5446,7 @@ static HitTestResult hitTestResultInFrame(LocalFrame* frame, const LayoutPoint& +@@ -5436,6 +5442,7 @@ static HitTestResult hitTestResultInFrame(LocalFrame* frame, const LayoutPoint& return result; } @@ -4744,7 +4873,7 @@ index c3b1e5711e275b22c4237f46fa307072f34edefb..28f2f4ea96febba6bf4c6c8b729e18d5 Expected EventHandler::handleTouchEvent(const PlatformTouchEvent& event) { Ref frame = m_frame.get(); -@@ -5571,7 +5578,7 @@ Expected EventHandler::handleTouchEvent(co +@@ -5567,7 +5574,7 @@ Expected EventHandler::handleTouchEvent(co if (!targetFrame) continue; @@ -4753,7 +4882,7 @@ index c3b1e5711e275b22c4237f46fa307072f34edefb..28f2f4ea96febba6bf4c6c8b729e18d5 RefPtr pointerTarget = touchTarget; if (pointState != PlatformTouchPoint::TouchPressed) { -@@ -5667,6 +5674,7 @@ Expected EventHandler::handleTouchEvent(co +@@ -5663,6 +5670,7 @@ Expected EventHandler::handleTouchEvent(co return swallowedEvent; } @@ -4783,10 +4912,10 @@ index 2afe4760de2da985bb31273ef3deb6d927093b80..54539f5b9779e3cffa6d7737c4649b88 } diff --git a/Source/WebCore/page/FrameConsoleClient.cpp b/Source/WebCore/page/FrameConsoleClient.cpp -index 23e6d8380ec1e3488bc746887ea06be8eed79fb9..7a1b91c923530472bf1bd613a237dd5a8739b88f 100644 +index bc7d6367981561914f4e0acbd2f9efabce6eb42f..4802a6d571a9f5d2fa032dbb5b61abce46d0209c 100644 --- a/Source/WebCore/page/FrameConsoleClient.cpp +++ b/Source/WebCore/page/FrameConsoleClient.cpp -@@ -478,4 +478,12 @@ void FrameConsoleClient::screenshot(JSC::JSGlobalObject* lexicalGlobalObject, Re +@@ -476,4 +476,12 @@ void FrameConsoleClient::screenshot(JSC::JSGlobalObject* lexicalGlobalObject, Re addMessage(makeUnique(MessageSource::ConsoleAPI, MessageType::Image, MessageLevel::Log, dataURL, ScriptArguments::create(lexicalGlobalObject, WTF::move(adjustedArguments)), lexicalGlobalObject, /* requestIdentifier */ 0, timestamp)); } @@ -4812,10 +4941,10 @@ index 7a6671ff0da0bbec833318a1c37e6280ad9f4255..5a16fa04c515f0c3842d1187eeacc487 WeakRef m_frame; }; diff --git a/Source/WebCore/page/FrameSnapshotting.cpp b/Source/WebCore/page/FrameSnapshotting.cpp -index dd21eb7cd8958c99a153769d26e03a8eb5bed0fb..a94d8b5d2eced11e03f963d3848159914d554ae3 100644 +index 2ab269da2d54786965489c0a07596be3e8784f3c..f0a2cb19308288cec3178a265c7da8cb12b73ace 100644 --- a/Source/WebCore/page/FrameSnapshotting.cpp +++ b/Source/WebCore/page/FrameSnapshotting.cpp -@@ -121,7 +121,7 @@ RefPtr snapshotFrameRectWithClip(LocalFrame& frame, const IntRect& +@@ -123,7 +123,7 @@ RefPtr snapshotFrameRectWithClip(LocalFrame& frame, const IntRect& // Other paint behaviors are set by paintContentsForSnapshot. frame.view()->setPaintBehavior(paintBehavior); @@ -4824,7 +4953,7 @@ index dd21eb7cd8958c99a153769d26e03a8eb5bed0fb..a94d8b5d2eced11e03f963d384815991 if (options.flags.contains(SnapshotFlags::PaintWith3xBaseScale)) scaleFactor = 3; -@@ -140,6 +140,8 @@ RefPtr snapshotFrameRectWithClip(LocalFrame& frame, const IntRect& +@@ -142,6 +142,8 @@ RefPtr snapshotFrameRectWithClip(LocalFrame& frame, const IntRect& return nullptr; buffer->context().translate(-imageRect.location()); @@ -4833,7 +4962,7 @@ index dd21eb7cd8958c99a153769d26e03a8eb5bed0fb..a94d8b5d2eced11e03f963d384815991 if (!clipRects.isEmpty()) { Path clipPath; -@@ -148,7 +150,10 @@ RefPtr snapshotFrameRectWithClip(LocalFrame& frame, const IntRect& +@@ -150,7 +152,10 @@ RefPtr snapshotFrameRectWithClip(LocalFrame& frame, const IntRect& buffer->context().clipPath(clipPath); } @@ -4846,19 +4975,28 @@ index dd21eb7cd8958c99a153769d26e03a8eb5bed0fb..a94d8b5d2eced11e03f963d384815991 } diff --git a/Source/WebCore/page/FrameSnapshotting.h b/Source/WebCore/page/FrameSnapshotting.h -index c34982712f798dff6c484e4a4b6b6a9b23905d4a..52da92d68344cd4d734db4565f174bbd05e26ad9 100644 +index 5126a48555a951dd4c705850f619fb6730166eb6..c043cf360a488ad41db6b457baaa5634712fff35 100644 --- a/Source/WebCore/page/FrameSnapshotting.h +++ b/Source/WebCore/page/FrameSnapshotting.h -@@ -60,6 +60,7 @@ enum class SnapshotFlags : uint16_t { - FixedAndStickyLayersOnly = 1 << 12, +@@ -44,7 +44,7 @@ class ImageBuffer; + class LocalFrame; + class Node; + +-enum class SnapshotFlags : uint16_t { ++enum class SnapshotFlags : uint32_t { + ExcludeSelectionHighlighting = 1 << 0, + PaintSelectionOnly = 1 << 1, + InViewCoordinates = 1 << 2, +@@ -61,6 +61,7 @@ enum class SnapshotFlags : uint16_t { DraggableElement = 1 << 13, IncludeDocumentMarkers = 1 << 14, -+ OmitDeviceScaleFactor = 1 << 15, + FastAndLowQualityFilters = 1 << 15, ++ OmitDeviceScaleFactor = 1 << 16, }; struct SnapshotOptions { diff --git a/Source/WebCore/page/History.cpp b/Source/WebCore/page/History.cpp -index 6cde8d25af42de8e96f6aa23dbeedb5abaa7feab..bef6092300de6f6d33c5a9e2a9354e00c200d9ef 100644 +index 4e43a48341c52ac028f48dfdf8620fa75f9eb0d1..bb23998151b69451ff749e0c39c739bc7104f0a5 100644 --- a/Source/WebCore/page/History.cpp +++ b/Source/WebCore/page/History.cpp @@ -35,6 +35,7 @@ @@ -4888,7 +5026,7 @@ index 6cde8d25af42de8e96f6aa23dbeedb5abaa7feab..bef6092300de6f6d33c5a9e2a9354e00 } diff --git a/Source/WebCore/page/LocalFrame.cpp b/Source/WebCore/page/LocalFrame.cpp -index cc2cb6b9918d4059514fb82b124eee533473b75e..dbe0d101ee889090bdfe473268a52fca6818ea6d 100644 +index 80ca45ad29bef4523c42bcc66d4f06083a58b53b..28af933cf7af65579046979b19e19f157dc09d21 100644 --- a/Source/WebCore/page/LocalFrame.cpp +++ b/Source/WebCore/page/LocalFrame.cpp @@ -93,6 +93,7 @@ @@ -4899,7 +5037,7 @@ index cc2cb6b9918d4059514fb82b124eee533473b75e..dbe0d101ee889090bdfe473268a52fca #include "NodeTraversal.h" #include "Page.h" #include "PaymentSession.h" -@@ -227,6 +228,7 @@ LocalFrame::LocalFrame(Page& page, ClientCreator&& clientCreator, FrameIdentifie +@@ -229,6 +230,7 @@ LocalFrame::LocalFrame(Page& page, ClientCreator&& clientCreator, FrameIdentifie void LocalFrame::init() { @@ -4907,7 +5045,7 @@ index cc2cb6b9918d4059514fb82b124eee533473b75e..dbe0d101ee889090bdfe473268a52fca loader().init(); } -@@ -467,7 +469,7 @@ void LocalFrame::orientationChanged() +@@ -462,7 +464,7 @@ void LocalFrame::orientationChanged() IntDegrees LocalFrame::orientation() const { if (RefPtr page = this->page()) @@ -4916,7 +5054,7 @@ index cc2cb6b9918d4059514fb82b124eee533473b75e..dbe0d101ee889090bdfe473268a52fca return 0; } #endif // ENABLE(ORIENTATION_EVENTS) -@@ -1677,7 +1679,6 @@ String LocalFrame::frameURLProtocol() const +@@ -1679,7 +1681,6 @@ String LocalFrame::frameURLProtocol() const return ""_s; } @@ -4924,7 +5062,7 @@ index cc2cb6b9918d4059514fb82b124eee533473b75e..dbe0d101ee889090bdfe473268a52fca static bool nodeIsMouseFocusable(Node& node) { -@@ -1913,7 +1914,7 @@ RefPtr LocalFrame::nodeRespondingToDoubleClickEvent(const FloatPoint& view +@@ -1915,7 +1916,7 @@ RefPtr LocalFrame::nodeRespondingToDoubleClickEvent(const FloatPoint& view for (; node && node != terminationNode; node = node->parentInComposedTree()) { if (!node->hasEventListeners(eventNames().dblclickEvent)) continue; @@ -4933,7 +5071,7 @@ index cc2cb6b9918d4059514fb82b124eee533473b75e..dbe0d101ee889090bdfe473268a52fca if (!node->allowsDoubleTapGesture()) continue; #endif -@@ -1927,7 +1928,6 @@ RefPtr LocalFrame::nodeRespondingToDoubleClickEvent(const FloatPoint& view +@@ -1929,7 +1930,6 @@ RefPtr LocalFrame::nodeRespondingToDoubleClickEvent(const FloatPoint& view return qualifyingNodeAtViewportLocation(viewportLocation, adjustedViewportLocation, WTF::move(ancestorRespondingToDoubleClickEvent), ShouldApproximate::Yes); } @@ -4942,7 +5080,7 @@ index cc2cb6b9918d4059514fb82b124eee533473b75e..dbe0d101ee889090bdfe473268a52fca } // namespace WebCore diff --git a/Source/WebCore/page/LocalFrame.h b/Source/WebCore/page/LocalFrame.h -index 8f81b0ea3fc343e6c214ad566ab4a6d9b95f5c3b..53ac6ea36a6fb13debd0f029f78c4dc4037d24f2 100644 +index 04ba5a1ede9495bad18c532676b2121c09c407d9..614217e465ff12a26f35b69f3f82e8b6d0ce06b4 100644 --- a/Source/WebCore/page/LocalFrame.h +++ b/Source/WebCore/page/LocalFrame.h @@ -29,6 +29,7 @@ @@ -4952,8 +5090,8 @@ index 8f81b0ea3fc343e6c214ad566ab4a6d9b95f5c3b..53ac6ea36a6fb13debd0f029f78c4dc4 +#include #include #include - #include -@@ -129,9 +130,7 @@ enum { + #include +@@ -127,9 +128,7 @@ enum { enum OverflowScrollAction { DoNotPerformOverflowScroll, PerformOverflowScroll }; #endif @@ -4963,7 +5101,7 @@ index 8f81b0ea3fc343e6c214ad566ab4a6d9b95f5c3b..53ac6ea36a6fb13debd0f029f78c4dc4 class LocalFrame final : public Frame { public: -@@ -232,7 +231,6 @@ public: +@@ -230,7 +229,6 @@ public: WEBCORE_EXPORT DataDetectionResultsStorage& dataDetectionResults() LIFETIME_BOUND; #endif @@ -4971,7 +5109,7 @@ index 8f81b0ea3fc343e6c214ad566ab4a6d9b95f5c3b..53ac6ea36a6fb13debd0f029f78c4dc4 RefPtr betterApproximateNode(const IntPoint& testPoint, const NodeQualifier&, Node* best, Node* failedNode, IntPoint& bestPoint, IntRect& bestRect, const IntRect& testRect); WEBCORE_EXPORT RefPtr nodeRespondingToInteraction(const FloatPoint& viewportLocation, FloatPoint& adjustedViewportLocation); -@@ -246,7 +244,6 @@ public: +@@ -244,7 +242,6 @@ public: WEBCORE_EXPORT RefPtr nodeRespondingToDoubleClickEvent(const FloatPoint& viewportLocation, FloatPoint& adjustedViewportLocation); static bool nodeWillRespondToMouseEvents(Node&); @@ -4979,7 +5117,7 @@ index 8f81b0ea3fc343e6c214ad566ab4a6d9b95f5c3b..53ac6ea36a6fb13debd0f029f78c4dc4 #if PLATFORM(IOS_FAMILY) const ViewportArguments& viewportArguments() const LIFETIME_BOUND; -@@ -324,6 +321,7 @@ public: +@@ -322,6 +319,7 @@ public: WEBCORE_EXPORT FloatSize screenSize() const; void setOverrideScreenSize(FloatSize&&); @@ -4987,11 +5125,41 @@ index 8f81b0ea3fc343e6c214ad566ab4a6d9b95f5c3b..53ac6ea36a6fb13debd0f029f78c4dc4 void NODELETE selfOnlyRef(); void selfOnlyDeref(); +diff --git a/Source/WebCore/page/Navigation.cpp b/Source/WebCore/page/Navigation.cpp +index 11edcb95f3bab26b3e1f6ab14a38f058098eac49..2d80fdee2e0df781271c56a7ed8d96f9c0d1532f 100644 +--- a/Source/WebCore/page/Navigation.cpp ++++ b/Source/WebCore/page/Navigation.cpp +@@ -47,6 +47,7 @@ + #include "HTMLFormElement.h" + #include "HistoryController.h" + #include "HistoryItem.h" ++#include "InspectorInstrumentation.h" + #include "JSDOMConvertAny.h" + #include "JSDOMConvertInterface.h" + #include "JSDOMGlobalObject.h" +@@ -1147,6 +1148,8 @@ void Navigation::setupInterceptionState(NavigateEvent& event, NavigationNavigati + // Only notify committed now if there are no handlers to wait for + auto shouldNotifyCommited = event.handlers().isEmpty() ? ShouldNotifyCommitted::Yes : ShouldNotifyCommitted::No; + updateForNavigation(entry->associatedHistoryItem(), navigationType, ShouldCopyStateObjectFromCurrentEntry::No, shouldNotifyCommited); ++ ++ InspectorInstrumentation::didNavigateWithinPage(*frame()); + } + } + } else if (navigationType == NavigationNavigationType::Reload) { +@@ -1155,6 +1158,8 @@ void Navigation::setupInterceptionState(NavigateEvent& event, NavigationNavigati + } else if (navigationType == NavigationNavigationType::Push || navigationType == NavigationNavigationType::Replace) { + auto historyHandling = navigationType == NavigationNavigationType::Replace ? NavigationHistoryBehavior::Replace : NavigationHistoryBehavior::Push; + frame()->loader().updateURLAndHistory(destination.url(), classicHistoryAPIState, historyHandling); ++ ++ InspectorInstrumentation::didNavigateWithinPage(*frame()); + } + } + diff --git a/Source/WebCore/page/Page.cpp b/Source/WebCore/page/Page.cpp -index 4431c7101d9f7637b951eaa80c1b68fa68ce0861..2b76f240f8e49c798dad3593ab4c3c9668e57241 100644 +index a523afe99c8dc4fc5925f2151500bab5eea67dfd..0a8ad0c18660bf9f41a04f662132221e06f634d5 100644 --- a/Source/WebCore/page/Page.cpp +++ b/Source/WebCore/page/Page.cpp -@@ -697,6 +697,44 @@ void Page::setOverrideViewportArguments(const std::optional& +@@ -699,6 +699,44 @@ void Page::setOverrideViewportArguments(const std::optional& localTopDocument->updateViewportArguments(); } @@ -5036,7 +5204,7 @@ index 4431c7101d9f7637b951eaa80c1b68fa68ce0861..2b76f240f8e49c798dad3593ab4c3c96 ScrollingCoordinator* Page::scrollingCoordinator() { if (!m_scrollingCoordinator && m_settings->scrollingCoordinatorEnabled()) { -@@ -4411,6 +4449,26 @@ void Page::setUseDarkAppearanceOverride(std::optional valueOverride) +@@ -4397,6 +4435,26 @@ void Page::setUseDarkAppearanceOverride(std::optional valueOverride) appearanceDidChange(); } @@ -5064,10 +5232,10 @@ index 4431c7101d9f7637b951eaa80c1b68fa68ce0861..2b76f240f8e49c798dad3593ab4c3c96 { if (insets == m_fullscreenInsets) diff --git a/Source/WebCore/page/Page.h b/Source/WebCore/page/Page.h -index 5bfae7e81b70cbd14f12e307bebd5b551f10c18b..ef95a268fd4aeb4ff5a325517386716ab17f257c 100644 +index f3d16a786093631e5eabe981478aeb66e14f2d32..ce7a7cae69455f79057bb0fd671c901939152c8f 100644 --- a/Source/WebCore/page/Page.h +++ b/Source/WebCore/page/Page.h -@@ -413,6 +413,9 @@ public: +@@ -410,6 +410,9 @@ public: const ViewportArguments* overrideViewportArguments() const LIFETIME_BOUND { return m_overrideViewportArguments.get(); } WEBCORE_EXPORT void setOverrideViewportArguments(const std::optional&); @@ -5077,7 +5245,7 @@ index 5bfae7e81b70cbd14f12e307bebd5b551f10c18b..ef95a268fd4aeb4ff5a325517386716a static void refreshPlugins(bool reload); WEBCORE_EXPORT PluginData& pluginData(); void clearPluginData(); -@@ -498,6 +501,10 @@ public: +@@ -496,6 +499,10 @@ public: #if ENABLE(DRAG_SUPPORT) DragController& dragController() LIFETIME_BOUND { return m_dragController.get(); } const DragController& dragController() const LIFETIME_BOUND { return m_dragController.get(); } @@ -5088,7 +5256,7 @@ index 5bfae7e81b70cbd14f12e307bebd5b551f10c18b..ef95a268fd4aeb4ff5a325517386716a #endif FocusController& focusController() const { return m_focusController; } #if ENABLE(CONTEXT_MENUS) -@@ -687,6 +694,10 @@ public: +@@ -685,6 +692,10 @@ public: WEBCORE_EXPORT void setUseColorAppearance(bool useDarkAppearance, bool useElevatedUserInterfaceLevel); bool defaultUseDarkAppearance() const { return m_useDarkAppearance; } void setUseDarkAppearanceOverride(std::optional); @@ -5111,7 +5279,7 @@ index 5bfae7e81b70cbd14f12e307bebd5b551f10c18b..ef95a268fd4aeb4ff5a325517386716a #if ENABLE(DEVICE_ORIENTATION) && PLATFORM(IOS_FAMILY) DeviceOrientationUpdateProvider* deviceOrientationUpdateProvider() const { return m_deviceOrientationUpdateProvider.get(); } #endif -@@ -1484,6 +1500,9 @@ private: +@@ -1490,6 +1506,9 @@ private: #if ENABLE(DRAG_SUPPORT) const UniqueRef m_dragController; @@ -5121,7 +5289,7 @@ index 5bfae7e81b70cbd14f12e307bebd5b551f10c18b..ef95a268fd4aeb4ff5a325517386716a #endif const UniqueRef m_focusController; #if ENABLE(CONTEXT_MENUS) -@@ -1562,6 +1581,8 @@ private: +@@ -1567,6 +1586,8 @@ private: bool m_useElevatedUserInterfaceLevel { false }; bool m_useDarkAppearance { false }; std::optional m_useDarkAppearanceOverride; @@ -5130,7 +5298,7 @@ index 5bfae7e81b70cbd14f12e307bebd5b551f10c18b..ef95a268fd4aeb4ff5a325517386716a #if ENABLE(TEXT_AUTOSIZING) float m_textAutosizingWidth { 0 }; -@@ -1739,6 +1760,11 @@ private: +@@ -1745,6 +1766,11 @@ private: #endif std::unique_ptr m_overrideViewportArguments; @@ -5143,7 +5311,7 @@ index 5bfae7e81b70cbd14f12e307bebd5b551f10c18b..ef95a268fd4aeb4ff5a325517386716a #if ENABLE(DEVICE_ORIENTATION) && PLATFORM(IOS_FAMILY) RefPtr m_deviceOrientationUpdateProvider; diff --git a/Source/WebCore/page/PointerCaptureController.cpp b/Source/WebCore/page/PointerCaptureController.cpp -index e9fa28ab43ecb4f3139791eca2bbb7bd82cfd6e0..8bbb8c76d913c37b60472972eb1647c58622c5a7 100644 +index ca28b7db28b39b711fa9c0a537fa8bb0e22187b0..3f36bd4c2e9c4e860be6cf1717161456df821057 100644 --- a/Source/WebCore/page/PointerCaptureController.cpp +++ b/Source/WebCore/page/PointerCaptureController.cpp @@ -213,7 +213,7 @@ bool PointerCaptureController::preventsCompatibilityMouseEventsForIdentifier(Poi @@ -5155,7 +5323,7 @@ index e9fa28ab43ecb4f3139791eca2bbb7bd82cfd6e0..8bbb8c76d913c37b60472972eb1647c5 static bool hierarchyHasCapturingEventListeners(Element* target, const AtomString& eventName) { for (RefPtr currentNode = target; currentNode; currentNode = currentNode->parentInComposedTree()) { -@@ -574,7 +574,7 @@ void PointerCaptureController::cancelPointer(PointerID pointerId, const IntPoint +@@ -571,7 +571,7 @@ void PointerCaptureController::cancelPointer(PointerID pointerId, const IntPoint capturingData->pendingTargetOverride = nullptr; capturingData->state = CapturingData::State::Cancelled; @@ -5165,7 +5333,7 @@ index e9fa28ab43ecb4f3139791eca2bbb7bd82cfd6e0..8bbb8c76d913c37b60472972eb1647c5 #endif diff --git a/Source/WebCore/page/PointerCaptureController.h b/Source/WebCore/page/PointerCaptureController.h -index bafa2e9995a142e1bc07d506204ff5f14cbeab9c..d06922b691b02699ca89483c6e4e5c43bb49303c 100644 +index 118fc0cbab5550f3aefa01d3c2b9dcd6a57033df..2f4ffa4b957d21ea3b8c0c31f34074e979adf619 100644 --- a/Source/WebCore/page/PointerCaptureController.h +++ b/Source/WebCore/page/PointerCaptureController.h @@ -63,7 +63,7 @@ public: @@ -5177,7 +5345,7 @@ index bafa2e9995a142e1bc07d506204ff5f14cbeab9c..d06922b691b02699ca89483c6e4e5c43 void dispatchEventForTouchAtIndex(EventTarget&, const PlatformTouchEvent&, unsigned, bool isPrimary, WindowProxy&, const DoublePoint&); #endif -@@ -91,12 +91,12 @@ private: +@@ -88,12 +88,12 @@ private: WeakPtr activeDocument; RefPtr pendingTargetOverride; RefPtr targetOverride; @@ -5237,10 +5405,10 @@ index cd1f3ce09ba0ab6e414bfcb12f38918404946700..f78cd1f89fb32ce4b9109c80f1f7c303 } diff --git a/Source/WebCore/page/csp/ContentSecurityPolicy.cpp b/Source/WebCore/page/csp/ContentSecurityPolicy.cpp -index cb4f081221a783cb05b6f65dba5cf338600148d5..88c4adcd84270f24bc4394125d44f34d7b2011c6 100644 +index d79832247cdbc228c21513ee2d76bec47f5d8ff8..cea351e8b6b8016697702967f3bdab8e17ea7f2d 100644 --- a/Source/WebCore/page/csp/ContentSecurityPolicy.cpp +++ b/Source/WebCore/page/csp/ContentSecurityPolicy.cpp -@@ -352,6 +352,8 @@ template +@@ -360,6 +360,8 @@ template bool ContentSecurityPolicy::allPoliciesWithDispositionAllow(Disposition disposition, Predicate&& predicate, Args&&... args) const requires (!std::is_convertible_v) { @@ -5249,7 +5417,7 @@ index cb4f081221a783cb05b6f65dba5cf338600148d5..88c4adcd84270f24bc4394125d44f34d bool isReportOnly = disposition == ContentSecurityPolicy::Disposition::ReportOnly; for (auto& policy : m_policies) { if (policy->isReportOnly() != isReportOnly) -@@ -365,6 +367,8 @@ bool ContentSecurityPolicy::allPoliciesWithDispositionAllow(Disposition disposit +@@ -373,6 +375,8 @@ bool ContentSecurityPolicy::allPoliciesWithDispositionAllow(Disposition disposit template bool ContentSecurityPolicy::allPoliciesWithDispositionAllow(Disposition disposition, ViolatedDirectiveCallback&& callback, Predicate&& predicate, Args&&... args) const { @@ -5258,7 +5426,7 @@ index cb4f081221a783cb05b6f65dba5cf338600148d5..88c4adcd84270f24bc4394125d44f34d bool isReportOnly = disposition == ContentSecurityPolicy::Disposition::ReportOnly; bool isAllowed = true; for (auto& policy : m_policies) { -@@ -381,6 +385,8 @@ bool ContentSecurityPolicy::allPoliciesWithDispositionAllow(Disposition disposit +@@ -389,6 +393,8 @@ bool ContentSecurityPolicy::allPoliciesWithDispositionAllow(Disposition disposit template bool ContentSecurityPolicy::allPoliciesAllow(NOESCAPE const ViolatedDirectiveCallback& callback, Predicate&& predicate, Args&&... args) const { @@ -5380,7 +5548,7 @@ index 9e2964b6e5f27f3a9c9f39e95b37de574cd52b1b..20e211ebc0217e6f4e66118fd98a7eb4 bool m_disallowFileAccess { false }; }; diff --git a/Source/WebCore/platform/Pasteboard.h b/Source/WebCore/platform/Pasteboard.h -index e4117f5fd101b2eb995fde1f27b1201827b894ea..1dcd26ce5381e0703b25cbb90f4104f9d1dc1eba 100644 +index d9385d5b78da7bc44fca6de52ff68f73726cd80b..81e810655b1b5e1ee1218e5606cc58ba34a57ed3 100644 --- a/Source/WebCore/platform/Pasteboard.h +++ b/Source/WebCore/platform/Pasteboard.h @@ -322,6 +322,7 @@ public: @@ -5412,10 +5580,10 @@ index 071f9fa0e6cfaa84d9b077e39a64bec9361ead9a..fdd0ec667cc7e2f59bd2d21c5c381ae5 #endif diff --git a/Source/WebCore/platform/PlatformScreen.cpp b/Source/WebCore/platform/PlatformScreen.cpp -index eac5bf40c150dba990c13775db0ae9e10f883574..563106c0d9fb259c2171aedd5d57efbf81049cc9 100644 +index 2f0f8a0c34ee8af144ef7b8787cae5f7eac9cf45..454f2a08ebddb21c4980a684697836b21f1f098f 100644 --- a/Source/WebCore/platform/PlatformScreen.cpp +++ b/Source/WebCore/platform/PlatformScreen.cpp -@@ -85,3 +85,24 @@ OptionSet screenContentsFormatsForTesting() +@@ -128,3 +128,24 @@ void PlatformScreen::updateSingletonContentsFormatsForTesting(OptionSet& touchPoints) { m_touchPoints = touchPoints; } diff --git a/Source/WebCore/platform/adwaita/AdwaitaScrollbarPainter.h b/Source/WebCore/platform/adwaita/AdwaitaScrollbarPainter.h -index a251b8f6a5e05997594a2962e7fcb3fab49a27b2..fd95b909650098869d44c8860a525051fc6996f8 100644 +index 6bf01bfe47a8802f53435afc946d5208c7d55533..ebc7558fdb9db1046f5d9f52b4cecc6e5057828e 100644 --- a/Source/WebCore/platform/adwaita/AdwaitaScrollbarPainter.h +++ b/Source/WebCore/platform/adwaita/AdwaitaScrollbarPainter.h -@@ -36,7 +36,7 @@ class GraphicsContext; +@@ -37,7 +37,7 @@ class GraphicsContext; namespace AdwaitaScrollbarPainter { @@ -5571,18 +5739,18 @@ index 775df102268b397e6c96e8b93296a7dbe6afcd26..cdbb7b79bde8ba30bc2b0f6bb579b44d // Determine the string for this item. const char16_t* str = cp.data() + items[i].iCharPos; diff --git a/Source/WebCore/platform/gtk/PlatformScreenGtk.cpp b/Source/WebCore/platform/gtk/PlatformScreenGtk.cpp -index 98d93889a1b97450fde21cee53e45111b622dca7..9362518e8351eb8fe5b30a2d7875417bb3da770d 100644 +index 0b4e0487f2288673918cdebdd13b70f5df4391a5..626679fa93f05c5a667fdcd6191128d915f4f988 100644 --- a/Source/WebCore/platform/gtk/PlatformScreenGtk.cpp +++ b/Source/WebCore/platform/gtk/PlatformScreenGtk.cpp -@@ -121,7 +121,7 @@ bool screenSupportsExtendedColor(Widget*) +@@ -127,7 +127,7 @@ bool screenSupportsExtendedColor(Widget*) } #if ENABLE(TOUCH_EVENTS) -bool screenHasTouchDevice() +bool platformScreenHasTouchDevice() { - return getScreenProperties().screenHasTouchDevice; - } + Ref platformScreen = PlatformScreen::singleton(); + return platformScreen->screenProperties().screenHasTouchDevice; diff --git a/Source/WebCore/platform/libwpe/PlatformKeyboardEventLibWPE.cpp b/Source/WebCore/platform/libwpe/PlatformKeyboardEventLibWPE.cpp index ee7238303b628d0be016c80e391fe8edb9de1439..dc3b000902cd7d8344ed3a16f49f57766864dd47 100644 --- a/Source/WebCore/platform/libwpe/PlatformKeyboardEventLibWPE.cpp @@ -5996,7 +6164,7 @@ index 0640345cf5f133b7f84d3e32b272871d9beff519..320dce768def18327ee25e889109cd6d WEBCORE_EXPORT HTTPCookieAcceptPolicy cookieAcceptPolicy() const; WEBCORE_EXPORT void setCookie(const Cookie&); diff --git a/Source/WebCore/platform/network/ResourceResponseBase.cpp b/Source/WebCore/platform/network/ResourceResponseBase.cpp -index e413db463376bcf31da4cd0f8e2f869f4b6a0a95..6d43a62553f3e82968964a89608a121a72bc3d74 100644 +index 8c5d3ca76595bdac8127b85901c1417b45e11210..77230ddedeb32d90edf610ac4479d24c41e6d1bd 100644 --- a/Source/WebCore/platform/network/ResourceResponseBase.cpp +++ b/Source/WebCore/platform/network/ResourceResponseBase.cpp @@ -21,7 +21,7 @@ @@ -6008,7 +6176,7 @@ index e413db463376bcf31da4cd0f8e2f869f4b6a0a95..6d43a62553f3e82968964a89608a121a */ #include "config.h" -@@ -77,6 +77,7 @@ ResourceResponseBase::ResourceResponseBase(std::optional&& +@@ -78,6 +78,7 @@ ResourceResponseBase::ResourceResponseBase(std::optional&& , m_httpStatusText(data ? WTF::move(data->httpStatusText) : String { }) , m_httpVersion(data ? WTF::move(data->httpVersion) : String { }) , m_httpHeaderFields(data ? WTF::move(data->httpHeaderFields) : HTTPHeaderMap { }) @@ -6016,7 +6184,7 @@ index e413db463376bcf31da4cd0f8e2f869f4b6a0a95..6d43a62553f3e82968964a89608a121a , m_networkLoadMetrics(data && data->networkLoadMetrics ? Box::create(WTF::move(*data->networkLoadMetrics)) : Box { }) , m_certificateInfo(data ? WTF::move(data->certificateInfo) : std::nullopt) , m_httpStatusCode(data ? data->httpStatusCode : 0) -@@ -277,7 +278,7 @@ const String& ResourceResponseBase::mimeType() const +@@ -278,7 +279,7 @@ const String& ResourceResponseBase::mimeType() const { lazyInit(CommonFieldsOnly); @@ -6025,7 +6193,7 @@ index e413db463376bcf31da4cd0f8e2f869f4b6a0a95..6d43a62553f3e82968964a89608a121a } void ResourceResponseBase::setMimeType(String&& mimeType) -@@ -291,7 +292,7 @@ void ResourceResponseBase::setMimeType(String&& mimeType) +@@ -292,7 +293,7 @@ void ResourceResponseBase::setMimeType(String&& mimeType) // FIXME: Should invalidate or update platform response if present. } @@ -6034,7 +6202,7 @@ index e413db463376bcf31da4cd0f8e2f869f4b6a0a95..6d43a62553f3e82968964a89608a121a { lazyInit(CommonFieldsOnly); -@@ -304,7 +305,7 @@ void ResourceResponseBase::setExpectedContentLength(long long expectedContentLen +@@ -305,7 +306,7 @@ void ResourceResponseBase::setExpectedContentLength(long long expectedContentLen m_isNull = false; // FIXME: Content length is determined by HTTP Content-Length header. We should update the header, so that it doesn't disagree with m_expectedContentLength. @@ -6161,7 +6329,7 @@ index e413db463376bcf31da4cd0f8e2f869f4b6a0a95..6d43a62553f3e82968964a89608a121a *source, *type, diff --git a/Source/WebCore/platform/network/ResourceResponseBase.h b/Source/WebCore/platform/network/ResourceResponseBase.h -index ef0c552143191b48c1a52a0702c7596da349eff4..b69149fef86c536d95dd01c39da64f719b86e89d 100644 +index a1e2bc02c8b9803f29c9896067b5a36ffef74970..05476499c284130c65aff44691ffa8f11365ab6a 100644 --- a/Source/WebCore/platform/network/ResourceResponseBase.h +++ b/Source/WebCore/platform/network/ResourceResponseBase.h @@ -21,7 +21,7 @@ @@ -6173,7 +6341,7 @@ index ef0c552143191b48c1a52a0702c7596da349eff4..b69149fef86c536d95dd01c39da64f71 */ #pragma once -@@ -228,9 +228,9 @@ public: +@@ -226,9 +226,9 @@ public: WEBCORE_EXPORT bool containsInvalidHTTPHeaders() const; WEBCORE_EXPORT static ResourceResponse dataURLResponse(const URL&, const DataURLDecoder::Result&); @@ -6185,7 +6353,7 @@ index ef0c552143191b48c1a52a0702c7596da349eff4..b69149fef86c536d95dd01c39da64f71 WEBCORE_EXPORT std::optional getResponseData() const; protected: -@@ -265,6 +265,11 @@ protected: +@@ -263,6 +263,11 @@ protected: String m_httpStatusText; String m_httpVersion; HTTPHeaderMap m_httpHeaderFields; @@ -6197,7 +6365,7 @@ index ef0c552143191b48c1a52a0702c7596da349eff4..b69149fef86c536d95dd01c39da64f71 Box m_networkLoadMetrics; mutable std::optional m_certificateInfo; -@@ -308,7 +313,7 @@ struct ResourceResponseData { +@@ -306,7 +311,7 @@ struct ResourceResponseData { ResourceResponseData() = default; ResourceResponseData(ResourceResponseData&&) = default; ResourceResponseData& operator=(ResourceResponseData&&) = default; @@ -6206,7 +6374,7 @@ index ef0c552143191b48c1a52a0702c7596da349eff4..b69149fef86c536d95dd01c39da64f71 : url(WTF::move(url)) , mimeType(WTF::move(mimeType)) , expectedContentLength(expectedContentLength) -@@ -317,6 +322,7 @@ struct ResourceResponseData { +@@ -315,6 +320,7 @@ struct ResourceResponseData { , httpStatusText(WTF::move(httpStatusText)) , httpVersion(WTF::move(httpVersion)) , httpHeaderFields(WTF::move(httpHeaderFields)) @@ -6214,7 +6382,7 @@ index ef0c552143191b48c1a52a0702c7596da349eff4..b69149fef86c536d95dd01c39da64f71 , networkLoadMetrics(WTF::move(networkLoadMetrics)) , source(source) , type(type) -@@ -341,6 +347,7 @@ struct ResourceResponseData { +@@ -339,6 +345,7 @@ struct ResourceResponseData { String httpStatusText; String httpVersion; HTTPHeaderMap httpHeaderFields; @@ -6223,7 +6391,7 @@ index ef0c552143191b48c1a52a0702c7596da349eff4..b69149fef86c536d95dd01c39da64f71 ResourceResponseBase::Source source; ResourceResponseBase::Type type; diff --git a/Source/WebCore/platform/network/cocoa/NetworkStorageSessionCocoa.mm b/Source/WebCore/platform/network/cocoa/NetworkStorageSessionCocoa.mm -index 3ed98966972014b88f2a4ca24ea615717a9a812c..7b75b20a84474ac8f227666d2d8305a60c1923f5 100644 +index 7b992ba6167f49f41841d907bce45f70c99417f9..f3c7f7fc3db242a6b18f52ab9a16c845be9a11da 100644 --- a/Source/WebCore/platform/network/cocoa/NetworkStorageSessionCocoa.mm +++ b/Source/WebCore/platform/network/cocoa/NetworkStorageSessionCocoa.mm @@ -582,6 +582,27 @@ bool NetworkStorageSession::setCookieFromDOM(const URL& firstParty, const SameSi @@ -6267,6 +6435,93 @@ index 96e0a63b347c170c963bc7e851f383b91665c5c8..2f0afc26bcc87e9440e201728778892b String m_databasePath; bool m_detectedDatabaseCorruption { false }; +diff --git a/Source/WebCore/platform/network/curl/CurlContext.cpp b/Source/WebCore/platform/network/curl/CurlContext.cpp +index 145c3e2b04c8120d80d07159bcc2db91de328352..2f8eae1a79cca273ec1e5abe9333813a66edc774 100644 +--- a/Source/WebCore/platform/network/curl/CurlContext.cpp ++++ b/Source/WebCore/platform/network/curl/CurlContext.cpp +@@ -437,6 +437,11 @@ void CurlHandle::appendRequestHeaders(const HTTPHeaderMap& headers) + } + } + ++static bool isProxyHeader(const String& name) ++{ ++ return startsWithLettersIgnoringASCIICase(name, "proxy-"_s); ++} ++ + void CurlHandle::appendRequestHeader(const String& name, const String& value) + { + String header; +@@ -448,14 +453,20 @@ void CurlHandle::appendRequestHeader(const String& name, const String& value) + header = makeString(name, ": "_s, value); + } + +- appendRequestHeader(WTF::move(header)); ++ if (isProxyHeader(name)) ++ appendProxyRequestHeader(WTF::move(header)); ++ else ++ appendRequestHeader(WTF::move(header)); + } + + void CurlHandle::removeRequestHeader(const String& name) + { +- // Add a header with no content, the internally used header will get disabled. ++ // Add a header with no content, the internally used header will get disabled. + auto header = makeString(name, ':'); +- appendRequestHeader(WTF::move(header)); ++ if (isProxyHeader(name)) ++ appendProxyRequestHeader(WTF::move(header)); ++ else ++ appendRequestHeader(WTF::move(header)); + } + + void CurlHandle::appendRequestHeader(const String& header) +@@ -477,6 +488,25 @@ void CurlHandle::enableRequestHeaders() + curl_easy_setopt(m_handle, CURLOPT_HTTPHEADER, headers); + } + ++void CurlHandle::appendProxyRequestHeader(String&& header) ++{ ++ bool needToEnable = m_proxyRequestHeaders.isEmpty(); ++ ++ m_proxyRequestHeaders.append(header); ++ ++ if (needToEnable) ++ enableProxyRequestHeaders(); ++} ++ ++void CurlHandle::enableProxyRequestHeaders() ++{ ++ if (m_proxyRequestHeaders.isEmpty()) ++ return; ++ ++ const struct curl_slist* headers = m_proxyRequestHeaders.head(); ++ curl_easy_setopt(m_handle, CURLOPT_PROXYHEADER, headers); ++} ++ + void CurlHandle::enableHttp() + { + auto isHttp2Enabled = CurlContext::singleton().isHttp2Enabled(); +diff --git a/Source/WebCore/platform/network/curl/CurlContext.h b/Source/WebCore/platform/network/curl/CurlContext.h +index 807138b0202ea76b5b505e47ac4dd0064364f129..4f39f0f5105d7c807dc67f70fbe6100c94d6968d 100644 +--- a/Source/WebCore/platform/network/curl/CurlContext.h ++++ b/Source/WebCore/platform/network/curl/CurlContext.h +@@ -328,6 +328,8 @@ private: + }; + + void enableRequestHeaders(); ++ void appendProxyRequestHeader(String&&); ++ void enableProxyRequestHeaders(); + + static CURLcode willSetupSslCtxCallback(CURL*, void* sslCtx, void* userData); + CURLcode willSetupSslCtx(void* sslCtx); +@@ -340,6 +342,7 @@ private: + URL m_url; + CurlSList m_localhostAlias; + CurlSList m_requestHeaders; ++ CurlSList m_proxyRequestHeaders; + + std::unique_ptr m_sslVerifier; + std::unique_ptr m_tlsConnectionInfo; diff --git a/Source/WebCore/platform/network/curl/NetworkStorageSessionCurl.cpp b/Source/WebCore/platform/network/curl/NetworkStorageSessionCurl.cpp index 5a78d6c775542cd62d3b192dc60972fdbea92270..5dab0edfef83cc1672012d94bcb4d6390b91bc97 100644 --- a/Source/WebCore/platform/network/curl/NetworkStorageSessionCurl.cpp @@ -6352,7 +6607,7 @@ index 8e6023c5f1200884723bd0871bf602ba4a37ad04..582365cfb1c9a109308d0133922ee4d2 ReleaseStgMedium(&store); } diff --git a/Source/WebCore/platform/win/ClipboardUtilitiesWin.h b/Source/WebCore/platform/win/ClipboardUtilitiesWin.h -index 61624f0c555886e11a82e933aa89a093b5273693..4471b458941d3394d99f85e983cf0d86e1a562d3 100644 +index 6ffe5f9345576be626ded3470c74a0ff661af98b..dbcfc108a67d580d6f6ab7cb3125973ebab3e613 100644 --- a/Source/WebCore/platform/win/ClipboardUtilitiesWin.h +++ b/Source/WebCore/platform/win/ClipboardUtilitiesWin.h @@ -34,6 +34,7 @@ namespace WebCore { @@ -6416,7 +6671,7 @@ index f6c0cc49e9c39686bb5a5b36c29762173f2b4d1f..f396b564ee4aa7203d1728ff84608373 OptionSet PlatformKeyboardEvent::currentStateOfModifierKeys() diff --git a/Source/WebCore/platform/win/PasteboardWin.cpp b/Source/WebCore/platform/win/PasteboardWin.cpp -index 89eb26e07f1bc8174ed50f088a1a9e61e227c627..5949b188bf03caa7c35af1966b3879465f2a92f6 100644 +index d079788986aedd2800a9870e9d4325a1d6fe98fe..24c1e2d4493c8afd95bfdfb7a088333427373a77 100644 --- a/Source/WebCore/platform/win/PasteboardWin.cpp +++ b/Source/WebCore/platform/win/PasteboardWin.cpp @@ -1145,7 +1145,21 @@ void Pasteboard::writeCustomData(const Vector& data) @@ -6468,10 +6723,10 @@ index 89eb26e07f1bc8174ed50f088a1a9e61e227c627..5949b188bf03caa7c35af1966b387946 + } // namespace WebCore diff --git a/Source/WebCore/rendering/RenderLayerCompositor.cpp b/Source/WebCore/rendering/RenderLayerCompositor.cpp -index 7cf7bbc03d7824b38b097427587367b50978c2c0..00f87e2b8155d18385cd2aae8461c442266634ef 100644 +index e55914e2ddd81f0c5cde874a25fb3677bfe5e518..9644bc8d9242598b3bdd2251ec0927ea20b06d87 100644 --- a/Source/WebCore/rendering/RenderLayerCompositor.cpp +++ b/Source/WebCore/rendering/RenderLayerCompositor.cpp -@@ -1065,8 +1065,10 @@ bool RenderLayerCompositor::updateCompositingLayers(CompositingUpdateType update +@@ -1067,8 +1067,10 @@ bool RenderLayerCompositor::updateCompositingLayers(CompositingUpdateType update return false; } @@ -6483,10 +6738,10 @@ index 7cf7bbc03d7824b38b097427587367b50978c2c0..00f87e2b8155d18385cd2aae8461c442 bool isPageScroll = !updateRootArg || updateRootArg == &rootRenderLayer(); CheckedPtr updateRoot = &rootRenderLayer(); diff --git a/Source/WebCore/rendering/RenderTextControl.cpp b/Source/WebCore/rendering/RenderTextControl.cpp -index 7b1911e2444250e1b87bb9fd9b2d6e1e56fa43f0..fdbb83e5ac55b35390f2bf5f383cff5f7bbcf0ae 100644 +index 6b4d0d81a5f64b78e8a628fa9a8323f465539a1f..9076eec04a892f371cc12d2a59b3c64d48b5755c 100644 --- a/Source/WebCore/rendering/RenderTextControl.cpp +++ b/Source/WebCore/rendering/RenderTextControl.cpp -@@ -244,13 +244,13 @@ void RenderTextControl::layoutExcludedChildren(RelayoutChildren relayoutChildren +@@ -258,13 +258,13 @@ void RenderTextControl::layoutExcludedChildren(RelayoutChildren relayoutChildren } } @@ -6502,7 +6757,7 @@ index 7b1911e2444250e1b87bb9fd9b2d6e1e56fa43f0..fdbb83e5ac55b35390f2bf5f383cff5f { if (auto innerTextElement = this->innerTextElement(); innerTextElement && innerTextElement->renderer()) diff --git a/Source/WebCore/rendering/RenderTextControl.h b/Source/WebCore/rendering/RenderTextControl.h -index 00011d61c20cf7509b03407ecbcc29ce2da0ccca..d939e62c37573487cf4df7a16cf6201b5400cf0c 100644 +index 4d3cc813b6dfe35c26ae57d6d8221754e79ce10e..8d2c14fa19dab737fdfd1ad4fffc3c309e24f36b 100644 --- a/Source/WebCore/rendering/RenderTextControl.h +++ b/Source/WebCore/rendering/RenderTextControl.h @@ -38,8 +38,8 @@ public: @@ -6516,10 +6771,10 @@ index 00011d61c20cf7509b03407ecbcc29ce2da0ccca..d939e62c37573487cf4df7a16cf6201b #endif diff --git a/Source/WebCore/workers/WorkerConsoleClient.cpp b/Source/WebCore/workers/WorkerConsoleClient.cpp -index fe03d2c8d0625725e07a2aa0eef3a0d9127cf211..9c661ee5f91766bb6d903cfd25f3c7a43d9f3538 100644 +index 03a2f715a535fc93c035076a07ec2047cd16ef49..db27ba21ede7d96c3635dab610fad619316f02dc 100644 --- a/Source/WebCore/workers/WorkerConsoleClient.cpp +++ b/Source/WebCore/workers/WorkerConsoleClient.cpp -@@ -257,4 +257,6 @@ void WorkerConsoleClient::screenshot(JSC::JSGlobalObject* lexicalGlobalObject, R +@@ -260,4 +260,6 @@ void WorkerConsoleClient::screenshot(JSC::JSGlobalObject* lexicalGlobalObject, R InspectorInstrumentation::addMessageToConsole(protect(globalScope()), makeUnique(MessageSource::ConsoleAPI, MessageType::Image, MessageLevel::Log, dataURL, ScriptArguments::create(lexicalGlobalObject, WTF::move(adjustedArguments)), lexicalGlobalObject, /* requestIdentifier */ 0, timestamp)); } @@ -6539,7 +6794,7 @@ index 60e744703647b7593426c59814975bf12dc4ebaa..d5156a30396fb2ad7e73c5c5429a07d5 WorkerOrWorkletGlobalScope& globalScope() { return m_globalScope; } diff --git a/Source/WebGPU/WGSL/UniformityAnalysis.cpp b/Source/WebGPU/WGSL/UniformityAnalysis.cpp -index 85806067d986a7f9047708500e1c510fd0df1834..39e066e5882c4fbe8f7b31bfa5d62e3188761709 100644 +index e8032692ff9b48bfe6994c4202b7c67993aee5da..3da1878782599d7dca9dcac47b023f674913c365 100644 --- a/Source/WebGPU/WGSL/UniformityAnalysis.cpp +++ b/Source/WebGPU/WGSL/UniformityAnalysis.cpp @@ -118,8 +118,10 @@ struct FunctionInfo { @@ -6565,7 +6820,7 @@ index 85806067d986a7f9047708500e1c510fd0df1834..39e066e5882c4fbe8f7b31bfa5d62e31 info.cfStart = info.createNode(); diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp -index 964de6c27cb21b49ba83e54ce80c96bad7081d5e..3f6d6bd758205cc8a3922c7c5be5bc44979868a9 100644 +index 3c29eca336f76ad99a2680552f8fef67331493af..550bbd001147a0468ae3a22426a29104867d6ea3 100644 --- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp +++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp @@ -99,6 +99,8 @@ @@ -6577,7 +6832,7 @@ index 964de6c27cb21b49ba83e54ce80c96bad7081d5e..3f6d6bd758205cc8a3922c7c5be5bc44 #include #include #endif -@@ -1318,6 +1320,14 @@ void NetworkConnectionToWebProcess::clearPageSpecificData(PageIdentifier pageID) +@@ -1332,6 +1334,14 @@ void NetworkConnectionToWebProcess::clearPageSpecificData(PageIdentifier pageID) storageSession->clearPageSpecificDataForResourceLoadStatistics(pageID); } @@ -6593,7 +6848,7 @@ index 964de6c27cb21b49ba83e54ce80c96bad7081d5e..3f6d6bd758205cc8a3922c7c5be5bc44 { if (CheckedPtr storageSession = m_networkProcess->storageSession(m_sessionID)) diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h -index 5d24c2e36dcdbee25a0bb005bf0a20963d442a31..aff9bb1a0a64d689e15f7be57bed9e5bec80be6a 100644 +index d367a92d9859dcc686f1fa020471d04821a5f4fe..3ccd6bfd43cd735c48cff2df68feb2db4900ba23 100644 --- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h +++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h @@ -399,6 +399,8 @@ private: @@ -6606,10 +6861,10 @@ index 5d24c2e36dcdbee25a0bb005bf0a20963d442a31..aff9bb1a0a64d689e15f7be57bed9e5b void logUserInteraction(RegistrableDomain&&); diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in -index 6f43a9c68ee5c247184e327bdda83ce65df444f5..b34971ca9e296106dda574f165c4f28d157ecdc6 100644 +index 7881aacfe9a52a3375173727e061000bd75a0fcc..3740b503f84992c48f175a6354d27242af9a3d52 100644 --- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in +++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in -@@ -83,6 +83,8 @@ messages -> NetworkConnectionToWebProcess WantsDispatchMessage { +@@ -85,6 +85,8 @@ messages -> NetworkConnectionToWebProcess WantsDispatchMessage { ClearPageSpecificData(WebCore::PageIdentifier pageID); @@ -6619,10 +6874,10 @@ index 6f43a9c68ee5c247184e327bdda83ce65df444f5..b34971ca9e296106dda574f165c4f28d LogUserInteraction(WebCore::RegistrableDomain domain) ResourceLoadStatisticsUpdated(Vector statistics) -> () diff --git a/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm b/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm -index 446f6d91016132ceb2a28122990e844e4576ed4c..0886e0642ac2353de9184b12b8a0d23d6f0f0fd3 100644 +index 9dd97c0cb8685f61f9a6d8a5dcb5748716a1037e..3588f4911b7d9151e5bcf4f166b4e9879ccb81f5 100644 --- a/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm +++ b/Source/WebKit/NetworkProcess/cocoa/NetworkSessionCocoa.mm -@@ -894,6 +894,14 @@ - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)data +@@ -896,6 +896,14 @@ - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)data resourceResponse.setDeprecatedNetworkLoadMetrics(WebCore::copyTimingData(taskMetrics.get(), networkDataTask->networkLoadMetrics())); resourceResponse.setProxyName(WTF::move(proxyName)); @@ -6650,10 +6905,10 @@ index 0a2c25f815f594b2de6168ffd8b083c625e42e1f..682c4584f34d6fab8b576b331561102d handleCookieHeaders(request.resourceRequest(), receivedResponse); diff --git a/Source/WebKit/NetworkProcess/mac/com.apple.WebKit.NetworkProcess.sb.in b/Source/WebKit/NetworkProcess/mac/com.apple.WebKit.NetworkProcess.sb.in -index b0a007b87d78f871b00db3bd37b5f5116b9d72f6..2dc529916b8b833284898107c0d942aedd3000d9 100644 +index 9185d134cdc35b19517b5ad22a84bb1152550dc9..5ae9654b1e051d0e562c48d134ec810438ca18e4 100644 --- a/Source/WebKit/NetworkProcess/mac/com.apple.WebKit.NetworkProcess.sb.in +++ b/Source/WebKit/NetworkProcess/mac/com.apple.WebKit.NetworkProcess.sb.in -@@ -451,9 +451,11 @@ +@@ -446,9 +446,11 @@ ;; FIXME: This should be removed when is fixed. ;; Restrict AppSandboxed processes from creating /Library/Keychains, but allow access to the contents of /Library/Keychains: @@ -6669,10 +6924,10 @@ index b0a007b87d78f871b00db3bd37b5f5116b9d72f6..2dc529916b8b833284898107c0d942ae ;; Except deny access to new-style iOS Keychain folders which are UUIDs. (deny file-read* file-write* diff --git a/Source/WebKit/NetworkProcess/soup/NetworkDataTaskSoup.cpp b/Source/WebKit/NetworkProcess/soup/NetworkDataTaskSoup.cpp -index f7edb09dfd0bcee4b60f4bf8ce16f8cbeea937f8..1c10fb6ea21b897015a5c0092fc8f6cd4fb34c0c 100644 +index a9221bc3fb2964776efd2861b906d681f3e90946..9ea9cbaf290b4e74b558959c3af82cff49c3b3b6 100644 --- a/Source/WebKit/NetworkProcess/soup/NetworkDataTaskSoup.cpp +++ b/Source/WebKit/NetworkProcess/soup/NetworkDataTaskSoup.cpp -@@ -427,6 +427,8 @@ void NetworkDataTaskSoup::didSendRequest(GRefPtr&& inputStream) +@@ -428,6 +428,8 @@ void NetworkDataTaskSoup::didSendRequest(GRefPtr&& inputStream) else m_inputStream = WTF::move(inputStream); @@ -6682,7 +6937,7 @@ index f7edb09dfd0bcee4b60f4bf8ce16f8cbeea937f8..1c10fb6ea21b897015a5c0092fc8f6cd } diff --git a/Source/WebKit/PlatformWPE.cmake b/Source/WebKit/PlatformWPE.cmake -index f98ae19e9c5fa4b0601572de46b1e0a1c62e92d8..747157cb6b47441ed2c1ca89465c81a89acc7f2a 100644 +index 581e3978a8a66c5b7fd28c877f4b311fbb8381a9..407888510c73788102c3c8e46ab64a9b87c68f88 100644 --- a/Source/WebKit/PlatformWPE.cmake +++ b/Source/WebKit/PlatformWPE.cmake @@ -227,6 +227,7 @@ set(WPE_API_HEADER_TEMPLATES @@ -6694,10 +6949,10 @@ index f98ae19e9c5fa4b0601572de46b1e0a1c62e92d8..747157cb6b47441ed2c1ca89465c81a8 if (ENABLE_2022_GLIB_API) diff --git a/Source/WebKit/PlatformWin.cmake b/Source/WebKit/PlatformWin.cmake -index 86a1febedca9fcbe7203db8cec94e8db1ef25a43..efdc87688149706c783906b643489fbe695d61d2 100644 +index 1003cc3cc60a91e27166ba0891fa63c2d0c27d4c..5d2e09bf814902341a72f4a1cad70a43d0a9b60a 100644 --- a/Source/WebKit/PlatformWin.cmake +++ b/Source/WebKit/PlatformWin.cmake -@@ -54,8 +54,13 @@ list(APPEND WebKit_SOURCES +@@ -55,8 +55,13 @@ list(APPEND WebKit_SOURCES UIProcess/win/AutomationClientWin.cpp UIProcess/win/AutomationSessionClientWin.cpp @@ -6711,7 +6966,7 @@ index 86a1febedca9fcbe7203db8cec94e8db1ef25a43..efdc87688149706c783906b643489fbe UIProcess/win/WebPageProxyWin.cpp UIProcess/win/WebPopupMenuProxyWin.cpp UIProcess/win/WebProcessPoolWin.cpp -@@ -71,6 +76,7 @@ list(APPEND WebKit_SOURCES +@@ -72,6 +77,7 @@ list(APPEND WebKit_SOURCES WebProcess/MediaCache/WebMediaKeyStorageManager.cpp WebProcess/WebCoreSupport/win/WebPopupMenuWin.cpp @@ -6732,10 +6987,10 @@ index 5c7a6999176357ad21ca673db75363f27cf33790..faa9a55d561a185a659519bcaa5a1026 #import #import diff --git a/Source/WebKit/Shared/AuxiliaryProcess.h b/Source/WebKit/Shared/AuxiliaryProcess.h -index 04ffbae16bdfe24465e635afce7f5042073158a0..9c87adff05fcdc47fb15cc4bcac86c26882c6e3e 100644 +index 664988a9998acec84a7b2b767b0c6d66a638322c..fb97365201c36db3a1e0da58a93e4e3a64d4f7f9 100644 --- a/Source/WebKit/Shared/AuxiliaryProcess.h +++ b/Source/WebKit/Shared/AuxiliaryProcess.h -@@ -216,6 +216,11 @@ struct AuxiliaryProcessInitializationParameters { +@@ -215,6 +215,11 @@ struct AuxiliaryProcessInitializationParameters { IPC::Connection::Identifier connectionIdentifier; HashMap extraInitializationData; WTF::AuxiliaryProcessType processType; @@ -6834,10 +7089,10 @@ index b5361fedd7d921f512956d20819c49d425221080..3628908eeaba9dda7c19ded41b6da6e5 NSEvent* nativeEvent() const { return m_nativeEvent.get(); } #elif PLATFORM(GTK) diff --git a/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in b/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in -index 2ced21e6c88f5ee6f78c8ac44d9ebd9a670507bb..9ef1b28fd00dfe6b5e802b117f597c55a5878108 100644 +index 9322ebc1c7d35e2c17375ee674af6c9d421c5b03..17e37b130d0538461ce86f9e1ce20aa339560e55 100644 --- a/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in +++ b/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in -@@ -2732,6 +2732,9 @@ class WebCore::AuthenticationChallenge { +@@ -2743,6 +2743,9 @@ class WebCore::AuthenticationChallenge { class WebCore::DragData { #if PLATFORM(COCOA) String pasteboardName(); @@ -6847,7 +7102,7 @@ index 2ced21e6c88f5ee6f78c8ac44d9ebd9a670507bb..9ef1b28fd00dfe6b5e802b117f597c55 #endif WebCore::IntPoint clientPosition(); WebCore::IntPoint globalPosition(); -@@ -3164,6 +3167,7 @@ enum class WebCore::WasPrivateRelayed : bool; +@@ -3175,6 +3178,7 @@ enum class WebCore::WasPrivateRelayed : bool; String httpStatusText; String httpVersion; WebCore::HTTPHeaderMap httpHeaderFields; @@ -6981,10 +7236,10 @@ index 3accf5c66062f746029665e4a9ef970418784df2..e16205860562432eedeb017d354cd0c9 void setPosition(const WebCore::DoublePoint& position) { m_position = position; } const WebCore::DoublePoint& globalPosition() const LIFETIME_BOUND { return m_globalPosition; } diff --git a/Source/WebKit/Shared/WebPageCreationParameters.h b/Source/WebKit/Shared/WebPageCreationParameters.h -index d0e04502450eedf0dc1e6104f10fe122edee992e..aaa5dfced49e1f20cbd02da81364dbf0285a04d9 100644 +index 523c1617004315196f6bad9135e0ce19870c4173..4c68eb42fd03a2f3f399818e3398cfb78a837698 100644 --- a/Source/WebKit/Shared/WebPageCreationParameters.h +++ b/Source/WebKit/Shared/WebPageCreationParameters.h -@@ -317,6 +317,9 @@ struct WebPageCreationParameters { +@@ -319,6 +319,9 @@ struct WebPageCreationParameters { WebCore::ShouldRelaxThirdPartyCookieBlocking shouldRelaxThirdPartyCookieBlocking { WebCore::ShouldRelaxThirdPartyCookieBlocking::No }; bool httpsUpgradeEnabled { true }; @@ -6995,10 +7250,10 @@ index d0e04502450eedf0dc1e6104f10fe122edee992e..aaa5dfced49e1f20cbd02da81364dbf0 #if ENABLE(APP_HIGHLIGHTS) WebCore::HighlightVisibility appHighlightsVisible { WebCore::HighlightVisibility::Hidden }; diff --git a/Source/WebKit/Shared/WebPageCreationParameters.serialization.in b/Source/WebKit/Shared/WebPageCreationParameters.serialization.in -index 707a31d075afd590890fb3e0054f256b4a567ac1..55ac3de3488b82b77888101d6677fb76c1b1c00c 100644 +index 578b3ac57d759663efc094a30d29f57d7fd72507..84a3fd354acd057098dae4243835628b2cede1a7 100644 --- a/Source/WebKit/Shared/WebPageCreationParameters.serialization.in +++ b/Source/WebKit/Shared/WebPageCreationParameters.serialization.in -@@ -233,6 +233,9 @@ enum class WebCore::UserInterfaceLayoutDirection : bool; +@@ -234,6 +234,9 @@ enum class WebCore::UserInterfaceLayoutDirection : bool; bool httpsUpgradeEnabled; @@ -7339,11 +7594,11 @@ index b321d93d23d0d1862385b022569dc65323e8691c..ba3bcc9bc20b9512b9288cb2d7e5a3d8 JSC::Config::configureForTesting(); else if (!strcmp(argv[i], "-disable-jit")) diff --git a/Source/WebKit/Sources.txt b/Source/WebKit/Sources.txt -index 382ea8985d8781d7d49f12a90bf0ecc65232985f..ebe6f275fed47f09490052f0548641b1dec4b8f4 100644 +index 1d107e47fea02bdf78144460df5a4e94d629f262..285027ccfba22952b023eb8a137e3ae87730feff 100644 --- a/Source/WebKit/Sources.txt +++ b/Source/WebKit/Sources.txt @@ -403,6 +403,7 @@ UIProcess/AboutSchemeHandler.cpp - UIProcess/AuxiliaryProcessProxy.cpp + UIProcess/AuxiliaryProcessProxy.cpp @cost:4 UIProcess/BackgroundProcessResponsivenessTimer.cpp UIProcess/BrowsingContextGroup.cpp +UIProcess/BrowserInspectorPipe.cpp @@ -7352,12 +7607,12 @@ index 382ea8985d8781d7d49f12a90bf0ecc65232985f..ebe6f275fed47f09490052f0548641b1 UIProcess/DisplayLinkProcessProxyClient.cpp @@ -414,17 +415,21 @@ UIProcess/FrameLoadState.cpp UIProcess/FrameProcess.cpp - UIProcess/GeolocationPermissionRequestManagerProxy.cpp + UIProcess/GeolocationPermissionRequestManagerProxy.cpp @cost:3 UIProcess/GeolocationPermissionRequestProxy.cpp +UIProcess/InspectorDialogAgent.cpp +UIProcess/InspectorPlaywrightAgent.cpp UIProcess/LegacyGlobalSettings.cpp - UIProcess/MediaKeySystemPermissionRequestManagerProxy.cpp + UIProcess/MediaKeySystemPermissionRequestManagerProxy.cpp @cost:4 UIProcess/MediaKeySystemPermissionRequestProxy.cpp UIProcess/OverrideLanguages.cpp UIProcess/PageClient.cpp @@ -7371,38 +7626,38 @@ index 382ea8985d8781d7d49f12a90bf0ecc65232985f..ebe6f275fed47f09490052f0548641b1 +UIProcess/RemoteInspectorPipe.cpp UIProcess/RemotePageDrawingAreaProxy.cpp UIProcess/RemotePageFullscreenManagerProxy.cpp - UIProcess/RemotePagePlaybackSessionManagerProxy.cpp -@@ -472,6 +477,8 @@ UIProcess/WebOpenPanelResultListenerProxy.cpp + UIProcess/RemotePageMediaSessionManagerProxy.cpp +@@ -474,6 +479,8 @@ UIProcess/WebOpenPanelResultListenerProxy.cpp UIProcess/WebPageDiagnosticLoggingClient.cpp UIProcess/WebPageGroup.cpp UIProcess/WebPageInjectedBundleClient.cpp +UIProcess/WebPageInspectorEmulationAgent.cpp +UIProcess/WebPageInspectorInputAgent.cpp UIProcess/WebPageProxy.cpp - UIProcess/WebPageProxyMessageReceiverRegistration.cpp - UIProcess/WebPageProxyTesting.cpp + UIProcess/WebPageProxyMessageReceiverRegistration.cpp @cost:3 + UIProcess/WebPageProxyTesting.cpp @cost:4 @@ -641,6 +648,7 @@ UIProcess/Inspector/WebPageDebuggable.cpp UIProcess/Inspector/WebPageInspectorController.cpp UIProcess/Inspector/Agents/InspectorBrowserAgent.cpp +UIProcess/Inspector/Agents/InspectorScreencastAgent.cpp + UIProcess/Inspector/Agents/ProxyingNetworkAgent.cpp + UIProcess/Inspector/Agents/ProxyingPageAgent.cpp - UIProcess/Media/AudioSessionRoutingArbitratorProxy.cpp - UIProcess/Media/MediaUsageManager.cpp diff --git a/Source/WebKit/SourcesCocoa.txt b/Source/WebKit/SourcesCocoa.txt -index 8f5e05d7905c09452701a390277d8528d3c53170..3ac17db7930b41d33d7e0f6ee9564b997967a8a6 100644 +index 97950c298c16becd2958bdb966efe9748af3c5e4..9bd63968ceb756a9a1f89f4d6091eb10e1774ea2 100644 --- a/Source/WebKit/SourcesCocoa.txt +++ b/Source/WebKit/SourcesCocoa.txt -@@ -286,6 +286,7 @@ UIProcess/API/Cocoa/_WKArchiveExclusionRule.mm @nonARC - UIProcess/API/Cocoa/_WKAttachment.mm @nonARC +@@ -287,6 +287,7 @@ UIProcess/API/Cocoa/_WKAttachment.mm @nonARC UIProcess/API/Cocoa/_WKAutomationSession.mm @nonARC UIProcess/API/Cocoa/_WKAutomationSessionConfiguration.mm @nonARC + UIProcess/API/Cocoa/_WKAutomationSessionTesting.mm @nonARC +UIProcess/API/Cocoa/_WKBrowserInspector.mm @nonARC UIProcess/API/Cocoa/_WKContentRuleListAction.mm @nonARC UIProcess/API/Cocoa/_WKContextMenuElementInfo.mm @nonARC - UIProcess/API/Cocoa/_WKCustomHeaderFields.mm @nonARC @no-unify + UIProcess/API/Cocoa/_WKCustomHeaderFields.mm @nonARC @no-unify-when(bundle<=8) diff --git a/Source/WebKit/SourcesGTK.txt b/Source/WebKit/SourcesGTK.txt -index 4b68f079d9d43d1b0bef8e3b31cd0d6f3a730ae9..3799718e6e5791791335171a8adb0365231501bf 100644 +index f05f1323f3516a4a0e18759c5776cac4778575fb..304d7783165bc9e7f03344d5375c3e83301f3317 100644 --- a/Source/WebKit/SourcesGTK.txt +++ b/Source/WebKit/SourcesGTK.txt @@ -134,6 +134,7 @@ UIProcess/API/glib/WebKitAutomationSession.cpp @no-unify @@ -7428,8 +7683,8 @@ index 4b68f079d9d43d1b0bef8e3b31cd0d6f3a730ae9..3799718e6e5791791335171a8adb0365 +UIProcess/glib/InspectorPlaywrightAgentClientGLib.cpp UIProcess/glib/ScreenManager.cpp UIProcess/glib/SystemSettingsManagerProxy.cpp - UIProcess/glib/WebPageProxyGLib.cpp -@@ -287,9 +290,9 @@ UIProcess/gtk/DisplayX11.cpp @no-unify + UIProcess/glib/TextCheckerGLib.cpp @no-unify +@@ -290,9 +293,9 @@ UIProcess/gtk/DisplayX11.cpp @no-unify UIProcess/gtk/DisplayWayland.cpp @no-unify UIProcess/gtk/GRefPtrGtk.cpp @no-unify UIProcess/gtk/GtkUtilities.cpp @no-unify @@ -7440,7 +7695,7 @@ index 4b68f079d9d43d1b0bef8e3b31cd0d6f3a730ae9..3799718e6e5791791335171a8adb0365 UIProcess/gtk/PointerLockManager.cpp @no-unify UIProcess/gtk/PointerLockManagerWayland.cpp @no-unify UIProcess/gtk/PointerLockManagerX11.cpp @no-unify -@@ -303,6 +306,9 @@ UIProcess/gtk/ViewGestureControllerGtk.cpp +@@ -305,6 +308,9 @@ UIProcess/gtk/ViewGestureControllerGtk.cpp UIProcess/gtk/WebColorPickerGtk.cpp UIProcess/gtk/WebContextMenuProxyGtk.cpp UIProcess/gtk/WebDataListSuggestionsDropdownGtk.cpp @@ -7451,7 +7706,7 @@ index 4b68f079d9d43d1b0bef8e3b31cd0d6f3a730ae9..3799718e6e5791791335171a8adb0365 UIProcess/gtk/WebPasteboardProxyGtk.cpp UIProcess/gtk/WebPopupMenuProxyGtk.cpp diff --git a/Source/WebKit/SourcesWPE.txt b/Source/WebKit/SourcesWPE.txt -index 3d1ed6156127f02614905e30835fa4c6334ae914..fb362b66d00c2081d6eb5380fb2bd7967223f489 100644 +index 2b1d3588fcbbbd9a8a72f259e58aadc282009749..b5a51b3c0339322d8827a9816460ac789635bb70 100644 --- a/Source/WebKit/SourcesWPE.txt +++ b/Source/WebKit/SourcesWPE.txt @@ -138,6 +138,7 @@ UIProcess/API/glib/WebKitAuthenticationRequest.cpp @no-unify @@ -7485,8 +7740,8 @@ index 3d1ed6156127f02614905e30835fa4c6334ae914..fb362b66d00c2081d6eb5380fb2bd796 +UIProcess/glib/InspectorPlaywrightAgentClientGLib.cpp UIProcess/glib/ScreenManager.cpp UIProcess/glib/SystemSettingsManagerProxy.cpp - UIProcess/glib/WebPageProxyGLib.cpp -@@ -284,9 +288,15 @@ UIProcess/soup/WebProcessPoolSoup.cpp + UIProcess/glib/TextCheckerGLib.cpp +@@ -287,9 +291,15 @@ UIProcess/soup/WebProcessPoolSoup.cpp UIProcess/wpe/AcceleratedBackingStore.cpp UIProcess/wpe/DisplayVBlankMonitorWPE.cpp @@ -7659,10 +7914,10 @@ index 026121d114c5fcad84c1396be8d692625beaa3bd..edd6e5cae033124c589959a42522fde0 } #endif diff --git a/Source/WebKit/UIProcess/API/C/WKPage.cpp b/Source/WebKit/UIProcess/API/C/WKPage.cpp -index a14d69ad745f665e6769d0d781e31010b6440e15..2c16da61112a7e8457a4845682453091a1ddfda5 100644 +index 2a30fd22991944ac82a1fa61886d1b0bd55a616c..1e0ab703701bd3550e3929bb8728f83f49671afb 100644 --- a/Source/WebKit/UIProcess/API/C/WKPage.cpp +++ b/Source/WebKit/UIProcess/API/C/WKPage.cpp -@@ -1913,6 +1913,13 @@ void WKPageSetPageUIClient(WKPageRef pageRef, const WKPageUIClientBase* wkClient +@@ -1918,6 +1918,13 @@ void WKPageSetPageUIClient(WKPageRef pageRef, const WKPageUIClientBase* wkClient m_client.addMessageToConsole(toAPI(&page), toAPI(message.impl()), m_client.base.clientInfo); } @@ -7676,7 +7931,7 @@ index a14d69ad745f665e6769d0d781e31010b6440e15..2c16da61112a7e8457a4845682453091 void setStatusText(WebPageProxy* page, const String& text) final { if (!m_client.setStatusText) -@@ -1950,6 +1957,8 @@ void WKPageSetPageUIClient(WKPageRef pageRef, const WKPageUIClientBase* wkClient +@@ -1955,6 +1962,8 @@ void WKPageSetPageUIClient(WKPageRef pageRef, const WKPageUIClientBase* wkClient { if (!m_client.didNotHandleKeyEvent) return; @@ -7686,7 +7941,7 @@ index a14d69ad745f665e6769d0d781e31010b6440e15..2c16da61112a7e8457a4845682453091 } diff --git a/Source/WebKit/UIProcess/API/C/WKPageUIClient.h b/Source/WebKit/UIProcess/API/C/WKPageUIClient.h -index ae4a98c2fe5782eb2356dc8b6b486f6b44db6db3..62b2f3c351fe287af11c1ee2815f1fabe044d537 100644 +index aa395e66a7dee2a08085fcf0041748d062400181..bcfdd3e9a3f469d4c18186d9bcc711afe0d52838 100644 --- a/Source/WebKit/UIProcess/API/C/WKPageUIClient.h +++ b/Source/WebKit/UIProcess/API/C/WKPageUIClient.h @@ -98,6 +98,7 @@ typedef void (*WKPageRunBeforeUnloadConfirmPanelCallback)(WKPageRef page, WKStri @@ -7697,7 +7952,7 @@ index ae4a98c2fe5782eb2356dc8b6b486f6b44db6db3..62b2f3c351fe287af11c1ee2815f1fab typedef void (*WKPageRequestStorageAccessConfirmCallback)(WKPageRef page, WKFrameRef frame, WKStringRef requestingDomain, WKStringRef currentDomain, WKPageRequestStorageAccessConfirmResultListenerRef listener, const void *clientInfo); typedef void (*WKPageTakeFocusCallback)(WKPageRef page, WKFocusDirection direction, const void *clientInfo); typedef void (*WKPageFocusCallback)(WKPageRef page, const void *clientInfo); -@@ -1366,6 +1367,7 @@ typedef struct WKPageUIClientV14 { +@@ -1364,6 +1365,7 @@ typedef struct WKPageUIClientV14 { // Version 14. WKPageRunWebAuthenticationPanelCallback runWebAuthenticationPanel; @@ -7705,46 +7960,46 @@ index ae4a98c2fe5782eb2356dc8b6b486f6b44db6db3..62b2f3c351fe287af11c1ee2815f1fab } WKPageUIClientV14; typedef struct WKPageUIClientV15 { -@@ -1473,6 +1475,7 @@ typedef struct WKPageUIClientV15 { +@@ -1471,6 +1473,7 @@ typedef struct WKPageUIClientV15 { // Version 14. WKPageRunWebAuthenticationPanelCallback runWebAuthenticationPanel; + WKPageHandleJavaScriptDialogCallback handleJavaScriptDialog; // Version 15. - WKPageDecidePolicyForSpeechRecognitionPermissionRequestCallback decidePolicyForSpeechRecognitionPermissionRequest; -@@ -1584,6 +1587,7 @@ typedef struct WKPageUIClientV16 { + void* unused8; // Used to be decidePolicyForSpeechRecognitionPermissionRequest. +@@ -1582,6 +1585,7 @@ typedef struct WKPageUIClientV16 { // Version 14. WKPageRunWebAuthenticationPanelCallback runWebAuthenticationPanel; + WKPageHandleJavaScriptDialogCallback handleJavaScriptDialog; // Version 15. - WKPageDecidePolicyForSpeechRecognitionPermissionRequestCallback decidePolicyForSpeechRecognitionPermissionRequest; -@@ -1698,6 +1702,7 @@ typedef struct WKPageUIClientV17 { + void* unused8; // Used to be decidePolicyForSpeechRecognitionPermissionRequest. +@@ -1696,6 +1700,7 @@ typedef struct WKPageUIClientV17 { // Version 14. WKPageRunWebAuthenticationPanelCallback runWebAuthenticationPanel; + WKPageHandleJavaScriptDialogCallback handleJavaScriptDialog; // Version 15. - WKPageDecidePolicyForSpeechRecognitionPermissionRequestCallback decidePolicyForSpeechRecognitionPermissionRequest; -@@ -1812,6 +1817,7 @@ typedef struct WKPageUIClientV18 { + void* unused8; // Used to be decidePolicyForSpeechRecognitionPermissionRequest. +@@ -1810,6 +1815,7 @@ typedef struct WKPageUIClientV18 { // Version 14. WKPageRunWebAuthenticationPanelCallback runWebAuthenticationPanel; + WKPageHandleJavaScriptDialogCallback handleJavaScriptDialog; // Version 15. - WKPageDecidePolicyForSpeechRecognitionPermissionRequestCallback decidePolicyForSpeechRecognitionPermissionRequest; -@@ -1928,6 +1934,7 @@ typedef struct WKPageUIClientV19 { + void* unused8; // Used to be decidePolicyForSpeechRecognitionPermissionRequest. +@@ -1926,6 +1932,7 @@ typedef struct WKPageUIClientV19 { // Version 14. WKPageRunWebAuthenticationPanelCallback runWebAuthenticationPanel; + WKPageHandleJavaScriptDialogCallback handleJavaScriptDialog; // Version 15. - WKPageDecidePolicyForSpeechRecognitionPermissionRequestCallback decidePolicyForSpeechRecognitionPermissionRequest; + void* unused8; // Used to be decidePolicyForSpeechRecognitionPermissionRequest. diff --git a/Source/WebKit/UIProcess/API/Cocoa/WKUIDelegate.h b/Source/WebKit/UIProcess/API/Cocoa/WKUIDelegate.h index acaeaef1be6d64f16d56d966db12cf9bb1f0507e..bb81783b20e33f69ceef2d00785a2e9c811dabe8 100644 --- a/Source/WebKit/UIProcess/API/Cocoa/WKUIDelegate.h @@ -7776,7 +8031,7 @@ index dcab0400bfdd58f9d16726aba6ce66492f7be34b..a1337cc7ab9f04fea3f51a3003fdb946 NS_ASSUME_NONNULL_END diff --git a/Source/WebKit/UIProcess/API/Cocoa/WKWebsiteDataStore.mm b/Source/WebKit/UIProcess/API/Cocoa/WKWebsiteDataStore.mm -index 632bf6f982628fe174eac150b36a10c1d7677b09..09f3075420aa9aa84a93c40533d3eea7acbdc587 100644 +index 74bb3649a58725889cdb8d61988b2e9159fdb7fb..512804cde9c46d4eb9193bc402166638e10fdf31 100644 --- a/Source/WebKit/UIProcess/API/Cocoa/WKWebsiteDataStore.mm +++ b/Source/WebKit/UIProcess/API/Cocoa/WKWebsiteDataStore.mm @@ -57,6 +57,7 @@ @@ -7787,7 +8042,7 @@ index 632bf6f982628fe174eac150b36a10c1d7677b09..09f3075420aa9aa84a93c40533d3eea7 #import #import #import -@@ -532,6 +533,11 @@ - (void)removeDataOfTypes:(NSSet *)dataTypes modifiedSince:(NSDate *)date comple +@@ -510,6 +511,11 @@ - (void)removeDataOfTypes:(NSSet *)dataTypes modifiedSince:(NSDate *)date comple }); } @@ -8206,7 +8461,7 @@ index 0000000000000000000000000000000000000000..e0b1da48465c850f541532ed961d1b77 +WebKit::WebPageProxy* webkitBrowserInspectorCreateNewPageInContext(WebKitWebContext*); +void webkitBrowserInspectorQuitApplication(); diff --git a/Source/WebKit/UIProcess/API/glib/WebKitProtocolHandler.cpp b/Source/WebKit/UIProcess/API/glib/WebKitProtocolHandler.cpp -index c816d0248895d9aa670e2226798237c696451b45..4d317eb4aaa630a97c03d70e7b15b05460354b01 100644 +index 432afab488fb4bd96640ed218c30065070fe8eda..ba050c8b762e81353d22cf232bf45126af1d16a9 100644 --- a/Source/WebKit/UIProcess/API/glib/WebKitProtocolHandler.cpp +++ b/Source/WebKit/UIProcess/API/glib/WebKitProtocolHandler.cpp @@ -160,51 +160,7 @@ static bool canvasAccelerationEnabled(WebKitURISchemeRequest* request) @@ -8413,7 +8668,7 @@ index c816d0248895d9aa670e2226798237c696451b45..4d317eb4aaa630a97c03d70e7b15b054 return builder.toString(); } #endif -@@ -732,14 +726,18 @@ void WebKitProtocolHandler::handleGPU(WebKitURISchemeRequest* request, RenderPro +@@ -734,14 +728,18 @@ void WebKitProtocolHandler::handleGPU(WebKitURISchemeRequest* request, RenderPro if (showBuffersInfo) { #if PLATFORM(GTK) || (PLATFORM(WPE) && ENABLE(WPE_PLATFORM)) addTableRow(hardwareAccelerationObject, "Renderer"_s, dmabufRendererWithSupportedBuffers()); @@ -8518,7 +8773,7 @@ index c1945fbe717a42afc1f51d64a80c7de3fa9009ba..ab63fe19b00ecbd64c9421e6eecad3e2 #endif +int webkitWebContextExistingCount(); diff --git a/Source/WebKit/UIProcess/API/glib/WebKitWebView.cpp b/Source/WebKit/UIProcess/API/glib/WebKitWebView.cpp -index 3ed130d4fb0345c7309a240d3a412991e15f9904..10c9c0c89e4a9caf68d8075cde195bbf805e6887 100644 +index d4dadc3f50368a2768fbd2b453bf0b63d2b79e28..0abc5f9bcaee216e61307383afcaa0f5c9c88610 100644 --- a/Source/WebKit/UIProcess/API/glib/WebKitWebView.cpp +++ b/Source/WebKit/UIProcess/API/glib/WebKitWebView.cpp @@ -40,6 +40,7 @@ @@ -8788,7 +9043,7 @@ index 496079da90993ac37689b060b69ecd4a67c2b6a8..af30181ca922f16c0f6e245c70e5ce7d G_BEGIN_DECLS diff --git a/Source/WebKit/UIProcess/API/gtk/WebKitWebViewBase.cpp b/Source/WebKit/UIProcess/API/gtk/WebKitWebViewBase.cpp -index d5bb72f0ffbf99eddebc9819cb3f24fdaffdf468..d248c201e0ee9db63a117b8cde34661595a594fa 100644 +index 12d114c4823bd64b2b836fa76157c44fe8291a07..521db62435bc244e28bd861ce560ff1416d99279 100644 --- a/Source/WebKit/UIProcess/API/gtk/WebKitWebViewBase.cpp +++ b/Source/WebKit/UIProcess/API/gtk/WebKitWebViewBase.cpp @@ -2879,6 +2879,11 @@ void webkitWebViewBaseResetClickCounter(WebKitWebViewBase* webkitWebViewBase) @@ -9165,10 +9420,10 @@ index 21131a4d26ba115f3249b227d3e1dabd42398c80..7b1a6e98f3c1fb5ef5d54f416c49ba3e + +PlatformImage webkitWebViewBackendTakeScreenshot(WebKitWebViewBackend*); diff --git a/Source/WebKit/UIProcess/Automation/WebAutomationSession.h b/Source/WebKit/UIProcess/Automation/WebAutomationSession.h -index c38751ae3064aa6a42a3ed8fdb902f70473a8e08..f6bf1a674402bc08f69bccc97058954981ad4ebc 100644 +index b7596a4c384d5cc0c21cc1bef1bc695c2c5275af..4fdb0cdb0c761aa48373e1eae0d132c0b40aa1b7 100644 --- a/Source/WebKit/UIProcess/Automation/WebAutomationSession.h +++ b/Source/WebKit/UIProcess/Automation/WebAutomationSession.h -@@ -318,6 +318,8 @@ public: +@@ -333,6 +333,8 @@ public: void didDestroyFrame(WebCore::FrameIdentifier); @@ -9177,7 +9432,7 @@ index c38751ae3064aa6a42a3ed8fdb902f70473a8e08..f6bf1a674402bc08f69bccc970589549 RefPtr webPageProxyForHandle(const String&); String effectiveHandleForWebFrameProxy(const WebFrameProxy&); String handleForWebFrameID(std::optional); -@@ -385,7 +387,6 @@ private: +@@ -400,7 +402,6 @@ private: // Get base64-encoded PNG data from a bitmap. static std::optional platformGetBase64EncodedPNGData(WebCore::ShareableBitmap::Handle&&); @@ -9186,7 +9441,7 @@ index c38751ae3064aa6a42a3ed8fdb902f70473a8e08..f6bf1a674402bc08f69bccc970589549 // Save base64-encoded file contents to a local file path and return the path. // This reuses the basename of the remote file path so that the filename exposed to DOM API remains the same. diff --git a/Source/WebKit/UIProcess/AuxiliaryProcessProxy.cpp b/Source/WebKit/UIProcess/AuxiliaryProcessProxy.cpp -index ae33d4c8ed5dd321a940f67351447d698c4e1e47..f704e6f32cbb3b0bb357862c366762fd145cba4d 100644 +index 696ac95f824b3aaf97394dae5969d901c3e75797..02fc887d85625ec56fc7b4c82c9b878958265e82 100644 --- a/Source/WebKit/UIProcess/AuxiliaryProcessProxy.cpp +++ b/Source/WebKit/UIProcess/AuxiliaryProcessProxy.cpp @@ -177,7 +177,11 @@ void AuxiliaryProcessProxy::getLaunchOptions(ProcessLauncher::LaunchOptions& lau @@ -9202,10 +9457,10 @@ index ae33d4c8ed5dd321a940f67351447d698c4e1e47..f704e6f32cbb3b0bb357862c366762fd platformGetLaunchOptions(launchOptions); } diff --git a/Source/WebKit/UIProcess/AuxiliaryProcessProxy.h b/Source/WebKit/UIProcess/AuxiliaryProcessProxy.h -index 0241fd8531d53a1f773e7c4a2c871e1903816eba..f199f4096d254ce0242c569db600554efac8c226 100644 +index 290a382e8c22691772d36041c116d048229898be..1173ad74843de165ba46812a39e1cc515519a72a 100644 --- a/Source/WebKit/UIProcess/AuxiliaryProcessProxy.h +++ b/Source/WebKit/UIProcess/AuxiliaryProcessProxy.h -@@ -296,13 +296,16 @@ protected: +@@ -304,13 +304,16 @@ protected: InitializationActivityAndGrant initializationActivityAndGrant(); @@ -9351,12 +9606,12 @@ index 0000000000000000000000000000000000000000..cd66887de171cda7d15a8e4dc6dbff63 + +#endif // ENABLE(REMOTE_INSPECTOR) diff --git a/Source/WebKit/UIProcess/Cocoa/SOAuthorization/NavigationSOAuthorizationSession.h b/Source/WebKit/UIProcess/Cocoa/SOAuthorization/NavigationSOAuthorizationSession.h -index 45abb108899e19cfe0cecd6716083afbff03d73c..11ced1bb2d03c949cb31435eeeee2a196928a2de 100644 +index 3db7f7b2c30ed7adb942dd37dfd8a4de048ad714..4b2a3a6c3b22155f0780c77a3e25dfff3efe1b71 100644 --- a/Source/WebKit/UIProcess/Cocoa/SOAuthorization/NavigationSOAuthorizationSession.h +++ b/Source/WebKit/UIProcess/Cocoa/SOAuthorization/NavigationSOAuthorizationSession.h -@@ -31,6 +31,7 @@ - #include "WebViewDidMoveToWindowObserver.h" +@@ -32,6 +32,7 @@ #include + #include #include +#include @@ -9423,7 +9678,7 @@ index c660d8204b89997bb25637ed24198acf5028e280..6d7c9102b1bc57fd179ba742cf703b1b { RefPtr uiDelegate = m_uiDelegate.get(); diff --git a/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm b/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm -index 177ac869fe4128bb459d2577d90f6b208e8395a2..e6ade97d800a2119d4abd9e241186915c432fd9c 100644 +index 2c860d013e41a4233c45ffa1e3c941c4e03c30af..c871f13b90c0389c0af55497eb900942c333806a 100644 --- a/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm +++ b/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm @@ -45,7 +45,9 @@ @@ -9436,7 +9691,7 @@ index 177ac869fe4128bb459d2577d90f6b208e8395a2..e6ade97d800a2119d4abd9e241186915 #import "PlatformXRSystem.h" #import "PlaybackSessionManagerProxy.h" #import "RemoteLayerTreeCommitBundle.h" -@@ -446,11 +448,85 @@ bool WebPageProxy::scrollingUpdatesDisabledForTesting() +@@ -493,11 +495,85 @@ bool WebPageProxy::scrollingUpdatesDisabledForTesting() void WebPageProxy::startDrag(const DragItem& dragItem, ShareableBitmap::Handle&& dragImageHandle, const std::optional& nodeID, const std::optional& frameID) { @@ -9524,10 +9779,10 @@ index 177ac869fe4128bb459d2577d90f6b208e8395a2..e6ade97d800a2119d4abd9e241186915 #if ENABLE(ATTACHMENT_ELEMENT) diff --git a/Source/WebKit/UIProcess/Cocoa/WebProcessPoolCocoa.mm b/Source/WebKit/UIProcess/Cocoa/WebProcessPoolCocoa.mm -index 4b43164f215ca8e72a68252f36862e25b2742ea8..0804791f8ef0174b905cb18851962f04b43151e4 100644 +index 6a07ad8cf130407f7a59f28a58e4478452de6f40..dd11957f65b3cdbc655a3d124b8e18cf8edb962b 100644 --- a/Source/WebKit/UIProcess/Cocoa/WebProcessPoolCocoa.mm +++ b/Source/WebKit/UIProcess/Cocoa/WebProcessPoolCocoa.mm -@@ -451,7 +451,7 @@ ALLOW_DEPRECATED_DECLARATIONS_END +@@ -461,7 +461,7 @@ ALLOW_DEPRECATED_DECLARATIONS_END auto screenProperties = WebCore::collectScreenProperties(); parameters.screenProperties = WTF::move(screenProperties); #if PLATFORM(MAC) @@ -9536,7 +9791,7 @@ index 4b43164f215ca8e72a68252f36862e25b2742ea8..0804791f8ef0174b905cb18851962f04 #endif #if PLATFORM(VISION) -@@ -864,8 +864,8 @@ void WebProcessPool::registerNotificationObservers() +@@ -880,8 +880,8 @@ void WebProcessPool::registerNotificationObservers() }]; m_scrollerStyleNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSPreferredScrollerStyleDidChangeNotification object:nil queue:[NSOperationQueue currentQueue] usingBlock:^(NSNotification *notification) { @@ -9548,7 +9803,7 @@ index 4b43164f215ca8e72a68252f36862e25b2742ea8..0804791f8ef0174b905cb18851962f04 m_activationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationDidBecomeActiveNotification object:NSAppSingleton() queue:[NSOperationQueue currentQueue] usingBlock:^(NSNotification *notification) { diff --git a/Source/WebKit/UIProcess/CoordinatedGraphics/DrawingAreaProxyCoordinatedGraphics.cpp b/Source/WebKit/UIProcess/CoordinatedGraphics/DrawingAreaProxyCoordinatedGraphics.cpp -index be40fa0e3e3e8698c29f70a2b8dac324225aba97..4e6cc206ed0a25914d8df604f61746562fd9c976 100644 +index be40fa0e3e3e8698c29f70a2b8dac324225aba97..6f8d77d390d41b40b1099baf53e3a3c1131de679 100644 --- a/Source/WebKit/UIProcess/CoordinatedGraphics/DrawingAreaProxyCoordinatedGraphics.cpp +++ b/Source/WebKit/UIProcess/CoordinatedGraphics/DrawingAreaProxyCoordinatedGraphics.cpp @@ -33,6 +33,7 @@ @@ -9559,7 +9814,7 @@ index be40fa0e3e3e8698c29f70a2b8dac324225aba97..4e6cc206ed0a25914d8df604f6174656 #include "WebPageProxy.h" #include "WebPreferences.h" #include "WebProcessPool.h" -@@ -40,6 +41,15 @@ +@@ -40,6 +41,16 @@ #include #include #include @@ -9567,6 +9822,7 @@ index be40fa0e3e3e8698c29f70a2b8dac324225aba97..4e6cc206ed0a25914d8df604f6174656 + +#if PLATFORM(GTK) +#include "WebKitWebViewBasePrivate.h" ++#include +#include +#include +#include @@ -9575,7 +9831,7 @@ index be40fa0e3e3e8698c29f70a2b8dac324225aba97..4e6cc206ed0a25914d8df604f6174656 #if USE(GLIB_EVENT_LOOP) #include -@@ -175,6 +185,11 @@ void DrawingAreaProxyCoordinatedGraphics::deviceScaleFactorDidChange(CompletionH +@@ -175,6 +186,11 @@ void DrawingAreaProxyCoordinatedGraphics::deviceScaleFactorDidChange(CompletionH sendWithAsyncReply(Messages::DrawingArea::SetDeviceScaleFactor(page()->deviceScaleFactor()), WTF::move(completionHandler)); } @@ -9587,51 +9843,29 @@ index be40fa0e3e3e8698c29f70a2b8dac324225aba97..4e6cc206ed0a25914d8df604f6174656 void DrawingAreaProxyCoordinatedGraphics::setBackingStoreIsDiscardable(bool isBackingStoreDiscardable) { #if !PLATFORM(WPE) && !PLATFORM(GTK) -@@ -234,6 +249,54 @@ void DrawingAreaProxyCoordinatedGraphics::updateAcceleratedCompositingMode(uint6 +@@ -234,6 +250,32 @@ void DrawingAreaProxyCoordinatedGraphics::updateAcceleratedCompositingMode(uint6 updateAcceleratedCompositingMode(layerTreeContext); } +#if PLATFORM(GTK) +void DrawingAreaProxyCoordinatedGraphics::captureFrame() +{ -+ RefPtr surface; -+ if (isInAcceleratedCompositingMode()) { -+ AcceleratedBackingStore* backingStore = webkitWebViewBaseGetAcceleratedBackingStore(WEBKIT_WEB_VIEW_BASE(protect(page())->viewWidget())); -+ if (!backingStore) -+ return; -+ -+ surface = backingStore->surface(); -+ } ++ if (!isInAcceleratedCompositingMode()) ++ return; + -+ if (!surface) ++ AcceleratedBackingStore* backingStore = webkitWebViewBaseGetAcceleratedBackingStore(WEBKIT_WEB_VIEW_BASE(protect(page())->viewWidget())); ++ if (!backingStore) + return; + -+ if (cairo_surface_get_type(surface.get()) != CAIRO_SURFACE_TYPE_IMAGE) ++ // Reuse the backing store's snapshot path (also used by the test runner). It ++ // returns the committed buffer's contents as a NativeImage in the natural ++ // top-down orientation for every buffer type (GBM, SHM, DMA-BUF, EGLImage), ++ // including on GTK 4.16+ where buffers are backed by a GdkTexture. ++ RefPtr image = backingStore->bufferAsNativeImageForTesting(); ++ if (!image) + return; + -+ // The original surface is upside down, so we flip it to match orientation in other accelerated backing stores. -+ auto flippedSurface = adoptRef(cairo_image_surface_create(CAIRO_FORMAT_ARGB32, cairo_image_surface_get_width(surface.get()), cairo_image_surface_get_height(surface.get()))); -+ { -+ RefPtr cr = adoptRef(cairo_create(flippedSurface.get())); -+ cairo_matrix_t transform; -+ cairo_matrix_init(&transform, 1, 0, 0, -1, 0, cairo_image_surface_get_height(surface.get())); -+ cairo_transform(cr.get(), &transform); -+ cairo_set_source_surface(cr.get(), surface.get(), 0, 0); -+ cairo_paint(cr.get()); -+ } -+ cairo_surface_flush(flippedSurface.get()); -+ -+ unsigned char* data = cairo_image_surface_get_data(flippedSurface.get()); -+ int width = cairo_image_surface_get_width(flippedSurface.get()); -+ int height = cairo_image_surface_get_height(flippedSurface.get()); -+ int stride = cairo_image_surface_get_stride(flippedSurface.get()); -+ -+ SkImageInfo info = SkImageInfo::Make( -+ width, height, -+ kBGRA_8888_SkColorType, // matches CAIRO_FORMAT_ARGB32 on LE -+ kPremul_SkAlphaType -+ ); -+ sk_sp skImage = SkImages::RasterFromData(info, SkData::MakeWithCopy(data, height * stride), stride); ++ sk_sp skImage = image->platformImage(); + if (!skImage) + return; + @@ -9642,7 +9876,7 @@ index be40fa0e3e3e8698c29f70a2b8dac324225aba97..4e6cc206ed0a25914d8df604f6174656 bool DrawingAreaProxyCoordinatedGraphics::alwaysUseCompositing() const { if (!page()) -@@ -301,6 +364,12 @@ void DrawingAreaProxyCoordinatedGraphics::didUpdateGeometry() +@@ -301,6 +343,12 @@ void DrawingAreaProxyCoordinatedGraphics::didUpdateGeometry() // we need to resend the new size here. if (m_lastSentSize != size()) sendUpdateGeometry(); @@ -10213,6 +10447,186 @@ index 0000000000000000000000000000000000000000..afadd2371dffab9d4b92e4245f562282 +}; + +} // namespace WebKit +diff --git a/Source/WebKit/UIProcess/Inspector/Agents/ProxyingNetworkAgent.cpp b/Source/WebKit/UIProcess/Inspector/Agents/ProxyingNetworkAgent.cpp +index 7d547b7eee13523e7b8f311a49e9670664c4959e..61e7716b45dc1d7ac28e4cb5f1490d123895ca6a 100644 +--- a/Source/WebKit/UIProcess/Inspector/Agents/ProxyingNetworkAgent.cpp ++++ b/Source/WebKit/UIProcess/Inspector/Agents/ProxyingNetworkAgent.cpp +@@ -399,6 +399,12 @@ CommandResult ProxyingNetworkAgent::interceptRequestWithError(const Protoc + return { }; + } + ++CommandResult ProxyingNetworkAgent::setEmulateOfflineState(bool) ++{ ++ // FIXME: Forward to all WebContent processes. ++ return { }; ++} ++ + #if ENABLE(INSPECTOR_NETWORK_THROTTLING) + + CommandResult ProxyingNetworkAgent::setEmulatedConditions(std::optional&&) +diff --git a/Source/WebKit/UIProcess/Inspector/Agents/ProxyingNetworkAgent.h b/Source/WebKit/UIProcess/Inspector/Agents/ProxyingNetworkAgent.h +index 408a8108556cdd384564091457a28e49976a9402..90de5101b8e2049038ca991754827243c8beb6b8 100644 +--- a/Source/WebKit/UIProcess/Inspector/Agents/ProxyingNetworkAgent.h ++++ b/Source/WebKit/UIProcess/Inspector/Agents/ProxyingNetworkAgent.h +@@ -92,6 +92,7 @@ public: + CommandResult interceptWithResponse(const Protocol::Network::RequestId&, const String& content, bool base64Encoded, const String& mimeType, std::optional&& status, const String& statusText, RefPtr&& headers) final; + CommandResult interceptRequestWithResponse(const Protocol::Network::RequestId&, const String& content, bool base64Encoded, const String& mimeType, int status, const String& statusText, Ref&& headers) final; + CommandResult interceptRequestWithError(const Protocol::Network::RequestId&, Protocol::Network::ResourceErrorType) final; ++ CommandResult setEmulateOfflineState(bool offline) final; + #if ENABLE(INSPECTOR_NETWORK_THROTTLING) + CommandResult setEmulatedConditions(std::optional&& bytesPerSecondLimit) final; + #endif +diff --git a/Source/WebKit/UIProcess/Inspector/Agents/ProxyingPageAgent.cpp b/Source/WebKit/UIProcess/Inspector/Agents/ProxyingPageAgent.cpp +index 0cef1a19215fba7f477b8ae1f7f16e9091ebd4ee..8306beaa340e2d038dfb76a63ec79237788bf131 100644 +--- a/Source/WebKit/UIProcess/Inspector/Agents/ProxyingPageAgent.cpp ++++ b/Source/WebKit/UIProcess/Inspector/Agents/ProxyingPageAgent.cpp +@@ -98,12 +98,14 @@ void ProxyingPageAgent::frameNavigated(FrameIdentifier frameID, const URL& url, + + void ProxyingPageAgent::domContentEventFired(double timestamp) + { +- m_frontendDispatcher->domContentEventFired(timestamp); ++ // FIXME: plumb FrameIdentifier through the IPC message. ++ m_frontendDispatcher->domContentEventFired(timestamp, { }); + } + + void ProxyingPageAgent::loadEventFired(double timestamp) + { +- m_frontendDispatcher->loadEventFired(timestamp); ++ // FIXME: plumb FrameIdentifier through the IPC message. ++ m_frontendDispatcher->loadEventFired(timestamp, { }); + } + + void ProxyingPageAgent::frameDetached(FrameIdentifier frameID) +@@ -346,17 +348,82 @@ CommandResult ProxyingPageAgent::setShowPaintRects(bool) + return { }; + } + ++CommandResult ProxyingPageAgent::goBack() ++{ ++ return { }; ++} ++ ++CommandResult ProxyingPageAgent::goForward() ++{ ++ return { }; ++} ++ ++CommandResult ProxyingPageAgent::overridePlatform(const String&) ++{ ++ return { }; ++} ++ ++CommandResult ProxyingPageAgent::setForcedColors(std::optional&&) ++{ ++ return { }; ++} ++ ++CommandResult ProxyingPageAgent::setTimeZone(const String&) ++{ ++ return { }; ++} ++ ++CommandResult ProxyingPageAgent::setTouchEmulationEnabled(bool) ++{ ++ return { }; ++} ++ + CommandResult ProxyingPageAgent::setEmulatedMedia(const String&) + { + return { }; + } + ++CommandResult ProxyingPageAgent::insertText(const String&) ++{ ++ return { }; ++} ++ ++CommandResult ProxyingPageAgent::setInterceptFileChooserDialog(bool) ++{ ++ return { }; ++} ++ ++CommandResult ProxyingPageAgent::setDefaultBackgroundColorOverride(RefPtr&&) ++{ ++ return { }; ++} ++ ++CommandResult ProxyingPageAgent::createUserWorld(const String&) ++{ ++ return { }; ++} ++ ++CommandResult ProxyingPageAgent::setBypassCSP(bool) ++{ ++ return { }; ++} ++ ++CommandResult ProxyingPageAgent::crash() ++{ ++ return { }; ++} ++ ++CommandResult ProxyingPageAgent::updateScrollingState() ++{ ++ return { }; ++} ++ + CommandResult ProxyingPageAgent::snapshotNode(Protocol::DOM::NodeId) + { + return makeUnexpected("Not yet implemented under Site Isolation"_s); + } + +-CommandResult ProxyingPageAgent::snapshotRect(int, int, int, int, Protocol::Page::CoordinateSystem) ++CommandResult ProxyingPageAgent::snapshotRect(int, int, int, int, Protocol::Page::CoordinateSystem, std::optional&&, std::optional&&, std::optional&&) + { + return makeUnexpected("Not yet implemented under Site Isolation"_s); + } +@@ -368,11 +435,9 @@ CommandResult ProxyingPageAgent::archive() + } + #endif + +-#if !PLATFORM(COCOA) + CommandResult ProxyingPageAgent::setScreenSizeOverride(std::optional&&, std::optional&&) + { + return { }; + } +-#endif + + } // namespace Inspector +diff --git a/Source/WebKit/UIProcess/Inspector/Agents/ProxyingPageAgent.h b/Source/WebKit/UIProcess/Inspector/Agents/ProxyingPageAgent.h +index 5391fbc1d384bd9be7c5f497567caef046fd2a9e..b23b30e28270eb2d615c01659299fe6ca5e6aeec 100644 +--- a/Source/WebKit/UIProcess/Inspector/Agents/ProxyingPageAgent.h ++++ b/Source/WebKit/UIProcess/Inspector/Agents/ProxyingPageAgent.h +@@ -86,15 +86,26 @@ public: + CommandResult setShowRulers(bool) final; + #endif + CommandResult setShowPaintRects(bool) final; ++ CommandResult goBack() final; ++ CommandResult goForward() final; ++ CommandResult overridePlatform(const String&) final; ++ CommandResult setForcedColors(std::optional&&) final; ++ CommandResult setTimeZone(const String&) final; ++ CommandResult setTouchEmulationEnabled(bool) final; + CommandResult setEmulatedMedia(const String&) final; ++ CommandResult insertText(const String&) final; ++ CommandResult setInterceptFileChooserDialog(bool) final; ++ CommandResult setDefaultBackgroundColorOverride(RefPtr&&) final; ++ CommandResult createUserWorld(const String&) final; ++ CommandResult setBypassCSP(bool) final; ++ CommandResult crash() final; ++ CommandResult updateScrollingState() final; + CommandResult snapshotNode(Protocol::DOM::NodeId) final; +- CommandResult snapshotRect(int x, int y, int width, int height, Protocol::Page::CoordinateSystem) final; ++ CommandResult snapshotRect(int x, int y, int width, int height, Protocol::Page::CoordinateSystem, std::optional&& omitDeviceScaleFactor, std::optional&&, std::optional&& quality) final; + #if ENABLE(WEB_ARCHIVE) && USE(CF) + CommandResult archive() final; + #endif +-#if !PLATFORM(COCOA) + CommandResult setScreenSizeOverride(std::optional&& width, std::optional&& height) final; +-#endif + + private: + // IPC::MessageReceiver diff --git a/Source/WebKit/UIProcess/Inspector/PageInspectorTargetProxy.cpp b/Source/WebKit/UIProcess/Inspector/PageInspectorTargetProxy.cpp index 66e1ddd0ad468d7e9d262442ad0efdbcf7989ea1..28c0a0f7b7edaaf7f30c677594365a436344512d 100644 --- a/Source/WebKit/UIProcess/Inspector/PageInspectorTargetProxy.cpp @@ -10300,10 +10714,10 @@ index 232b5dc1c574e4d4231307fee144aff59b519758..651fc7fe63d80cf093192e4eec1576c7 #include "WebProcessProxyMessages.h" #include diff --git a/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.cpp b/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.cpp -index 13da6142d0889583d399b4ee9ead9a6850b700b2..626f87341d5eb3d7192d4c80621e84bf9181940f 100644 +index 05d99a1b1c4348dc30f72f0c107b61aec9894678..f51d1ad17b748377a8232a3ef3346f2ac4a621d9 100644 --- a/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.cpp +++ b/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.cpp -@@ -26,19 +26,27 @@ +@@ -26,10 +26,14 @@ #include "config.h" #include "WebPageInspectorController.h" @@ -10318,7 +10732,8 @@ index 13da6142d0889583d399b4ee9ead9a6850b700b2..626f87341d5eb3d7192d4c80621e84bf #include "PageInspectorTarget.h" #include "PageInspectorTargetProxy.h" #include "ProvisionalFrameProxy.h" - #include "ProvisionalPageProxy.h" +@@ -38,9 +42,13 @@ + #include "ProxyingPageAgent.h" #include "WebFrameProxy.h" #include "WebPageInspectorAgentBase.h" +#include "WebPageInspectorEmulationAgent.h" @@ -10331,7 +10746,7 @@ index 13da6142d0889583d399b4ee9ead9a6850b700b2..626f87341d5eb3d7192d4c80621e84bf #include #include #include -@@ -69,6 +77,17 @@ static String getTargetID(const ProvisionalFrameProxy& provisionalFrame) +@@ -77,6 +85,17 @@ static String getTargetID(const ProvisionalFrameProxy& provisionalFrame) WTF_MAKE_TZONE_ALLOCATED_IMPL(WebPageInspectorController); @@ -10349,7 +10764,7 @@ index 13da6142d0889583d399b4ee9ead9a6850b700b2..626f87341d5eb3d7192d4c80621e84bf WebPageInspectorController::WebPageInspectorController(WebPageProxy& inspectedPage) : m_frontendRouter(FrontendRouter::create()) , m_backendDispatcher(BackendDispatcher::create(m_frontendRouter.copyRef())) -@@ -82,16 +101,92 @@ WebPageInspectorController::WebPageInspectorController(WebPageProxy& inspectedPa +@@ -90,16 +109,92 @@ WebPageInspectorController::WebPageInspectorController(WebPageProxy& inspectedPa WebPageInspectorController::~WebPageInspectorController() = default; void WebPageInspectorController::init() @@ -10442,7 +10857,7 @@ index 13da6142d0889583d399b4ee9ead9a6850b700b2..626f87341d5eb3d7192d4c80621e84bf } bool WebPageInspectorController::hasLocalFrontend() const -@@ -105,6 +200,14 @@ void WebPageInspectorController::connectFrontend(Inspector::FrontendChannel& fro +@@ -113,6 +208,14 @@ void WebPageInspectorController::connectFrontend(Inspector::FrontendChannel& fro bool connectingFirstFrontend = !m_frontendRouter->hasFrontends(); @@ -10456,20 +10871,16 @@ index 13da6142d0889583d399b4ee9ead9a6850b700b2..626f87341d5eb3d7192d4c80621e84bf + m_frontendRouter->connectFrontend(frontendChannel); - if (connectingFirstFrontend) -@@ -124,8 +227,10 @@ void WebPageInspectorController::disconnectFrontend(FrontendChannel& frontendCha - m_frontendRouter->disconnectFrontend(frontendChannel); - - bool disconnectingLastFrontend = !m_frontendRouter->hasFrontends(); -- if (disconnectingLastFrontend) -+ if (disconnectingLastFrontend) { - m_agents.willDestroyFrontendAndBackend(DisconnectReason::InspectorDestroyed); + if (connectingFirstFrontend) { +@@ -143,6 +246,7 @@ void WebPageInspectorController::disconnectFrontend(FrontendChannel& frontendCha + networkAgent->willDestroyFrontendAndBackend(DisconnectReason::InspectorDestroyed); + if (RefPtr pageAgent = m_pageAgent) + pageAgent->willDestroyFrontendAndBackend(DisconnectReason::InspectorDestroyed); + m_pendingNavigations.clear(); -+ } + } Ref inspectedPage = m_inspectedPage.get(); - inspectedPage->didChangeInspectorFrontendCount(m_frontendRouter->frontendCount()); -@@ -149,6 +254,8 @@ void WebPageInspectorController::disconnectAllFrontends() +@@ -171,6 +275,8 @@ void WebPageInspectorController::disconnectAllFrontends() // Disconnect any remaining remote frontends. m_frontendRouter->disconnectAllFrontends(); @@ -10478,7 +10889,7 @@ index 13da6142d0889583d399b4ee9ead9a6850b700b2..626f87341d5eb3d7192d4c80621e84bf Ref inspectedPage = m_inspectedPage.get(); inspectedPage->didChangeInspectorFrontendCount(m_frontendRouter->frontendCount()); -@@ -177,6 +284,66 @@ void WebPageInspectorController::setIndicating(bool indicating) +@@ -199,6 +305,66 @@ void WebPageInspectorController::setIndicating(bool indicating) } #endif @@ -10545,7 +10956,7 @@ index 13da6142d0889583d399b4ee9ead9a6850b700b2..626f87341d5eb3d7192d4c80621e84bf void WebPageInspectorController::sendMessageToInspectorFrontend(const String& targetId, const String& message) { if (!m_targets.contains(targetId)) { -@@ -191,6 +358,52 @@ void WebPageInspectorController::sendMessageToInspectorFrontend(const String& ta +@@ -213,6 +379,52 @@ void WebPageInspectorController::sendMessageToInspectorFrontend(const String& ta protect(m_targetAgent)->sendMessageFromTargetToFrontend(targetId, message); } @@ -10595,10 +11006,10 @@ index 13da6142d0889583d399b4ee9ead9a6850b700b2..626f87341d5eb3d7192d4c80621e84bf + target->setResumeCallback(WTF::move(callback)); +} + - bool WebPageInspectorController::shouldPauseLoading(const ProvisionalPageProxy& provisionalPage) const + bool WebPageInspectorController::shouldPauseLoadingForPage(const ProvisionalPageProxy& provisionalPage) const { if (!m_frontendRouter->hasFrontends()) -@@ -210,7 +423,7 @@ void WebPageInspectorController::setContinueLoadingCallback(const ProvisionalPag +@@ -267,7 +479,7 @@ void WebPageInspectorController::setContinueLoadingCallbackForFrame(const Provis void WebPageInspectorController::didCreateProvisionalPage(ProvisionalPageProxy& provisionalPage, WebCore::FrameIdentifier mainFrameID, WebProcessProxy& mainFrameProcess) { @@ -10608,7 +11019,7 @@ index 13da6142d0889583d399b4ee9ead9a6850b700b2..626f87341d5eb3d7192d4c80621e84bf if (shouldManageFrameTargets()) { constexpr bool isProvisional = true; diff --git a/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.h b/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.h -index f670f9007a9386bf9933e59c32463f697dbb6597..17fa5c0f2860c9423de3e167616d46fa5d857125 100644 +index d335f13ded0edda8144f158f0944b8bae3b009a7..18c710fe57a85ffe738f0e1fd425f090d91a29e9 100644 --- a/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.h +++ b/Source/WebKit/UIProcess/Inspector/WebPageInspectorController.h @@ -28,9 +28,11 @@ @@ -10623,7 +11034,7 @@ index f670f9007a9386bf9933e59c32463f697dbb6597..17fa5c0f2860c9423de3e167616d46fa #include #include #include -@@ -38,11 +40,29 @@ +@@ -38,6 +40,12 @@ #include #include #include @@ -10636,8 +11047,10 @@ index f670f9007a9386bf9933e59c32463f697dbb6597..17fa5c0f2860c9423de3e167616d46fa namespace Inspector { class BackendDispatcher; - class FrontendChannel; +@@ -45,6 +53,18 @@ class FrontendChannel; class FrontendRouter; + class ProxyingNetworkAgent; + class ProxyingPageAgent; +class InspectorTarget; +} + @@ -10653,7 +11066,7 @@ index f670f9007a9386bf9933e59c32463f697dbb6597..17fa5c0f2860c9423de3e167616d46fa } namespace WebKit { -@@ -51,6 +71,22 @@ class InspectorBrowserAgent; +@@ -53,6 +73,22 @@ class InspectorBrowserAgent; class ProvisionalPageProxy; struct WebPageAgentContext; @@ -10676,7 +11089,7 @@ index f670f9007a9386bf9933e59c32463f697dbb6597..17fa5c0f2860c9423de3e167616d46fa class WebPageInspectorController { WTF_MAKE_TZONE_ALLOCATED(WebPageInspectorController); WTF_MAKE_NONCOPYABLE(WebPageInspectorController); -@@ -59,7 +95,21 @@ public: +@@ -61,7 +97,21 @@ public: ~WebPageInspectorController(); void init(); @@ -10698,7 +11111,7 @@ index f670f9007a9386bf9933e59c32463f697dbb6597..17fa5c0f2860c9423de3e167616d46fa bool hasLocalFrontend() const; -@@ -72,9 +122,25 @@ public: +@@ -74,9 +124,25 @@ public: #if ENABLE(REMOTE_INSPECTOR) void setIndicating(bool); #endif @@ -10721,10 +11134,10 @@ index f670f9007a9386bf9933e59c32463f697dbb6597..17fa5c0f2860c9423de3e167616d46fa + bool shouldPauseInInspectorWhenShown() const; + void setContinueLoadingCallback(WTF::Function&&); + - bool shouldPauseLoading(const ProvisionalPageProxy&) const; - void setContinueLoadingCallback(const ProvisionalPageProxy&, WTF::Function&&); - -@@ -111,9 +177,16 @@ private: + bool shouldPauseLoadingForPage(const ProvisionalPageProxy&) const; + void setContinueLoadingCallbackForPage(const ProvisionalPageProxy&, WTF::Function&&); + bool shouldPauseLoadingForFrame(const ProvisionalFrameProxy&) const; +@@ -117,11 +183,18 @@ private: CheckedPtr m_targetAgent; HashMap> m_targets; @@ -10733,6 +11146,8 @@ index f670f9007a9386bf9933e59c32463f697dbb6597..17fa5c0f2860c9423de3e167616d46fa + InspectorScreencastAgent* m_screecastAgent { nullptr }; + CheckedPtr m_enabledBrowserAgent; + RefPtr m_networkAgent; + RefPtr m_pageAgent; bool m_didCreateLazyAgents { false }; + UncheckedKeyHashMap m_pendingNavigations; @@ -12179,7 +12594,7 @@ index 0000000000000000000000000000000000000000..af71e4077eb0c6f95396de7bfef89a3e + +#endif // ENABLE(REMOTE_INSPECTOR) diff --git a/Source/WebKit/UIProcess/Launcher/glib/ProcessLauncherGLib.cpp b/Source/WebKit/UIProcess/Launcher/glib/ProcessLauncherGLib.cpp -index 9e0f05585ae96f4ec9ab2390cb015b5307fe85e6..048dfad9f9221fc5389de5d7874a0a67ec61499a 100644 +index 4b51695cfad05edf88ec3d50ed49bcdd55411195..e568c845e9801153b1d37a88e4631d0917abba65 100644 --- a/Source/WebKit/UIProcess/Launcher/glib/ProcessLauncherGLib.cpp +++ b/Source/WebKit/UIProcess/Launcher/glib/ProcessLauncherGLib.cpp @@ -160,6 +160,13 @@ void ProcessLauncher::launchProcess() @@ -12235,22 +12650,22 @@ index 6723ee0d9943be07bc8ad09d2b678838aca968df..0d7fb3c7b1a4c877a2ff2f2189d12c7d BOOL result = ::CreateProcess(0, commandLine.mutableSpan().data(), 0, 0, true, 0, 0, 0, &startupInfo, &processInformation); diff --git a/Source/WebKit/UIProcess/Media/RemoteMediaSessionManagerProxy.cpp b/Source/WebKit/UIProcess/Media/RemoteMediaSessionManagerProxy.cpp -index e9bf93632655ddfe5104808d67cb1e550e965098..a62ab1cb298654ea69cc8131f275d2cc7fe75602 100644 +index f82f809cf2552bc1a64920efaf8cdbfbfad680da..a9f787163555d265d5209bd9f9f0cda40f9203ce 100644 --- a/Source/WebKit/UIProcess/Media/RemoteMediaSessionManagerProxy.cpp +++ b/Source/WebKit/UIProcess/Media/RemoteMediaSessionManagerProxy.cpp -@@ -31,6 +31,7 @@ +@@ -30,6 +30,7 @@ + #include "MessageSenderInlines.h" - #include "RemoteMediaSessionClientProxy.h" #include "RemoteMediaSessionManagerMessages.h" +#include "RemoteMediaSessionManager.h" #include "RemoteMediaSessionManagerProxyMessages.h" #include "RemoteMediaSessionProxy.h" #include "RemoteMediaSessionState.h" diff --git a/Source/WebKit/UIProcess/PageClient.h b/Source/WebKit/UIProcess/PageClient.h -index ec8a8accd195844f2ceb5fea0d73bc0ab50ce733..164e60cbc7fbd520d4d43a99021891c770f1fe3e 100644 +index ed60169d3da046ea7e98e402b2af032476c87ac9..d8aa7edba575a3c6e596e6535ec1cebe38da7e87 100644 --- a/Source/WebKit/UIProcess/PageClient.h +++ b/Source/WebKit/UIProcess/PageClient.h -@@ -77,6 +77,11 @@ +@@ -78,6 +78,11 @@ #include #endif @@ -12262,7 +12677,7 @@ index ec8a8accd195844f2ceb5fea0d73bc0ab50ce733..164e60cbc7fbd520d4d43a99021891c7 OBJC_CLASS AVPlayerViewController; OBJC_CLASS CALayer; OBJC_CLASS NSFileWrapper; -@@ -400,7 +405,15 @@ public: +@@ -405,7 +410,15 @@ public: virtual void selectionDidChange() = 0; #endif @@ -12281,10 +12696,10 @@ index ec8a8accd195844f2ceb5fea0d73bc0ab50ce733..164e60cbc7fbd520d4d43a99021891c7 diff --git a/Source/WebKit/UIProcess/PlaywrightFullScreenManagerProxyClient.cpp b/Source/WebKit/UIProcess/PlaywrightFullScreenManagerProxyClient.cpp new file mode 100644 -index 0000000000000000000000000000000000000000..95b682567eba682f927317cd3327a531358dfebc +index 0000000000000000000000000000000000000000..e982e8e129b89e33305bb3dd7616fb7ad9b7e1a9 --- /dev/null +++ b/Source/WebKit/UIProcess/PlaywrightFullScreenManagerProxyClient.cpp -@@ -0,0 +1,64 @@ +@@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 Microsoft Corporation. + * @@ -12321,8 +12736,7 @@ index 0000000000000000000000000000000000000000..95b682567eba682f927317cd3327a531 +namespace WebKit { +using namespace WebCore; + -+PlaywrightFullScreenManagerProxyClient::PlaywrightFullScreenManagerProxyClient(WebPageProxy& page) -+ : m_pageProxy(page) ++PlaywrightFullScreenManagerProxyClient::PlaywrightFullScreenManagerProxyClient(WebPageProxy&) +{ +} + @@ -12351,10 +12765,10 @@ index 0000000000000000000000000000000000000000..95b682567eba682f927317cd3327a531 +#endif // ENABLE(FULLSCREEN_API) diff --git a/Source/WebKit/UIProcess/PlaywrightFullScreenManagerProxyClient.h b/Source/WebKit/UIProcess/PlaywrightFullScreenManagerProxyClient.h new file mode 100644 -index 0000000000000000000000000000000000000000..f855bb5ff6e91cc3383fb9a96d32392ff7aa5493 +index 0000000000000000000000000000000000000000..e729f8cbd1dd87a489f226611d9657ad2f5a339a --- /dev/null +++ b/Source/WebKit/UIProcess/PlaywrightFullScreenManagerProxyClient.h -@@ -0,0 +1,56 @@ +@@ -0,0 +1,55 @@ +/* + * Copyright (C) 2023 Microsoft Corporation. + * @@ -12404,7 +12818,6 @@ index 0000000000000000000000000000000000000000..f855bb5ff6e91cc3383fb9a96d32392f + void beganEnterFullScreen(const WebCore::IntRect& initialFrame, const WebCore::IntRect& finalFrame, CompletionHandler&&) override; + void beganExitFullScreen(const WebCore::IntRect& initialFrame, const WebCore::IntRect& finalFrame, CompletionHandler&&) override; + -+ WebPageProxy& m_pageProxy; + bool m_isFullScreen { false }; +}; + @@ -12424,7 +12837,7 @@ index 0e8f21982c6499e6886f5db6c193afc43663f29c..43ed4c8837479744a98ef76ceb2911c0 #include "FrameProcess.h" #include "ProvisionalFrameCreationParameters.h" diff --git a/Source/WebKit/UIProcess/ProvisionalPageProxy.h b/Source/WebKit/UIProcess/ProvisionalPageProxy.h -index 90b6b6b10906e9bf9f4229dc413421380be8a01a..0620c78e35e40e5b4997ab3813cac98cfdef7372 100644 +index 619f6cfac2e777136b4cb91773e996c57708e776..2655dfacb8262c80fe23db5a9d4fd43c8e0c10d0 100644 --- a/Source/WebKit/UIProcess/ProvisionalPageProxy.h +++ b/Source/WebKit/UIProcess/ProvisionalPageProxy.h @@ -32,8 +32,10 @@ @@ -12438,7 +12851,7 @@ index 90b6b6b10906e9bf9f4229dc413421380be8a01a..0620c78e35e40e5b4997ab3813cac98c #include #include #include -@@ -74,7 +76,6 @@ class WebBackForwardListItem; +@@ -76,7 +78,6 @@ class WebBackForwardListItem; class WebFrameProxy; class WebPageProxy; class WebProcessProxy; @@ -12765,7 +13178,7 @@ index 7ef986965d3fda34b4f09279c62bdad40712ab12..5e0bc508f72bbbd0bcb4bb1254782028 namespace WebKit { diff --git a/Source/WebKit/UIProcess/RemoteLayerTree/mac/RemoteScrollingTreeMac.mm b/Source/WebKit/UIProcess/RemoteLayerTree/mac/RemoteScrollingTreeMac.mm -index 07e7aec9d5f53dd4adfb83b6626f469621362663..18d049adb49ab565f60e9e1dded55da1a6aaaf3a 100644 +index 103523006c9f2b5771591082939a14069e12c9b6..0ee29c2c2d14e43c857ec17eec4b312f83b6c2ed 100644 --- a/Source/WebKit/UIProcess/RemoteLayerTree/mac/RemoteScrollingTreeMac.mm +++ b/Source/WebKit/UIProcess/RemoteLayerTree/mac/RemoteScrollingTreeMac.mm @@ -46,6 +46,7 @@ @@ -12794,18 +13207,18 @@ index 3f6cd845733e2e8f5e25c318e7ab54f77032d4cf..e4399bfd734a28ef5df5ec855b7c78b4 class RemotePagePlaybackSessionManagerProxy : public IPC::MessageReceiver, public RefCounted { diff --git a/Source/WebKit/UIProcess/RemotePageProxy.cpp b/Source/WebKit/UIProcess/RemotePageProxy.cpp -index e4c7157e5f1269e8adebbeb63fed76a1fa78ceaf..ad5012a4be5f114c193c4645c2515d492fdb50f0 100644 +index 7590c1860cf7a13dd5100d2868e7ad2bc3d68679..c09c1ba1993913a31418fec10fb2ba17394e2508 100644 --- a/Source/WebKit/UIProcess/RemotePageProxy.cpp +++ b/Source/WebKit/UIProcess/RemotePageProxy.cpp -@@ -37,6 +37,7 @@ - #include "ProvisionalFrameProxy.h" +@@ -39,6 +39,7 @@ #include "RemotePageDrawingAreaProxy.h" #include "RemotePageFullscreenManagerProxy.h" + #include "RemotePageMediaSessionManagerProxy.h" +#include "RemotePagePlaybackSessionManagerProxy.h" #include "RemotePageScreenOrientationManagerProxy.h" #include "RemotePageVisitedLinkStoreRegistration.h" - #include "RemotePageWebDeviceOrientationUpdateProviderProxy.h" -@@ -56,6 +57,7 @@ + #include "RemotePageWebAuthenticatorCoordinatorProxy.h" +@@ -60,6 +61,7 @@ #include #include @@ -12813,25 +13226,6 @@ index e4c7157e5f1269e8adebbeb63fed76a1fa78ceaf..ad5012a4be5f114c193c4645c2515d49 #if ENABLE(FULLSCREEN_API) #include "WebFullScreenManagerProxy.h" #endif -diff --git a/Source/WebKit/UIProcess/TextExtractionAssertionScope.h b/Source/WebKit/UIProcess/TextExtractionAssertionScope.h -index 986c0812fedd2be938675ef5de41041e9fc76a0b..69da96c1e1ccb3b6d6b142ccb3b337f0bcb14957 100644 ---- a/Source/WebKit/UIProcess/TextExtractionAssertionScope.h -+++ b/Source/WebKit/UIProcess/TextExtractionAssertionScope.h -@@ -25,13 +25,12 @@ - - #pragma once - -+#include "WebPageProxy.h" - #include - #include - - namespace WebKit { - --class WebPageProxy; -- - class TextExtractionAssertionScope { - WTF_MAKE_TZONE_ALLOCATED(TextExtractionAssertionScope); - WTF_MAKE_NONCOPYABLE(TextExtractionAssertionScope); diff --git a/Source/WebKit/UIProcess/WebContextMenuProxy.h b/Source/WebKit/UIProcess/WebContextMenuProxy.h index 364666d82ffd69aef6cb4f8d63b61ae13e163d87..a491699c43502e6feae3b3df73b3558a79bb73e3 100644 --- a/Source/WebKit/UIProcess/WebContextMenuProxy.h @@ -13095,7 +13489,7 @@ index 0000000000000000000000000000000000000000..8c772fb1b67ec7d83cbe2394d8ab0478 +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/WebPageInspectorInputAgent.cpp b/Source/WebKit/UIProcess/WebPageInspectorInputAgent.cpp new file mode 100644 -index 0000000000000000000000000000000000000000..a29ae94cda78928fab928bbdd250f8cd1c8b4cc5 +index 0000000000000000000000000000000000000000..a7de88ca37063e44c74348cdeeb2b7d28692df3d --- /dev/null +++ b/Source/WebKit/UIProcess/WebPageInspectorInputAgent.cpp @@ -0,0 +1,397 @@ @@ -13370,22 +13764,22 @@ index 0000000000000000000000000000000000000000..a29ae94cda78928fab928bbdd250f8cd + + // Convert css coordinates to view coordinates (dip). + double totalScale = m_page.pageScaleFactor() * m_page.viewScaleFactor() * m_page.pageZoomFactor(); -+ x = clampToInteger(roundf(x * totalScale)); -+ y = clampToInteger(roundf(y * totalScale)); -+ eventDeltaX = clampToInteger(roundf(eventDeltaX * totalScale)); -+ eventDeltaY = clampToInteger(roundf(eventDeltaY * totalScale)); ++ x = clampTo(roundf(x * totalScale)); ++ y = clampTo(roundf(y * totalScale)); ++ eventDeltaX = clampTo(roundf(eventDeltaX * totalScale)); ++ eventDeltaY = clampTo(roundf(eventDeltaY * totalScale)); + + // We intercept any drags generated by this mouse event + // to prevent them from creating actual drags in the host + // operating system. This is turned off in the callback. + m_page.setInterceptDrags(true); ++ MonotonicTime timestamp = MonotonicTime::now(); +#if PLATFORM(MAC) + UNUSED_VARIABLE(eventType); + UNUSED_VARIABLE(eventButton); + UNUSED_VARIABLE(eventClickCount); -+ platformDispatchMouseEvent(type, x, y, WTF::move(modifiers), button, WTF::move(clickCount), eventButtons); ++ platformDispatchMouseEvent(type, x, y, WTF::move(modifiers), button, WTF::move(clickCount), eventButtons, timestamp); +#elif PLATFORM(GTK) || PLATFORM(WPE) || PLATFORM(WIN) -+ MonotonicTime timestamp = MonotonicTime::now(); + NativeWebMouseEvent event( + eventType, + eventButton, @@ -13483,8 +13877,8 @@ index 0000000000000000000000000000000000000000..a29ae94cda78928fab928bbdd250f8cd + + // Convert css coordinates to view coordinates (dip). + double totalScale = m_page.pageScaleFactor() * m_page.viewScaleFactor() * m_page.pageZoomFactor(); -+ x = clampToInteger(roundf(x * totalScale)); -+ y = clampToInteger(roundf(y * totalScale)); ++ x = clampTo(roundf(x * totalScale)); ++ y = clampTo(roundf(y * totalScale)); + + MonotonicTime timestamp = MonotonicTime::now(); + WebCore::FloatSize delta = {-eventDeltaX, -eventDeltaY}; @@ -13498,7 +13892,7 @@ index 0000000000000000000000000000000000000000..a29ae94cda78928fab928bbdd250f8cd +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/WebPageInspectorInputAgent.h b/Source/WebKit/UIProcess/WebPageInspectorInputAgent.h new file mode 100644 -index 0000000000000000000000000000000000000000..fb0cd51d362dfd8af2370f43ecb8835c96450f21 +index 0000000000000000000000000000000000000000..d57c86a11d49efb8105a871a4b420e2b9190a587 --- /dev/null +++ b/Source/WebKit/UIProcess/WebPageInspectorInputAgent.h @@ -0,0 +1,87 @@ @@ -13572,7 +13966,7 @@ index 0000000000000000000000000000000000000000..fb0cd51d362dfd8af2370f43ecb8835c +private: + void platformDispatchKeyEvent(WebEventType type, const String& text, const String& unmodifiedText, const String& key, const String& code, const String& keyIdentifier, int windowsVirtualKeyCode, int nativeVirtualKeyCode, bool isAutoRepeat, bool isKeypad, bool isSystemKey, OptionSet modifiers, Vector& commands, MonotonicTime timestamp); +#if PLATFORM(MAC) -+ void platformDispatchMouseEvent(const String& type, int x, int y, std::optional&& modifier, const String& button, std::optional&& clickCount, unsigned short buttons); ++ void platformDispatchMouseEvent(const String& type, int x, int y, std::optional&& modifier, const String& button, std::optional&& clickCount, unsigned short buttons, MonotonicTime timestamp); +#endif + + Ref m_backendDispatcher; @@ -13590,10 +13984,10 @@ index 0000000000000000000000000000000000000000..fb0cd51d362dfd8af2370f43ecb8835c + +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp -index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff53bb97bf 100644 +index 5828bd882da8056e3d5dd97b07416ae2f5c0b4e0..96668f979953d797ab8391a0eb89a4109414c22c 100644 --- a/Source/WebKit/UIProcess/WebPageProxy.cpp +++ b/Source/WebKit/UIProcess/WebPageProxy.cpp -@@ -219,6 +219,7 @@ +@@ -220,6 +220,7 @@ #include #include #include @@ -13601,23 +13995,23 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff #include #include #include -@@ -232,6 +233,7 @@ +@@ -233,6 +234,7 @@ #include #include #include +#include + #include #include #include - #include -@@ -260,6 +262,7 @@ - #include +@@ -263,6 +265,7 @@ #include #include + #include +#include #include #include #include -@@ -270,10 +273,12 @@ +@@ -273,11 +276,13 @@ #include #include #include @@ -13625,12 +14019,13 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff #include #include #include + #include #include +#include #include #include #include -@@ -367,7 +372,7 @@ +@@ -371,7 +376,7 @@ #include "ViewSnapshotStore.h" #endif @@ -13639,7 +14034,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff #include #endif -@@ -497,6 +502,17 @@ static constexpr Seconds tryCloseTimeoutDelay = 50_ms; +@@ -509,6 +514,17 @@ static constexpr Seconds tryCloseTimeoutDelay = 50_ms; static constexpr Seconds audibleActivityClearDelay = 10_s; #endif @@ -13657,7 +14052,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff #if PLATFORM(COCOA) static WorkQueue& sharedFileQueueSingleton() { -@@ -1106,6 +1122,10 @@ WebPageProxy::~WebPageProxy() +@@ -1104,6 +1120,10 @@ WebPageProxy::~WebPageProxy() ASSERT(webPageProxyMap().get(m_identifier) == this); webPageProxyMap().remove(m_identifier); @@ -13668,7 +14063,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff } void WebPageProxy::addAllMessageReceivers() -@@ -1679,7 +1699,7 @@ void WebPageProxy::didAttachToRunningProcess() +@@ -1680,7 +1700,7 @@ void WebPageProxy::didAttachToRunningProcess() #if ENABLE(FULLSCREEN_API) ASSERT(!m_fullScreenManager); @@ -13677,7 +14072,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff #endif #if ENABLE(VIDEO_PRESENTATION_MODE) ASSERT(!m_playbackSessionManager); -@@ -1711,7 +1731,7 @@ void WebPageProxy::didAttachToRunningProcess() +@@ -1719,7 +1739,7 @@ void WebPageProxy::didAttachToRunningProcess() #endif #if !PLATFORM(IOS_FAMILY) @@ -13686,15 +14081,15 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff #else auto currentOrientation = toScreenOrientationType(m_deviceOrientation); #endif -@@ -1851,6 +1871,7 @@ void WebPageProxy::initializeWebPage(const Site& site, WebCore::SandboxFlags eff +@@ -1861,6 +1881,7 @@ void WebPageProxy::initializeWebPage(const Site& site, WebCore::SandboxFlags eff if (preferences->siteIsolationEnabled()) browsingContextGroup->addPage(*this); process->send(Messages::WebProcess::CreateWebPage(m_webPageID, creationParameters(process, *protect(drawingArea()), m_mainFrame->frameID(), std::nullopt)), 0); + m_inspectorController->didInitializeWebPage(); - #if ENABLE(WINDOW_PROXY_PROPERTY_ACCESS_NOTIFICATION) - internals().frameLoadStateObserver = WebPageProxyFrameLoadStateObserver::create(); -@@ -2172,6 +2193,21 @@ WebProcessProxy& WebPageProxy::ensureRunningProcess() + process->addVisitedLinkStoreUser(m_visitedLinkStore, identifier()); + +@@ -2177,6 +2198,21 @@ WebProcessProxy& WebPageProxy::ensureRunningProcess() return m_legacyMainFrameProcess; } @@ -13716,7 +14111,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff RefPtr WebPageProxy::loadRequest(WebCore::ResourceRequest&& request, ShouldOpenExternalURLsPolicy shouldOpenExternalURLsPolicy, NavigationUpgradeToHTTPSBehavior navigationUpgradeToHTTPSBehavior, std::unique_ptr&& lastNavigationAction, API::Object* userData, bool isRequestFromClientOrUserInput) { if (m_isClosed) -@@ -2297,11 +2333,29 @@ void WebPageProxy::loadRequestWithNavigationShared(Ref&& proces +@@ -2306,11 +2342,29 @@ void WebPageProxy::loadRequestWithNavigationShared(Ref&& proces navigation->setIsLoadedWithNavigationShared(true); protectedProcess->markProcessAsRecentlyUsed(); @@ -13750,7 +14145,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff }); } -@@ -2897,6 +2951,53 @@ RefPtr WebPageProxy::activeAutomationSession() const +@@ -3019,6 +3073,53 @@ RefPtr WebPageProxy::activeAutomationSession() const return m_configuration->processPool().automationSession(); } @@ -13804,9 +14199,9 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff void WebPageProxy::sendMessageToInspectorFrontend(const String& targetId, const String& message) { m_inspectorController->sendMessageToInspectorFrontend(targetId, message); -@@ -3200,6 +3301,24 @@ void WebPageProxy::updateActivityState(OptionSet flagsToUpdate) - bool wasVisible = isViewVisible(); - RefPtr pageClient = this->pageClient(); +@@ -3324,6 +3425,24 @@ void WebPageProxy::updateActivityState(OptionSet flagsToUpdate) + if (!pageClient) + return; internals().activityState.remove(flagsToUpdate); + + if (m_activeForAutomation) { @@ -13829,7 +14224,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff if (flagsToUpdate & ActivityState::IsFocused && pageClient->isViewFocused()) internals().activityState.add(ActivityState::IsFocused); if (flagsToUpdate & ActivityState::WindowIsActive && pageClient->isViewWindowActive()) -@@ -3994,7 +4113,7 @@ void WebPageProxy::performDragOperation(DragData& dragData, const String& dragSt +@@ -4157,7 +4276,7 @@ void WebPageProxy::performDragOperation(DragData& dragData, const String& dragSt if (!hasRunningProcess()) return; @@ -13838,7 +14233,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff URL url { dragData.asURL() }; if (url.protocolIsFile()) protect(legacyMainFrameProcess())->assumeReadAccessToBaseURL(*this, url.string(), [] { }); -@@ -4037,6 +4156,8 @@ void WebPageProxy::performDragControllerAction(DragControllerAction action, Drag +@@ -4200,6 +4319,8 @@ void WebPageProxy::performDragControllerAction(DragControllerAction action, Drag if (!hasRunningProcess()) return; @@ -13847,7 +14242,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff auto completionHandler = [this, protectedThis = Ref { *this }, action, dragData] (std::optional dragOperation, WebCore::DragHandlingMethod dragHandlingMethod, bool mouseIsOverFileInput, unsigned numberOfItemsToBeAccepted, const IntRect& insertionRect, const IntRect& editableElementRect, std::optional remoteUserInputEventData) mutable { if (!m_pageClient) return; -@@ -4048,7 +4169,7 @@ void WebPageProxy::performDragControllerAction(DragControllerAction action, Drag +@@ -4211,7 +4332,7 @@ void WebPageProxy::performDragControllerAction(DragControllerAction action, Drag dragData.setClientPosition(roundedIntPoint(remoteUserInputEventData->transformedPoint)); performDragControllerAction(action, dragData, remoteUserInputEventData->targetFrameID); }; @@ -13856,7 +14251,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff ASSERT(dragData.platformData()); sendWithAsyncReplyToProcessContainingFrame(frameID, Messages::WebPage::PerformDragControllerAction(action, dragData.clientPosition(), dragData.globalPosition(), dragData.draggingSourceOperationMask(), *dragData.platformData(), dragData.flags()), WTF::move(completionHandler)); #else -@@ -4083,17 +4204,36 @@ void WebPageProxy::didPerformDragControllerAction(std::optionalpageClient()) pageClient->didPerformDragControllerAction(); @@ -13896,7 +14291,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff didStartDrag(); } #endif -@@ -4114,6 +4254,24 @@ void WebPageProxy::dragEnded(const IntPoint& clientPosition, const IntPoint& glo +@@ -4277,6 +4417,24 @@ void WebPageProxy::dragEnded(const IntPoint& clientPosition, const IntPoint& glo setDragCaretRect({ }); } @@ -13921,7 +14316,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff void WebPageProxy::didStartDrag(const std::optional& targetFrameID) { if (!hasRunningProcess()) -@@ -4122,6 +4280,25 @@ void WebPageProxy::didStartDrag(const std::optional& targetFram +@@ -4285,6 +4443,25 @@ void WebPageProxy::didStartDrag(const std::optional& targetFram discardQueuedMouseEvents(); sendToProcessContainingFrame(targetFrameID, Messages::WebPage::DidStartDrag(targetFrameID)); @@ -13947,7 +14342,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff } void WebPageProxy::dragCancelled() -@@ -4306,26 +4483,47 @@ void WebPageProxy::processNextQueuedMouseEvent() +@@ -4469,26 +4646,47 @@ void WebPageProxy::processNextQueuedMouseEvent() auto eventType = event->type(); startResponsivenessTimerForMouseEvent(*targetFrame, eventType); @@ -14007,7 +14402,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff } #if ENABLE(MAC_GESTURE_EVENTS) -@@ -4592,6 +4790,8 @@ void WebPageProxy::wheelEventHandlingCompleted(bool wasHandled) +@@ -4751,6 +4949,8 @@ void WebPageProxy::wheelEventHandlingCompleted(bool wasHandled) if (RefPtr automationSession = m_configuration->processPool().automationSession()) automationSession->wheelEventsFlushedForPage(*this); @@ -14016,7 +14411,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff } void WebPageProxy::cacheWheelEventScrollingAccelerationCurve(const NativeWebWheelEvent& nativeWheelEvent) -@@ -4735,7 +4935,7 @@ static TrackingType mergeTrackingTypes(TrackingType a, TrackingType b) +@@ -4894,7 +5094,7 @@ static TrackingType mergeTrackingTypes(TrackingType a, TrackingType b) void WebPageProxy::updateTouchEventTracking(const WebTouchEvent& touchStartEvent) { @@ -14025,7 +14420,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff for (auto& touchPoint : touchStartEvent.touchPoints()) { auto location = touchPoint.locationInRootView(); auto update = [this, location](TrackingType& trackingType, EventTrackingRegions::EventType eventType) { -@@ -5577,6 +5777,7 @@ Ref WebPageProxy::navigationOriginatingPage(const FrameInfoData& f +@@ -5759,6 +5959,7 @@ Ref WebPageProxy::navigationOriginatingPage(const FrameInfoData& f void WebPageProxy::receivedPolicyDecision(PolicyAction action, API::Navigation* navigation, std::optional, Ref>>&& websitePoliciesAndProcess, Ref&& navigationAction, WillContinueLoadInNewProcess willContinueLoadInNewProcess, std::optional sandboxExtensionHandle, std::optional&& consoleMessage, CompletionHandler&& completionHandler) { @@ -14033,15 +14428,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff if (!hasRunningProcess()) return completionHandler(PolicyDecision { }); -@@ -6677,6 +6878,7 @@ void WebPageProxy::viewScaleFactorDidChange(IPC::Connection& connection, double - MESSAGE_CHECK_BASE(scaleFactorIsValid(scaleFactor), connection); - if (!legacyMainFrameProcess().hasConnection(connection)) - return; -+ m_viewScaleFactor = scaleFactor; - - forEachWebContentProcess([&] (auto& process, auto pageID) { - if (&process == &legacyMainFrameProcess()) -@@ -7530,6 +7732,7 @@ void WebPageProxy::didDestroyNavigationShared(Ref&& process, We +@@ -7816,6 +8017,7 @@ void WebPageProxy::didDestroyNavigationShared(Ref&& process, We RefPtr protectedPageClient { pageClient() }; m_navigationState->didDestroyNavigation(process->coreProcessIdentifier(), navigationID); @@ -14049,7 +14436,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff } void WebPageProxy::didStartProvisionalLoadForFrame(IPC::Connection& connection, FrameIdentifier frameID, FrameInfoData&& frameInfo, ResourceRequest&& request, std::optional navigationID, URL&& url, URL&& unreachableURL, const UserData& userData, WallTime timestamp) -@@ -7924,6 +8127,8 @@ void WebPageProxy::didFailProvisionalLoadForFrameShared(Ref&& p +@@ -8209,6 +8411,8 @@ void WebPageProxy::didFailProvisionalLoadForFrameShared(Ref&& p m_failingProvisionalLoadURL = { }; m_allowsLoadingAlternateHTMLForFailingProvisionalLoadURL = true; @@ -14058,7 +14445,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff // If the provisional page's load fails then we destroy the provisional page. if (m_provisionalPage && m_provisionalPage->mainFrame() == &frame && (willContinueLoading == WillContinueLoading::No)) m_provisionalPage = nullptr; -@@ -9693,6 +9898,8 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w +@@ -10212,6 +10416,8 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w if (RefPtr page = originatingFrameInfo->page()) openerAppInitiatedState = page->lastNavigationWasAppInitiated(); @@ -14067,7 +14454,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff auto completionHandler = [ this, protectedThis = Ref { *this }, -@@ -9780,6 +9987,7 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w +@@ -10305,6 +10511,7 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w configuration->setInitialReferrerPolicy(effectiveReferrerPolicy); configuration->setWindowFeatures(WTF::move(windowFeatures)); configuration->setOpenedMainFrameName(openedMainFrameName); @@ -14075,7 +14462,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff if (RefPtr openerFrame = WebFrameProxy::webFrame(originatingFrameInfoData.frameID); navigationActionData.hasOpener && openerFrame) { configuration->setRelatedPage(*this); -@@ -9815,6 +10023,7 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w +@@ -10340,6 +10547,7 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w void WebPageProxy::showPage() { m_uiClient->showPage(this); @@ -14083,7 +14470,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff } bool WebPageProxy::hasOpenedPage() const -@@ -9958,6 +10167,10 @@ void WebPageProxy::closePage() +@@ -10483,6 +10691,10 @@ void WebPageProxy::closePage() if (isClosed()) return; @@ -14094,7 +14481,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff WEBPAGEPROXY_RELEASE_LOG(Process, "closePage:"); if (RefPtr pageClient = this->pageClient()) pageClient->clearAllEditCommands(); -@@ -9997,6 +10210,8 @@ void WebPageProxy::runJavaScriptAlert(IPC::Connection& connection, FrameIdentifi +@@ -10522,6 +10734,8 @@ void WebPageProxy::runJavaScriptAlert(IPC::Connection& connection, FrameIdentifi auto showModal = [protectedThis = Ref { *this }](RefPtr&& frame, FrameInfoData&& frameInfo, String&& message, CompletionHandler&& reply) mutable { protectedThis->runModalJavaScriptDialog(WTF::move(frame), WTF::move(frameInfo), WTF::move(message), [reply = WTF::move(reply)](WebPageProxy& page, WebFrameProxy* frame, FrameInfoData&& frameInfo, String&& message, CompletionHandler&& completion) mutable { @@ -14103,7 +14490,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff page.m_uiClient->runJavaScriptAlert(page, WTF::move(message), frame, WTF::move(frameInfo), [reply = WTF::move(reply), completion = WTF::move(completion)]() mutable { reply(); completion(); -@@ -10030,6 +10245,8 @@ void WebPageProxy::runJavaScriptConfirm(IPC::Connection& connection, FrameIdenti +@@ -10555,6 +10769,8 @@ void WebPageProxy::runJavaScriptConfirm(IPC::Connection& connection, FrameIdenti if (RefPtr automationSession = configuration().processPool().automationSession()) automationSession->willShowJavaScriptDialog(*this, message, std::nullopt); } @@ -14112,7 +14499,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff auto showModal = [protectedThis = Ref { *this }](RefPtr&& frame, FrameInfoData&& frameInfo, String&& message, CompletionHandler&& reply) mutable { protectedThis->runModalJavaScriptDialog(WTF::move(frame), WTF::move(frameInfo), WTF::move(message), [reply = WTF::move(reply)](WebPageProxy& page, WebFrameProxy* frame, FrameInfoData&& frameInfo, String&& message, CompletionHandler&& completion) mutable { -@@ -10066,6 +10283,8 @@ void WebPageProxy::runJavaScriptPrompt(IPC::Connection& connection, FrameIdentif +@@ -10591,6 +10807,8 @@ void WebPageProxy::runJavaScriptPrompt(IPC::Connection& connection, FrameIdentif if (RefPtr automationSession = configuration().processPool().automationSession()) automationSession->willShowJavaScriptDialog(*this, message, defaultValue); } @@ -14121,7 +14508,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff auto showModal = [protectedThis = Ref { *this }](RefPtr&& frame, FrameInfoData&& frameInfo, String&& message, String&& defaultValue, CompletionHandler&& reply) mutable { protectedThis->runModalJavaScriptDialog(WTF::move(frame), WTF::move(frameInfo), WTF::move(message), [reply = WTF::move(reply), defaultValue = WTF::move(defaultValue)](WebPageProxy& page, WebFrameProxy* frame, FrameInfoData&& frameInfo, String&& message, CompletionHandler&& completion) mutable { -@@ -10280,6 +10499,8 @@ void WebPageProxy::runBeforeUnloadConfirmPanel(IPC::Connection& connection, Fram +@@ -10805,6 +11023,8 @@ void WebPageProxy::runBeforeUnloadConfirmPanel(IPC::Connection& connection, Fram return; } } @@ -14130,7 +14517,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff // Since runBeforeUnloadConfirmPanel() can spin a nested run loop we need to turn off the responsiveness timer and the tryClose timer. webProcess->stopResponsivenessTimer(); -@@ -10969,6 +11190,11 @@ void WebPageProxy::resourceLoadDidCompleteWithError(ResourceLoadInfo&& loadInfo, +@@ -11494,6 +11714,11 @@ void WebPageProxy::resourceLoadDidCompleteWithError(ResourceLoadInfo&& loadInfo, } #if ENABLE(FULLSCREEN_API) @@ -14142,7 +14529,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff WebFullScreenManagerProxy* WebPageProxy::fullScreenManager() { return m_fullScreenManager.get(); -@@ -11098,6 +11324,17 @@ void WebPageProxy::requestDOMPasteAccess(IPC::Connection& connection, DOMPasteAc +@@ -11606,6 +11831,17 @@ void WebPageProxy::requestDOMPasteAccess(IPC::Connection& connection, DOMPasteAc } } @@ -14160,7 +14547,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff protect(pageClient())->requestDOMPasteAccess(pasteAccessCategory, requiresInteraction, elementRect, originIdentifier, WTF::move(completionHandler)); } -@@ -12080,6 +12317,8 @@ void WebPageProxy::mouseEventHandlingCompleted(std::optional event +@@ -12595,6 +12831,8 @@ void WebPageProxy::mouseEventHandlingCompleted(std::optional event if (RefPtr automationSession = configuration().processPool().automationSession()) automationSession->mouseEventsFlushedForPage(*this); didFinishProcessingAllPendingMouseEvents(); @@ -14169,7 +14556,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff } } -@@ -12138,6 +12377,7 @@ void WebPageProxy::keyEventHandlingCompleted(std::optional eventTy +@@ -12653,6 +12891,7 @@ void WebPageProxy::keyEventHandlingCompleted(std::optional eventTy if (RefPtr automationSession = configuration().processPool().automationSession()) automationSession->keyboardEventsFlushedForPage(*this); didFinishProcessingAllPendingKeyEvents(); @@ -14177,7 +14564,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff } } -@@ -12589,7 +12829,10 @@ void WebPageProxy::dispatchProcessDidTerminate(WebProcessProxy& process, Process +@@ -13104,7 +13343,10 @@ void WebPageProxy::dispatchProcessDidTerminate(WebProcessProxy& process, Process protect(browsingContextGroup())->processDidTerminate(*this, process); } @@ -14189,7 +14576,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff if (m_loaderClient) handledByClient = reason != ProcessTerminationReason::RequestedByClient && m_loaderClient->processDidCrash(*this); else -@@ -13264,6 +13507,9 @@ WebPageCreationParameters WebPageProxy::creationParameters(WebProcessProxy& proc +@@ -13785,6 +14027,9 @@ WebPageCreationParameters WebPageProxy::creationParameters(WebProcessProxy& proc parameters.allowPostingLegacySynchronousMessages = m_configuration->allowPostingLegacySynchronousMessages(); parameters.backgroundTextExtractionEnabled = m_configuration->backgroundTextExtractionEnabled(); @@ -14199,7 +14586,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff #if ENABLE(APP_HIGHLIGHTS) parameters.appHighlightsVisible = appHighlightsVisibility() ? HighlightVisibility::Visible : HighlightVisibility::Hidden; #endif -@@ -13442,8 +13688,47 @@ void WebPageProxy::allowGamepadAccess() +@@ -13964,8 +14209,47 @@ void WebPageProxy::allowGamepadAccess() #endif // ENABLE(GAMEPAD) @@ -14247,7 +14634,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff if (negotiatedLegacyTLS == NegotiatedLegacyTLS::Yes) { m_navigationClient->shouldAllowLegacyTLS(*this, authenticationChallenge.get(), [this, protectedThis = Ref { *this }, authenticationChallenge] (bool shouldAllowLegacyTLS) { if (shouldAllowLegacyTLS) -@@ -13539,6 +13824,12 @@ void WebPageProxy::requestGeolocationPermissionForFrame(IPC::Connection& connect +@@ -14061,6 +14345,12 @@ void WebPageProxy::requestGeolocationPermissionForFrame(IPC::Connection& connect request->deny(); }; @@ -14260,7 +14647,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff // FIXME: Once iOS migrates to the new WKUIDelegate SPI, clean this up // and make it one UIClient call that calls the completionHandler with false // if there is no delegate instead of returning the completionHandler -@@ -13647,6 +13938,12 @@ void WebPageProxy::queryPermission(const ClientOrigin& clientOrigin, const Permi +@@ -14169,6 +14459,12 @@ void WebPageProxy::queryPermission(const ClientOrigin& clientOrigin, const Permi shouldChangeDeniedToPrompt = false; if (sessionID().isEphemeral()) { @@ -14273,7 +14660,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff completionHandler(shouldChangeDeniedToPrompt ? PermissionState::Prompt : PermissionState::Denied); return; } -@@ -13661,6 +13958,12 @@ void WebPageProxy::queryPermission(const ClientOrigin& clientOrigin, const Permi +@@ -14183,6 +14479,12 @@ void WebPageProxy::queryPermission(const ClientOrigin& clientOrigin, const Permi return; } @@ -14287,7 +14674,7 @@ index 6d146def06cd6dc28a77e362d0dcd271571ed165..0d01738ce95c10aaa45922ab954b3fff completionHandler(shouldChangeDeniedToPrompt ? PermissionState::Prompt : PermissionState::Denied); return; diff --git a/Source/WebKit/UIProcess/WebPageProxy.h b/Source/WebKit/UIProcess/WebPageProxy.h -index 7986a8110cb475c181cde5446e230bbc58b3408c..cb5f630fc1d420c4ff045ec20f9bb0e6fae6a816 100644 +index fc916a39ed7e220e7dc715609ce30d8c0d0ce729..747653a1589a83cbf0aaa4c2b2c912e879970c05 100644 --- a/Source/WebKit/UIProcess/WebPageProxy.h +++ b/Source/WebKit/UIProcess/WebPageProxy.h @@ -28,6 +28,7 @@ @@ -14296,9 +14683,9 @@ index 7986a8110cb475c181cde5446e230bbc58b3408c..cb5f630fc1d420c4ff045ec20f9bb0e6 #include "APIObject.h" +#include "APIWebsitePolicies.h" #include "MessageReceiver.h" + #include "TextExtractionAssertionScope.h" #include - #include -@@ -40,6 +41,20 @@ +@@ -41,6 +42,20 @@ #include #include #include @@ -14319,7 +14706,7 @@ index 7986a8110cb475c181cde5446e230bbc58b3408c..cb5f630fc1d420c4ff045ec20f9bb0e6 #if USE(COORDINATED_GRAPHICS) && HAVE(DISPLAY_LINK) #include "DisplayLinkObserverID.h" -@@ -127,6 +142,7 @@ class DragData; +@@ -132,6 +147,7 @@ class DragData; class Exception; class FloatPoint; class FloatQuad; @@ -14327,7 +14714,7 @@ index 7986a8110cb475c181cde5446e230bbc58b3408c..cb5f630fc1d420c4ff045ec20f9bb0e6 class FloatRect; class FloatSize; class FontAttributeChanges; -@@ -841,6 +857,8 @@ public: +@@ -847,6 +863,8 @@ public: RefPtr activeAutomationSession() const; @@ -14336,7 +14723,7 @@ index 7986a8110cb475c181cde5446e230bbc58b3408c..cb5f630fc1d420c4ff045ec20f9bb0e6 WebPageInspectorController& inspectorController() LIFETIME_BOUND { return m_inspectorController.get(); } #if PLATFORM(IOS_FAMILY) -@@ -874,6 +892,7 @@ public: +@@ -881,6 +899,7 @@ public: bool NODELETE hasSleepDisabler() const; #if ENABLE(FULLSCREEN_API) @@ -14344,7 +14731,7 @@ index 7986a8110cb475c181cde5446e230bbc58b3408c..cb5f630fc1d420c4ff045ec20f9bb0e6 WebFullScreenManagerProxy* NODELETE fullScreenManager(); void setFullScreenClientForTesting(std::unique_ptr&&); -@@ -942,6 +961,12 @@ public: +@@ -944,6 +963,12 @@ public: void setPageLoadStateObserver(RefPtr&&); @@ -14357,7 +14744,7 @@ index 7986a8110cb475c181cde5446e230bbc58b3408c..cb5f630fc1d420c4ff045ec20f9bb0e6 void initializeWebPage(const WebCore::Site&, WebCore::SandboxFlags, WebCore::ReferrerPolicy); void setDrawingArea(RefPtr&&); -@@ -973,6 +998,8 @@ public: +@@ -975,6 +1000,8 @@ public: RefPtr loadRequest(WebCore::ResourceRequest&&, WebCore::ShouldOpenExternalURLsPolicy, WebCore::NavigationUpgradeToHTTPSBehavior); RefPtr loadRequest(WebCore::ResourceRequest&&, WebCore::ShouldOpenExternalURLsPolicy, WebCore::NavigationUpgradeToHTTPSBehavior, std::unique_ptr&&, API::Object* userData = nullptr, bool isRequestFromClientOrUserInput = true); @@ -14366,7 +14753,7 @@ index 7986a8110cb475c181cde5446e230bbc58b3408c..cb5f630fc1d420c4ff045ec20f9bb0e6 RefPtr loadFile(const String& fileURL, const String& resourceDirectoryURL, bool isAppInitiated = true, API::Object* userData = nullptr); RefPtr loadData(Ref&&, const String& MIMEType, const String& encoding, const String& baseURL, API::Object* userData = nullptr); RefPtr loadData(Ref&&, const String& MIMEType, const String& encoding, const String& baseURL, API::Object* userData, WebCore::ShouldOpenExternalURLsPolicy); -@@ -1074,6 +1101,7 @@ public: +@@ -1076,6 +1103,7 @@ public: void restoreSelectionInFocusedEditableElement(); PageClient* NODELETE pageClient() const; @@ -14374,7 +14761,7 @@ index 7986a8110cb475c181cde5446e230bbc58b3408c..cb5f630fc1d420c4ff045ec20f9bb0e6 void setViewNeedsDisplay(const WebCore::Region&); void requestScroll(const WebCore::FloatPoint& scrollPosition, const WebCore::IntPoint& scrollOrigin, WebCore::ScrollIsAnimated, WebCore::InterruptScrollAnimation); -@@ -1757,11 +1785,14 @@ public: +@@ -1761,11 +1789,14 @@ public: void didStartDrag(const std::optional& = std::nullopt); void dragCancelled(); void setDragCaretRect(const WebCore::IntRect&); @@ -14389,7 +14776,7 @@ index 7986a8110cb475c181cde5446e230bbc58b3408c..cb5f630fc1d420c4ff045ec20f9bb0e6 #endif #if PLATFORM(GTK) || PLATFORM(WPE) void startDrag(WebCore::SelectionData&&, OptionSet, std::optional&& dragImage, WebCore::IntPoint&& dragImageHotspot); -@@ -1769,6 +1800,9 @@ public: +@@ -1773,6 +1804,9 @@ public: #if ENABLE(MODEL_PROCESS) void modelDragEnded(const WebCore::NodeIdentifier); #endif @@ -14399,7 +14786,7 @@ index 7986a8110cb475c181cde5446e230bbc58b3408c..cb5f630fc1d420c4ff045ec20f9bb0e6 #endif void processDidBecomeUnresponsive(WebProcessProxy&); -@@ -2032,6 +2066,7 @@ public: +@@ -2036,6 +2070,7 @@ public: void setViewportSizeForCSSViewportUnits(const WebCore::FloatSize&); WebCore::FloatSize NODELETE viewportSizeForCSSViewportUnits() const; @@ -14407,7 +14794,7 @@ index 7986a8110cb475c181cde5446e230bbc58b3408c..cb5f630fc1d420c4ff045ec20f9bb0e6 void didReceiveAuthenticationChallengeProxy(Ref&&, NegotiatedLegacyTLS); void negotiatedLegacyTLS(); void didNegotiateModernTLS(const URL&); -@@ -2065,6 +2100,8 @@ public: +@@ -2069,6 +2104,8 @@ public: // TODO Replace RefPtr with Expected for error reporting https://webkit.org/b/300271 RefPtr takeViewSnapshot(std::optional&&); RefPtr takeViewSnapshot(std::optional&&, ForceSoftwareCapturingViewportSnapshot); @@ -14416,7 +14803,7 @@ index 7986a8110cb475c181cde5446e230bbc58b3408c..cb5f630fc1d420c4ff045ec20f9bb0e6 #endif void serializeAndWrapCryptoKey(IPC::Connection&, WebCore::CryptoKeyData&&, CompletionHandler>&&)>&&); -@@ -3205,6 +3242,7 @@ private: +@@ -3226,6 +3263,7 @@ private: RefPtr launchProcessForReload(); void requestNotificationPermission(const String& originString, CompletionHandler&&); @@ -14424,7 +14811,7 @@ index 7986a8110cb475c181cde5446e230bbc58b3408c..cb5f630fc1d420c4ff045ec20f9bb0e6 #if ENABLE(WEB_ARCHIVE) bool NODELETE shouldAlwaysPromptForPermission(WebCore::PermissionName) const; -@@ -3755,11 +3793,13 @@ private: +@@ -3789,11 +3827,13 @@ private: String m_openedMainFrameName; RefPtr m_inspector; @@ -14438,7 +14825,7 @@ index 7986a8110cb475c181cde5446e230bbc58b3408c..cb5f630fc1d420c4ff045ec20f9bb0e6 RefPtr m_fullScreenManager; std::unique_ptr m_fullscreenClient; #endif -@@ -3965,6 +4005,22 @@ private: +@@ -4000,6 +4040,22 @@ private: std::optional m_currentDragOperation; bool m_currentDragIsOverFileInput { false }; unsigned m_currentDragNumberOfFilesToBeAccepted { 0 }; @@ -14461,7 +14848,7 @@ index 7986a8110cb475c181cde5446e230bbc58b3408c..cb5f630fc1d420c4ff045ec20f9bb0e6 #endif bool m_mainFrameHasHorizontalScrollbar { false }; -@@ -4136,6 +4192,11 @@ private: +@@ -4167,6 +4223,11 @@ private: RefPtr messageBody; }; Vector m_pendingInjectedBundleMessages; @@ -14474,7 +14861,7 @@ index 7986a8110cb475c181cde5446e230bbc58b3408c..cb5f630fc1d420c4ff045ec20f9bb0e6 #if PLATFORM(IOS_FAMILY) && ENABLE(DEVICE_ORIENTATION) RefPtr m_webDeviceOrientationUpdateProviderProxy; diff --git a/Source/WebKit/UIProcess/WebPageProxy.messages.in b/Source/WebKit/UIProcess/WebPageProxy.messages.in -index 155543cbd71b814586ab391daaa2efa2d11c22ea..b39e2260133f4aa7fe9580496bd4237b3fd52629 100644 +index 98ac446552a0ba482be6f18ea8c5d1d4cab62d24..52ea072824d6b97bfe58e311b93a0237d03cf566 100644 --- a/Source/WebKit/UIProcess/WebPageProxy.messages.in +++ b/Source/WebKit/UIProcess/WebPageProxy.messages.in @@ -35,6 +35,7 @@ messages -> WebPageProxy { @@ -14497,7 +14884,7 @@ index 155543cbd71b814586ab391daaa2efa2d11c22ea..b39e2260133f4aa7fe9580496bd4237b WillReceiveEditDragSnapshot() DidReceiveEditDragSnapshot(RefPtr textIndicator) diff --git a/Source/WebKit/UIProcess/WebProcessCache.cpp b/Source/WebKit/UIProcess/WebProcessCache.cpp -index 1593c4e060fb5fbf399c7d53cb7a81896b945556..c10a4b847f118012380a8d8f35984c81c841d697 100644 +index 81a690dd2023557c4b2f26a367c0dd8c86a795bd..c18c6cba370f510a44a2bee0876188f7480b1b47 100644 --- a/Source/WebKit/UIProcess/WebProcessCache.cpp +++ b/Source/WebKit/UIProcess/WebProcessCache.cpp @@ -125,6 +125,10 @@ bool WebProcessCache::canCacheProcess(WebProcessProxy& process) const @@ -14512,10 +14899,10 @@ index 1593c4e060fb5fbf399c7d53cb7a81896b945556..c10a4b847f118012380a8d8f35984c81 } diff --git a/Source/WebKit/UIProcess/WebProcessPool.cpp b/Source/WebKit/UIProcess/WebProcessPool.cpp -index c9e5f1f1a7dfed59616429838f635538c34124f9..af87d3e1c00f0513658180c5fb397621c1b5505a 100644 +index 7eb5a50ca22231a40f554b4ed88b063e18777654..18eaefd6ae145dd3918e9cc00c1b8131be23095a 100644 --- a/Source/WebKit/UIProcess/WebProcessPool.cpp +++ b/Source/WebKit/UIProcess/WebProcessPool.cpp -@@ -423,10 +423,10 @@ void WebProcessPool::setAutomationClient(std::unique_ptr& +@@ -428,10 +428,10 @@ void WebProcessPool::setAutomationClient(std::unique_ptr& void WebProcessPool::setOverrideLanguages(Vector&& languages) { @@ -14528,7 +14915,7 @@ index c9e5f1f1a7dfed59616429838f635538c34124f9..af87d3e1c00f0513658180c5fb397621 #if ENABLE(GPU_PROCESS) if (RefPtr gpuProcess = GPUProcessProxy::singletonIfCreated()) -@@ -434,9 +434,10 @@ void WebProcessPool::setOverrideLanguages(Vector&& languages) +@@ -439,9 +439,10 @@ void WebProcessPool::setOverrideLanguages(Vector&& languages) #endif #if USE(SOUP) for (Ref networkProcess : NetworkProcessProxy::allNetworkProcesses()) @@ -14540,7 +14927,7 @@ index c9e5f1f1a7dfed59616429838f635538c34124f9..af87d3e1c00f0513658180c5fb397621 void WebProcessPool::fullKeyboardAccessModeChanged(bool fullKeyboardAccessEnabled) { -@@ -977,7 +978,7 @@ void WebProcessPool::initializeNewWebProcess(WebProcessProxy& process, WebsiteDa +@@ -987,7 +988,7 @@ void WebProcessPool::initializeNewWebProcess(WebProcessProxy& process, WebsiteDa #endif parameters.cacheModel = LegacyGlobalSettings::singleton().cacheModel(); @@ -14549,7 +14936,7 @@ index c9e5f1f1a7dfed59616429838f635538c34124f9..af87d3e1c00f0513658180c5fb397621 LOG_WITH_STREAM(Language, stream << "WebProcessPool is initializing a new web process with overrideLanguages: " << parameters.overrideLanguages); parameters.urlSchemesRegisteredAsSecure = copyToVector(LegacyGlobalSettings::singleton().schemesToRegisterAsSecure()); -@@ -1056,7 +1057,7 @@ void WebProcessPool::initializeNewWebProcess(WebProcessProxy& process, WebsiteDa +@@ -1069,7 +1070,7 @@ void WebProcessPool::initializeNewWebProcess(WebProcessProxy& process, WebsiteDa if (!injectedBundleInitializationUserData) injectedBundleInitializationUserData = m_injectedBundleInitializationUserData; parameters.initializationUserData = UserData(process.transformObjectsToHandles(injectedBundleInitializationUserData.get())); @@ -14558,7 +14945,7 @@ index c9e5f1f1a7dfed59616429838f635538c34124f9..af87d3e1c00f0513658180c5fb397621 if (websiteDataStore) parameters.websiteDataStoreParameters = webProcessDataStoreParameters(process, *websiteDataStore); -@@ -1150,14 +1151,14 @@ void WebProcessPool::processDidFinishLaunching(WebProcessProxy& process) +@@ -1163,14 +1164,14 @@ void WebProcessPool::processDidFinishLaunching(WebProcessProxy& process) // Sometimes the memorySampler gets initialized after process initialization has happened but before the process has finished launching // so check if it needs to be started here if (m_memorySamplerEnabled) { @@ -14575,7 +14962,7 @@ index c9e5f1f1a7dfed59616429838f635538c34124f9..af87d3e1c00f0513658180c5fb397621 process.send(Messages::WebProcess::StartMemorySampler(WTF::move(sampleLogSandboxHandle), sampleLogFilePath, m_memorySamplerInterval), 0); } -@@ -1327,6 +1328,12 @@ Ref WebProcessPool::createWebPage(PageClient& pageClient, Ref WebProcessPool::createWebPage(PageClient& pageClient, Refpreferences())->forceEnhancedSecurity() || pageConfiguration->isEnhancedSecurityEnabled() || useEnhancedSecurityFallback) ? EnhancedSecurity::EnabledPolicy : EnhancedSecurity::Disabled; RefPtr relatedPage = pageConfiguration->relatedPage(); @@ -14586,9 +14973,9 @@ index c9e5f1f1a7dfed59616429838f635538c34124f9..af87d3e1c00f0513658180c5fb397621 + pageConfiguration->preferences().setStorageBlockingPolicy(relatedPage->preferences().storageBlockingPolicy()); + bool siteIsolationEnabled = protect(pageConfiguration->preferences())->siteIsolationEnabled(); - if (siteIsolationEnabled) - protect(pageConfiguration->preferences())->setUseUIProcessForBackForwardItemLoading(true); -@@ -1358,9 +1365,9 @@ Ref WebProcessPool::createWebPage(PageClient& pageClient, Ref preferences = pageConfiguration->preferences(); +@@ -1379,9 +1386,9 @@ Ref WebProcessPool::createWebPage(PageClient& pageClient, RefuserContentController(); @@ -14600,7 +14987,7 @@ index c9e5f1f1a7dfed59616429838f635538c34124f9..af87d3e1c00f0513658180c5fb397621 process->setAllowTestOnlyIPC(pageConfiguration->allowTestOnlyIPC()); auto page = process->createWebPage(pageClient, WTF::move(pageConfiguration)); -@@ -1745,18 +1752,18 @@ void WebProcessPool::setEnhancedAccessibility(bool flag) +@@ -1762,18 +1769,18 @@ void WebProcessPool::setEnhancedAccessibility(bool flag) { sendToAllProcesses(Messages::WebProcess::SetEnhancedAccessibility(flag)); } @@ -14623,7 +15010,7 @@ index c9e5f1f1a7dfed59616429838f635538c34124f9..af87d3e1c00f0513658180c5fb397621 for (auto& process : m_processes) { if (!process->canSendMessage()) continue; -@@ -1775,10 +1782,10 @@ void WebProcessPool::startMemorySampler(const double interval) +@@ -1792,10 +1799,10 @@ void WebProcessPool::startMemorySampler(const double interval) } void WebProcessPool::stopMemorySampler() @@ -14636,7 +15023,7 @@ index c9e5f1f1a7dfed59616429838f635538c34124f9..af87d3e1c00f0513658180c5fb397621 // For UIProcess #if ENABLE(MEMORY_SAMPLER) WebMemorySampler::singleton()->stop(); -@@ -1830,7 +1837,7 @@ void WebProcessPool::setAutomationSession(RefPtr&& automat +@@ -1847,7 +1854,7 @@ void WebProcessPool::setAutomationSession(RefPtr&& automat { if (RefPtr previousSession = m_automationSession) previousSession->setProcessPool(nullptr); @@ -14645,7 +15032,7 @@ index c9e5f1f1a7dfed59616429838f635538c34124f9..af87d3e1c00f0513658180c5fb397621 m_automationSession = WTF::move(automationSession); #if ENABLE(REMOTE_INSPECTOR) -@@ -2403,7 +2410,7 @@ std::tuple, RefPtr, ASCIILiteral> WebPr +@@ -2438,7 +2445,7 @@ std::tuple, RefPtr, ASCIILiteral> WebPr } auto reason = "Navigation is cross-site"_s; @@ -14654,7 +15041,7 @@ index c9e5f1f1a7dfed59616429838f635538c34124f9..af87d3e1c00f0513658180c5fb397621 if (m_configuration->alwaysKeepAndReuseSwappedProcesses()) { LOG(ProcessSwapping, "(ProcessSwapping) Considering re-use of a previously cached process for domain %s", targetSite.domain().string().utf8().data()); -@@ -2508,7 +2515,7 @@ void WebProcessPool::setDomainsWithUserInteraction(HashSet>&& domains, CompletionHandler&& completionHandler) @@ -14664,10 +15051,10 @@ index c9e5f1f1a7dfed59616429838f635538c34124f9..af87d3e1c00f0513658180c5fb397621 for (Ref process : borrow(this->processes()).get()) diff --git a/Source/WebKit/UIProcess/WebProcessProxy.cpp b/Source/WebKit/UIProcess/WebProcessProxy.cpp -index fe73856c57fa63cb42aa87d8fbdbf9213b60d903..5a5bf311a3e8c8f7630308d645076d518102f933 100644 +index 4cb137d51b39aeb46c2b61218f5df9d73f920dac..237b21ea3cb10b0f36898080439a374dc4f0256b 100644 --- a/Source/WebKit/UIProcess/WebProcessProxy.cpp +++ b/Source/WebKit/UIProcess/WebProcessProxy.cpp -@@ -215,6 +215,11 @@ Vector> WebProcessProxy::allProcesses() +@@ -216,6 +216,11 @@ Vector> WebProcessProxy::allProcesses() }); } @@ -14679,7 +15066,7 @@ index fe73856c57fa63cb42aa87d8fbdbf9213b60d903..5a5bf311a3e8c8f7630308d645076d51 RefPtr WebProcessProxy::processForIdentifier(ProcessIdentifier identifier) { return allProcessMap().get(identifier); -@@ -583,6 +588,26 @@ void WebProcessProxy::getLaunchOptions(ProcessLauncher::LaunchOptions& launchOpt +@@ -595,6 +600,26 @@ void WebProcessProxy::getLaunchOptions(ProcessLauncher::LaunchOptions& launchOpt if (WebKit::isInspectorProcessPool(processPool())) launchOptions.extraInitializationData.add("inspector-process"_s, "1"_s); @@ -14707,22 +15094,23 @@ index fe73856c57fa63cb42aa87d8fbdbf9213b60d903..5a5bf311a3e8c8f7630308d645076d51 if (isPrewarmed()) diff --git a/Source/WebKit/UIProcess/WebProcessProxy.h b/Source/WebKit/UIProcess/WebProcessProxy.h -index de24d98d450c11eeb1125b037c11c7487f8b2a64..430d84091fc6edda4b7c83ba97002755cbcd5802 100644 +index df560e2b0fd58e4a58d709d9bbc77b3240c3a10e..8cc41f2747d229fffc6c1d566b0086e294c73735 100644 --- a/Source/WebKit/UIProcess/WebProcessProxy.h +++ b/Source/WebKit/UIProcess/WebProcessProxy.h -@@ -208,6 +208,7 @@ public: +@@ -212,6 +212,8 @@ public: + void addAllowedFirstPartyForCookies(const WebCore::RegistrableDomain&, LoadedWebArchive); + const std::pair>& allowedFirstPartiesForCookiesData() const { return m_allowedFirstPartiesForCookies; } - static void forWebPagesWithOrigin(PAL::SessionID, const WebCore::SecurityOriginData&, NOESCAPE const Function&); - static Vector> allowedFirstPartiesForCookies(); + static Vector> allProcessesForInspector(); - ++ void initializeWebProcess(WebProcessCreationParameters&&); + unsigned suspendedPageCount() const; diff --git a/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.cpp b/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.cpp -index 30393ff0101f487d3e88ad21d83d3a4b54f5af1c..db7fc92889f792a386437837e2a875639d6439a7 100644 +index 4cbf31a5842ab9fdda6c95d9eb929153c6d49dc7..ec8d77c5306a079bd30b51d538f154b25e73e4a2 100644 --- a/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.cpp +++ b/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.cpp -@@ -315,15 +315,10 @@ SOAuthorizationCoordinator& WebsiteDataStore::soAuthorizationCoordinator(const W +@@ -316,15 +316,10 @@ SOAuthorizationCoordinator& WebsiteDataStore::soAuthorizationCoordinator(const W static Ref networkProcessForSession(PAL::SessionID sessionID) { @@ -14741,7 +15129,7 @@ index 30393ff0101f487d3e88ad21d83d3a4b54f5af1c..db7fc92889f792a386437837e2a87563 #else UNUSED_PARAM(sessionID); return NetworkProcessProxy::ensureDefaultNetworkProcess(); -@@ -2105,6 +2100,15 @@ void WebsiteDataStore::setCacheModelSynchronouslyForTesting(CacheModel cacheMode +@@ -2119,6 +2114,15 @@ void WebsiteDataStore::setCacheModelSynchronouslyForTesting(CacheModel cacheMode processPool->setCacheModelSynchronouslyForTesting(cacheModel); } @@ -14757,7 +15145,7 @@ index 30393ff0101f487d3e88ad21d83d3a4b54f5af1c..db7fc92889f792a386437837e2a87563 Vector WebsiteDataStore::parametersFromEachWebsiteDataStore() { return WTF::map(allDataStores(), [](auto& entry) { -@@ -2529,6 +2533,12 @@ void WebsiteDataStore::lastPageLoadNetworkActivityCompletionCodeForTesting(WebCo +@@ -2550,6 +2554,12 @@ void WebsiteDataStore::lastPageLoadNetworkActivityCompletionCodeForTesting(WebCo protect(networkProcess())->lastPageLoadNetworkActivityCompletionCodeForTesting(m_sessionID, pageID, WTF::move(completionHandler)); } @@ -14771,7 +15159,7 @@ index 30393ff0101f487d3e88ad21d83d3a4b54f5af1c..db7fc92889f792a386437837e2a87563 void WebsiteDataStore::hasAppBoundSession(CompletionHandler&& completionHandler) const { diff --git a/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.h b/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.h -index 5638384abdaa629b6b41ecfea4a3314cdce4a28f..cefc3c54d4ff37c0cbe09761ffbbadcbc12b815a 100644 +index 948c89fc30236146c2faea29f2ea8b2842963406..bc667cd080b1ba3f6710a7d40d0bbc497fdd02c4 100644 --- a/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.h +++ b/Source/WebKit/UIProcess/WebsiteData/WebsiteDataStore.h @@ -103,6 +103,7 @@ class DeviceIdHashSaltStorage; @@ -14782,7 +15170,7 @@ index 5638384abdaa629b6b41ecfea4a3314cdce4a28f..cefc3c54d4ff37c0cbe09761ffbbadcb class VirtualAuthenticatorManager; class WebPageProxy; class WebProcessPool; -@@ -118,6 +119,7 @@ enum class UnifiedOriginStorageLevel : uint8_t; +@@ -119,6 +120,7 @@ enum class UnifiedOriginStorageLevel : uint8_t; enum class WebsiteDataFetchOption : uint8_t; enum class WebsiteDataType : uint32_t; @@ -14790,7 +15178,7 @@ index 5638384abdaa629b6b41ecfea4a3314cdce4a28f..cefc3c54d4ff37c0cbe09761ffbbadcb struct ITPThirdPartyData; struct NetworkProcessConnectionInfo; struct WebPushMessage; -@@ -127,6 +129,14 @@ struct WebsiteDataStoreParameters; +@@ -128,6 +130,14 @@ struct WebsiteDataStoreParameters; enum RemoveDataTaskCounterType { }; using RemoveDataTaskCounter = RefCounter; @@ -14805,7 +15193,7 @@ index 5638384abdaa629b6b41ecfea4a3314cdce4a28f..cefc3c54d4ff37c0cbe09761ffbbadcb class WebsiteDataStore : public API::ObjectImpl, public CanMakeWeakPtr { public: static WebsiteDataStore& defaultDataStore(); -@@ -331,8 +341,10 @@ public: +@@ -334,8 +344,10 @@ public: #if USE(SOUP) void setPersistentCredentialStorageEnabled(bool); bool persistentCredentialStorageEnabled() const { return m_persistentCredentialStorageEnabled && isPersistent(); } @@ -14816,7 +15204,7 @@ index 5638384abdaa629b6b41ecfea4a3314cdce4a28f..cefc3c54d4ff37c0cbe09761ffbbadcb void setNetworkProxySettings(WebCore::SoupNetworkProxySettings&&); const WebCore::SoupNetworkProxySettings& networkProxySettings() const LIFETIME_BOUND { return m_networkProxySettings; } void setCookiePersistentStorage(const String&, SoupCookiePersistentStorageType); -@@ -423,6 +435,12 @@ public: +@@ -426,6 +438,12 @@ public: static const String& defaultBaseDataDirectory(); #endif @@ -14829,7 +15217,7 @@ index 5638384abdaa629b6b41ecfea4a3314cdce4a28f..cefc3c54d4ff37c0cbe09761ffbbadcb void resetQuota(CompletionHandler&&); void resetStoragePersistedState(CompletionHandler&&); #if PLATFORM(IOS_FAMILY) -@@ -643,7 +661,9 @@ private: +@@ -644,7 +662,9 @@ private: #if USE(SOUP) bool m_persistentCredentialStorageEnabled { true }; @@ -14840,7 +15228,7 @@ index 5638384abdaa629b6b41ecfea4a3314cdce4a28f..cefc3c54d4ff37c0cbe09761ffbbadcb WebCore::SoupNetworkProxySettings m_networkProxySettings; String m_cookiePersistentStoragePath; SoupCookiePersistentStorageType m_cookiePersistentStorageType { SoupCookiePersistentStorageType::SQLite }; -@@ -670,6 +690,10 @@ private: +@@ -671,6 +691,10 @@ private: const RefPtr m_cookieStore; RefPtr m_networkProcess; @@ -14907,10 +15295,10 @@ index 96bf77411e2e1f4c835f56b409dc179977d197ee..512af5ffce511711b502248e34e49e45 RunLoop::Timer m_destroyLaterTimer; diff --git a/Source/WebKit/UIProcess/glib/BrowserInspectorWebSocketServer.cpp b/Source/WebKit/UIProcess/glib/BrowserInspectorWebSocketServer.cpp new file mode 100644 -index 0000000000000000000000000000000000000000..00483c4a7fdd24c93702c773687643ad045e7127 +index 0000000000000000000000000000000000000000..0a90ebf6104f4f2eb8e8538396fe3fc15c769274 --- /dev/null +++ b/Source/WebKit/UIProcess/glib/BrowserInspectorWebSocketServer.cpp -@@ -0,0 +1,172 @@ +@@ -0,0 +1,171 @@ +/* + * Copyright (C) 2026 Microsoft Corporation. + * @@ -15026,7 +15414,6 @@ index 0000000000000000000000000000000000000000..00483c4a7fdd24c93702c773687643ad + gsize messageSize; + gconstpointer messageData = g_bytes_get_data(message, &messageSize); + String messageString = String::fromUTF8(std::span(static_cast(messageData), messageSize)); -+ fprintf(stderr, "WebSocket message received: %s\n", messageString.utf8().data()); + m_playwrightAgent.dispatchMessageFromFrontend(messageString); + } + @@ -15379,43 +15766,8 @@ index e69e33b4359e24075e9054f014f5e5ab488b78c7..f395398acf790cc4fa80122dd4f59381 #if ENABLE(TOUCH_EVENTS) return AvailableInputDevices::Touchscreen; #else -diff --git a/Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.cpp b/Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.cpp -index 3dd2a77a24c59cb7fb1f01d3a74d75267a0f36bf..a020ccc929af8c9130ecca0125e5229ee8670e54 100644 ---- a/Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.cpp -+++ b/Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.cpp -@@ -864,4 +864,30 @@ RefPtr AcceleratedBackingStore::bufferAsNativeImageForTesting() con - return m_committedBuffer->asNativeImageForTesting(); - } - -+// Playwright begin -+cairo_surface_t* AcceleratedBackingStore::surface() -+{ -+ RefPtr buffer = m_committedBuffer.get(); -+ if (!buffer) -+ return nullptr; -+ -+ RefPtr surface = buffer->surface(); -+ if (!surface) -+ return nullptr; -+ -+ // The original surface is upside down, so we flip it to match orientation in other accelerated backing stores. -+ m_flippedSurface = adoptRef(cairo_image_surface_create(CAIRO_FORMAT_ARGB32, cairo_image_surface_get_width(surface.get()), cairo_image_surface_get_height(surface.get()))); -+ { -+ RefPtr cr = adoptRef(cairo_create(m_flippedSurface.get())); -+ cairo_matrix_t transform; -+ cairo_matrix_init(&transform, 1, 0, 0, -1, 0, cairo_image_surface_get_height(surface.get()) / buffer->deviceScaleFactor()); -+ cairo_transform(cr.get(), &transform); -+ cairo_set_source_surface(cr.get(), surface.get(), 0, 0); -+ cairo_paint(cr.get()); -+ } -+ cairo_surface_flush(m_flippedSurface.get()); -+ return m_flippedSurface.get(); -+} -+// Playwright end -+ - } // namespace WebKit diff --git a/Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.h b/Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.h -index 2a15a005441f41bcc3a44ce666dabeab7ab671f2..cc802e0634696b9455cd650527886c35a14a3ea3 100644 +index be0abb4d9e173bc4a4971537cd6abfc38eaee8bd..c3b720d440395df99d44d31b1e815b50020f8d9a 100644 --- a/Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.h +++ b/Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.h @@ -42,6 +42,7 @@ @@ -15425,26 +15777,7 @@ index 2a15a005441f41bcc3a44ce666dabeab7ab671f2..cc802e0634696b9455cd650527886c35 +typedef struct _cairo_surface cairo_surface_t; #if USE(GBM) - #include -@@ -86,6 +87,7 @@ public: - void unrealize(); - RendererBufferDescription bufferDescription() const; - RefPtr bufferAsNativeImageForTesting() const; -+ cairo_surface_t* surface(); - - private: - explicit AcceleratedBackingStore(WebPageProxy&); -@@ -268,6 +270,10 @@ private: - RefPtr m_committedBuffer; - Rects m_pendingDamageRects; - HashMap> m_buffers; -+// Playwright begin -+ RefPtr m_flippedSurface; -+// Playwright end -+ - }; - - } // namespace WebKit + struct gbm_bo; diff --git a/Source/WebKit/UIProcess/gtk/InspectorTargetProxyGtk.cpp b/Source/WebKit/UIProcess/gtk/InspectorTargetProxyGtk.cpp new file mode 100644 index 0000000000000000000000000000000000000000..8f661f8c62dc091670f9474db037e7eee0ec37ce @@ -15998,7 +16331,7 @@ index 0000000000000000000000000000000000000000..8adbd51bfecad2a273117588bf50f8f7 + +#endif diff --git a/Source/WebKit/UIProcess/mac/PageClientImplMac.h b/Source/WebKit/UIProcess/mac/PageClientImplMac.h -index a621f334b3f1a1d2ea1c4bba63d73fa0757fe2bd..3d6b14a3a2a8a6f42f8da31b535e76715b6fe20a 100644 +index 8470a55dcac2775b9218e118fa11df2bdbfbf682..6a85bdc22713dc724d92f2ae901e04d12f151e46 100644 --- a/Source/WebKit/UIProcess/mac/PageClientImplMac.h +++ b/Source/WebKit/UIProcess/mac/PageClientImplMac.h @@ -31,9 +31,11 @@ @@ -16044,10 +16377,10 @@ index a621f334b3f1a1d2ea1c4bba63d73fa0757fe2bd..3d6b14a3a2a8a6f42f8da31b535e7671 void navigationGestureWillEnd(bool willNavigate, WebBackForwardListItem&) override; void navigationGestureDidEnd(bool willNavigate, WebBackForwardListItem&) override; diff --git a/Source/WebKit/UIProcess/mac/PageClientImplMac.mm b/Source/WebKit/UIProcess/mac/PageClientImplMac.mm -index 23ae9a262af01284fadc90a18436b77af5752c24..730af4dee8db0f9a5dcf34623d499f4619e0d83d 100644 +index 27838ea2c98739c73f4f87057e45d33eee6704fe..95a7b9edbfc95f075600abc32b7ae054b0fd416e 100644 --- a/Source/WebKit/UIProcess/mac/PageClientImplMac.mm +++ b/Source/WebKit/UIProcess/mac/PageClientImplMac.mm -@@ -113,6 +113,13 @@ using namespace WebCore; +@@ -115,6 +115,13 @@ using namespace WebCore; WTF_MAKE_TZONE_ALLOCATED_IMPL(PageClientImpl); @@ -16061,7 +16394,7 @@ index 23ae9a262af01284fadc90a18436b77af5752c24..730af4dee8db0f9a5dcf34623d499f46 PageClientImpl::PageClientImpl(NSView *view, WKWebView *webView) : PageClientImplCocoa(webView) , m_view(view) -@@ -172,6 +179,9 @@ NSWindow *PageClientImpl::activeWindow() const +@@ -174,6 +181,9 @@ NSWindow *PageClientImpl::activeWindow() const bool PageClientImpl::isViewWindowActive() { @@ -16071,7 +16404,7 @@ index 23ae9a262af01284fadc90a18436b77af5752c24..730af4dee8db0f9a5dcf34623d499f46 ASSERT(hasProcessPrivilege(ProcessPrivilege::CanCommunicateWithWindowServer)); RetainPtr activeViewWindow = activeWindow(); return activeViewWindow.get().isKeyWindow || (activeViewWindow && [NSApp keyWindow] == activeViewWindow.get()); -@@ -179,6 +189,9 @@ bool PageClientImpl::isViewWindowActive() +@@ -181,6 +191,9 @@ bool PageClientImpl::isViewWindowActive() bool PageClientImpl::isViewFocused() { @@ -16081,7 +16414,7 @@ index 23ae9a262af01284fadc90a18436b77af5752c24..730af4dee8db0f9a5dcf34623d499f46 // FIXME: This is called from the WebPageProxy constructor before we have a WebViewImpl. // Once WebViewImpl and PageClient merge, this won't be a problem. if (CheckedPtr impl = m_impl.get()) -@@ -202,6 +215,9 @@ void PageClientImpl::makeFirstResponder() +@@ -204,6 +217,9 @@ void PageClientImpl::makeFirstResponder() bool PageClientImpl::isViewVisible(NSView *view, NSWindow *viewWindow) const { @@ -16091,7 +16424,7 @@ index 23ae9a262af01284fadc90a18436b77af5752c24..730af4dee8db0f9a5dcf34623d499f46 auto windowIsOccluded = [&]()->bool { return m_impl && m_impl->windowOcclusionDetectionEnabled() && (viewWindow.occlusionState & NSWindowOcclusionStateVisible) != NSWindowOcclusionStateVisible; }; -@@ -300,7 +316,8 @@ void PageClientImpl::didRelaunchProcess() +@@ -302,7 +318,8 @@ void PageClientImpl::didRelaunchProcess() void PageClientImpl::preferencesDidChange() { @@ -16101,7 +16434,7 @@ index 23ae9a262af01284fadc90a18436b77af5752c24..730af4dee8db0f9a5dcf34623d499f46 } void PageClientImpl::toolTipChanged(const String& oldToolTip, const String& newToolTip) -@@ -533,6 +550,8 @@ IntRect PageClientImpl::rootViewToAccessibilityScreen(const IntRect& rect) +@@ -547,6 +564,8 @@ IntRect PageClientImpl::rootViewToAccessibilityScreen(const IntRect& rect) void PageClientImpl::doneWithKeyEvent(const NativeWebKeyboardEvent& event, bool eventWasHandled) { @@ -16110,7 +16443,7 @@ index 23ae9a262af01284fadc90a18436b77af5752c24..730af4dee8db0f9a5dcf34623d499f46 protect(m_impl)->doneWithKeyEvent(RetainPtr { event.nativeEvent() }.get(), eventWasHandled); } -@@ -552,6 +571,8 @@ void PageClientImpl::computeHasVisualSearchResults(const URL& imageURL, Shareabl +@@ -566,6 +585,8 @@ void PageClientImpl::computeHasVisualSearchResults(const URL& imageURL, Shareabl RefPtr PageClientImpl::createPopupMenuProxy(WebPageProxy& page) { @@ -16119,7 +16452,7 @@ index 23ae9a262af01284fadc90a18436b77af5752c24..730af4dee8db0f9a5dcf34623d499f46 return WebPopupMenuProxyMac::create(m_view.get().get(), protect(page.popupMenuClient())); } -@@ -677,6 +698,12 @@ CALayer *PageClientImpl::footerBannerLayer() const +@@ -691,6 +712,12 @@ CALayer *PageClientImpl::footerBannerLayer() const return m_impl->footerBannerLayer(); } @@ -16132,7 +16465,7 @@ index 23ae9a262af01284fadc90a18436b77af5752c24..730af4dee8db0f9a5dcf34623d499f46 RefPtr PageClientImpl::takeViewSnapshot(std::optional&&) { return protect(m_impl)->takeViewSnapshot(); -@@ -898,6 +925,13 @@ void PageClientImpl::beganExitFullScreen(const IntRect& initialFrame, const IntR +@@ -912,6 +939,13 @@ void PageClientImpl::beganExitFullScreen(const IntRect& initialFrame, const IntR #endif // ENABLE(FULLSCREEN_API) @@ -16146,7 +16479,7 @@ index 23ae9a262af01284fadc90a18436b77af5752c24..730af4dee8db0f9a5dcf34623d499f46 void PageClientImpl::navigationGestureDidBegin() { protect(m_impl)->dismissContentRelativeChildWindowsWithAnimation(true); -@@ -1078,6 +1112,9 @@ void PageClientImpl::requestScrollToRect(const WebCore::FloatRect& targetRect, c +@@ -1099,6 +1133,9 @@ void PageClientImpl::requestScrollToRect(const WebCore::FloatRect& targetRect, c bool PageClientImpl::windowIsFrontWindowUnderMouse(const NativeWebMouseEvent& event) { @@ -16252,10 +16585,10 @@ index d063c571127353548110ca306fabf076cae2c966..015f0932a0b9da6888365eb072f7c6d5 bool showAfterPostProcessingContextData(); diff --git a/Source/WebKit/UIProcess/mac/WebContextMenuProxyMac.mm b/Source/WebKit/UIProcess/mac/WebContextMenuProxyMac.mm -index 960f2529fe031dd6799c5d0d16ebf899089620e6..0a9e4684a6a149cffc4addc64b3b79bffc07211b 100644 +index b80415eadff1728127f4d2a97efb45b1f845649d..0ad28207442c018b740f0ded3f72c8620b33e2db 100644 --- a/Source/WebKit/UIProcess/mac/WebContextMenuProxyMac.mm +++ b/Source/WebKit/UIProcess/mac/WebContextMenuProxyMac.mm -@@ -543,6 +543,12 @@ RetainPtr WebContextMenuProxyMac::createShareMenuItem(ShareMenuItemT +@@ -544,6 +544,12 @@ RetainPtr WebContextMenuProxyMac::createShareMenuItem(ShareMenuItemT } #endif @@ -16320,10 +16653,10 @@ index 0000000000000000000000000000000000000000..6113f4cd60a5d72b8ead61176cb43200 +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/mac/WebPageInspectorInputAgentMac.mm b/Source/WebKit/UIProcess/mac/WebPageInspectorInputAgentMac.mm new file mode 100644 -index 0000000000000000000000000000000000000000..e6d3a8764ef084790a4ca4e9891b09ddcbb70781 +index 0000000000000000000000000000000000000000..c0dbaec1d6a595896498239c0cdd7dcb4e99492f --- /dev/null +++ b/Source/WebKit/UIProcess/mac/WebPageInspectorInputAgentMac.mm -@@ -0,0 +1,141 @@ +@@ -0,0 +1,144 @@ +/* + * Copyright (C) 2019 Microsoft Corporation. + * @@ -16365,7 +16698,7 @@ index 0000000000000000000000000000000000000000..e6d3a8764ef084790a4ca4e9891b09dd + +using namespace WebCore; + -+void WebPageInspectorInputAgent::platformDispatchMouseEvent(const String& type, int x, int y, std::optional&& optionalModifiers, const String& button, std::optional&& optionalClickCount, unsigned short buttons) { ++void WebPageInspectorInputAgent::platformDispatchMouseEvent(const String& type, int x, int y, std::optional&& optionalModifiers, const String& button, std::optional&& optionalClickCount, unsigned short buttons, MonotonicTime monotonicTimestamp) { + IntPoint locationInWindow(x, y); + + NSEventModifierFlags modifiers = 0; @@ -16382,7 +16715,10 @@ index 0000000000000000000000000000000000000000..e6d3a8764ef084790a4ca4e9891b09dd + } + int clickCount = optionalClickCount ? *optionalClickCount : 0; + -+ NSTimeInterval timestamp = [NSDate timeIntervalSinceReferenceDate]; ++ // NSEvent.timestamp must be raw seconds matching MonotonicTime so that ++ // WebEventFactory::createWebMouseEvent's MonotonicTime::fromRawSeconds round-trips ++ // and DOM event.timeStamp comes out as a sensible DOMHighResTimeStamp. ++ NSTimeInterval timestamp = monotonicTimestamp.secondsSinceEpoch().value(); + NSWindow *window = m_page.platformWindow(); + NSInteger windowNumber = window.windowNumber; + @@ -16466,10 +16802,10 @@ index 0000000000000000000000000000000000000000..e6d3a8764ef084790a4ca4e9891b09dd + +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/mac/WebViewImpl.h b/Source/WebKit/UIProcess/mac/WebViewImpl.h -index e16a2dc21d968512a9753336bbc26912225e7551..f33dce2233b9c9fae2e350101f7c598ba409c8cc 100644 +index 745b88f688d8901a7a140d8414469e12df69e041..b28ceb988248a4528213c972ded84567480e1e16 100644 --- a/Source/WebKit/UIProcess/mac/WebViewImpl.h +++ b/Source/WebKit/UIProcess/mac/WebViewImpl.h -@@ -39,6 +39,7 @@ +@@ -40,6 +40,7 @@ #include "WKLayoutMode.h" #include "WebMouseEvent.h" #include @@ -16477,7 +16813,7 @@ index e16a2dc21d968512a9753336bbc26912225e7551..f33dce2233b9c9fae2e350101f7c598b #include #include #include -@@ -615,6 +616,9 @@ public: +@@ -617,6 +618,9 @@ public: void provideDataForPasteboard(NSPasteboard *, NSString *type); NSArray *namesOfPromisedFilesDroppedAtDestination(NSURL *dropDestination); @@ -16488,10 +16824,10 @@ index e16a2dc21d968512a9753336bbc26912225e7551..f33dce2233b9c9fae2e350101f7c598b RefPtr takeViewSnapshot(ForceSoftwareCapturingViewportSnapshot); void saveBackForwardSnapshotForCurrentItem(); diff --git a/Source/WebKit/UIProcess/mac/WebViewImpl.mm b/Source/WebKit/UIProcess/mac/WebViewImpl.mm -index c9c3ac7055aa857b5f0a6c9baeabbc7194b479d2..f3f09912076b3c9d74d848098273b4c7662d94b0 100644 +index 0d8681f20caeee8027af28e5f69ed361601f26a4..a2e410e256779120ba09438dac9065e26c85f765 100644 --- a/Source/WebKit/UIProcess/mac/WebViewImpl.mm +++ b/Source/WebKit/UIProcess/mac/WebViewImpl.mm -@@ -2608,6 +2608,11 @@ WebCore::DestinationColorSpace WebViewImpl::colorSpace() +@@ -2649,6 +2649,11 @@ WebCore::DestinationColorSpace WebViewImpl::colorSpace() if (!m_colorSpace) m_colorSpace = [NSColorSpace sRGBColorSpace]; } @@ -16503,7 +16839,7 @@ index c9c3ac7055aa857b5f0a6c9baeabbc7194b479d2..f3f09912076b3c9d74d848098273b4c7 ASSERT(m_colorSpace); return WebCore::DestinationColorSpace { [m_colorSpace CGColorSpace] }; -@@ -5092,6 +5097,17 @@ static RetainPtr takeWindowSnapshot(CGSWindowID windowID, bool captu +@@ -5158,6 +5163,17 @@ static RetainPtr takeWindowSnapshot(CGSWindowID windowID, bool captu return WebCore::cgWindowListCreateImage(CGRectNull, kCGWindowListOptionIncludingWindow, windowID, imageOptions); } @@ -16965,7 +17301,7 @@ index 0000000000000000000000000000000000000000..36fc7af8eb98b3f70d255ee858dccf58 + +} // namespace WebKit diff --git a/Source/WebKit/UIProcess/win/WebView.cpp b/Source/WebKit/UIProcess/win/WebView.cpp -index 30fcc5c80d118dc6389c12a43f03aede984856ab..af8b868d88c998d9105cc8a5c0f3db5158ba2a47 100644 +index f3beb2f8390b050913bcb8111c50ced57194c98f..7011eae4d384e0d459bef82bf92099bcca20f2c9 100644 --- a/Source/WebKit/UIProcess/win/WebView.cpp +++ b/Source/WebKit/UIProcess/win/WebView.cpp @@ -571,7 +571,7 @@ LRESULT WebView::onSizeEvent(HWND hwnd, UINT, WPARAM, LPARAM lParam, bool& handl @@ -16978,7 +17314,7 @@ index 30fcc5c80d118dc6389c12a43f03aede984856ab..af8b868d88c998d9105cc8a5c0f3db51 if (m_page && m_page->drawingArea()) { // FIXME specify correctly layerPosition. diff --git a/Source/WebKit/UIProcess/wpe/AcceleratedBackingStore.cpp b/Source/WebKit/UIProcess/wpe/AcceleratedBackingStore.cpp -index 0705d7a9536a44a09c410d9e0e14b99c7ce5623e..7c9d8a156147707740cd984c170271593dcdacbd 100644 +index 9c642983d668120d8ac35ff9961848c05b6c7d04..0963431a137e83040b42b7aa1cf7d677614bfd1b 100644 --- a/Source/WebKit/UIProcess/wpe/AcceleratedBackingStore.cpp +++ b/Source/WebKit/UIProcess/wpe/AcceleratedBackingStore.cpp @@ -231,7 +231,7 @@ static Expected getImageInfoFromBuffer(const GRefPtr m_frame; std::unique_ptr m_channel; diff --git a/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp b/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp -index ef050f1bf94dc201a89055164a5c0c9f33b9bbad..2e228513e686dd67e3e8129670b0ad374a5f54d2 100644 +index f9e0b62dc43d4de454a3eaa517a18577883aeb42..4d1c184367fa8e97e1a67b8afe4470bbc466d4c0 100644 --- a/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp +++ b/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp @@ -273,6 +273,11 @@ void WebLoaderStrategy::scheduleLoad(ResourceLoader& resourceLoader, CachedResou @@ -17933,7 +18270,7 @@ index ef050f1bf94dc201a89055164a5c0c9f33b9bbad..2e228513e686dd67e3e8129670b0ad37 } WEBLOADERSTRATEGY_RELEASE_LOG_FORWARDABLE(WebLoaderStrategyScheduleLoad); -@@ -431,7 +440,7 @@ static void addParametersShared(const LocalFrame* frame, NetworkResourceLoadPara +@@ -439,7 +448,7 @@ static void addParametersShared(const LocalFrame* frame, NetworkResourceLoadPara parameters.linkPreconnectEarlyHintsEnabled = mainFrame->settings().linkPreconnectEarlyHintsEnabled(); } @@ -17942,7 +18279,7 @@ index ef050f1bf94dc201a89055164a5c0c9f33b9bbad..2e228513e686dd67e3e8129670b0ad37 { auto identifier = *resourceLoader.identifier(); -@@ -443,10 +452,10 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL +@@ -451,10 +460,10 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL && resourceLoader.frameLoader()->notifier().isInitialRequestIdentifier(identifier) ? MainFrameMainResource::Yes : MainFrameMainResource::No; if (!page->allowsLoadFromURL(request.url(), mainFrameMainResource)) { @@ -17955,7 +18292,7 @@ index ef050f1bf94dc201a89055164a5c0c9f33b9bbad..2e228513e686dd67e3e8129670b0ad37 } } -@@ -456,19 +465,6 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL +@@ -464,19 +473,6 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL LOG(NetworkScheduling, "(WebProcess) WebLoaderStrategy::scheduleLoad, url '%s' will be scheduled with the NetworkProcess with priority %d, storedCredentialsPolicy %i", resourceLoader.url().string().latin1().data(), static_cast(resourceLoader.request().priority()), (int)storedCredentialsPolicy); @@ -17975,7 +18312,7 @@ index ef050f1bf94dc201a89055164a5c0c9f33b9bbad..2e228513e686dd67e3e8129670b0ad37 loadParameters.identifier = identifier; loadParameters.parentPID = legacyPresentingApplicationPID(); loadParameters.contentSniffingPolicy = contentSniffingPolicy; -@@ -563,14 +559,11 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL +@@ -571,14 +567,11 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL if (loadParameters.options.mode != FetchOptions::Mode::Navigate) { ASSERT(loadParameters.sourceOrigin); @@ -17993,7 +18330,7 @@ index ef050f1bf94dc201a89055164a5c0c9f33b9bbad..2e228513e686dd67e3e8129670b0ad37 loadParameters.isMainFrameNavigation = isMainFrameNavigation; if (loadParameters.isMainFrameNavigation && document) { -@@ -635,6 +628,30 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL +@@ -651,6 +644,30 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL if (RefPtr handle = resourceLoader.cachedResource()) loadParameters.isInitiatorPrefetch = handle->type() == CachedResource::Type::LinkPrefetch; @@ -18024,7 +18361,7 @@ index ef050f1bf94dc201a89055164a5c0c9f33b9bbad..2e228513e686dd67e3e8129670b0ad37 std::optional existingNetworkResourceLoadIdentifierToResume; if (loadParameters.isMainFrameNavigation) existingNetworkResourceLoadIdentifierToResume = std::exchange(m_existingNetworkResourceLoadIdentifierToResume, std::nullopt); -@@ -650,7 +667,7 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL +@@ -666,7 +683,7 @@ void WebLoaderStrategy::scheduleLoadFromNetworkProcess(ResourceLoader& resourceL } auto loader = WebResourceLoader::create(resourceLoader, trackingParameters); @@ -18033,7 +18370,7 @@ index ef050f1bf94dc201a89055164a5c0c9f33b9bbad..2e228513e686dd67e3e8129670b0ad37 } void WebLoaderStrategy::scheduleInternallyFailedLoad(WebCore::ResourceLoader& resourceLoader) -@@ -1070,7 +1087,7 @@ void WebLoaderStrategy::didFinishPreconnection(WebCore::ResourceLoaderIdentifier +@@ -1086,7 +1103,7 @@ void WebLoaderStrategy::didFinishPreconnection(WebCore::ResourceLoaderIdentifier bool WebLoaderStrategy::isOnLine() const { @@ -18042,7 +18379,7 @@ index ef050f1bf94dc201a89055164a5c0c9f33b9bbad..2e228513e686dd67e3e8129670b0ad37 } void WebLoaderStrategy::addOnlineStateChangeListener(Function&& listener) -@@ -1096,6 +1113,11 @@ void WebLoaderStrategy::isResourceLoadFinished(CachedResource& resource, Complet +@@ -1112,6 +1129,11 @@ void WebLoaderStrategy::isResourceLoadFinished(CachedResource& resource, Complet void WebLoaderStrategy::setOnLineState(bool isOnLine) { @@ -18054,7 +18391,7 @@ index ef050f1bf94dc201a89055164a5c0c9f33b9bbad..2e228513e686dd67e3e8129670b0ad37 if (m_isOnLine == isOnLine) return; -@@ -1104,6 +1126,12 @@ void WebLoaderStrategy::setOnLineState(bool isOnLine) +@@ -1120,6 +1142,12 @@ void WebLoaderStrategy::setOnLineState(bool isOnLine) listener(isOnLine); } @@ -18098,10 +18435,10 @@ index ab7f322fc4e1b340bb0acdc84d4f7ba39a6e0df0..d79aea95dc4e2ead71a161b24c0e1f08 } // namespace WebKit diff --git a/Source/WebKit/WebProcess/Network/WebResourceLoader.cpp b/Source/WebKit/WebProcess/Network/WebResourceLoader.cpp -index 24c2649b31ce74a91911c8d8ff9be630dc29d576..11e1b09f7fe0573374f887bcbe57e25e4a8f6b87 100644 +index 92c407cd8349da4fa94d46a31c39b3935240af61..86e7de852319d854cb3f63b26e90fbcbc984ee0f 100644 --- a/Source/WebKit/WebProcess/Network/WebResourceLoader.cpp +++ b/Source/WebKit/WebProcess/Network/WebResourceLoader.cpp -@@ -236,9 +236,6 @@ void WebResourceLoader::didReceiveResponse(ResourceResponse&& response, PrivateR +@@ -237,9 +237,6 @@ void WebResourceLoader::didReceiveResponse(ResourceResponse&& response, PrivateR coreLoader->didReceiveResponse(ResourceResponse { inspectorResponse }, [this, protectedThis = Ref { *this }, interceptedRequestIdentifier, policyDecisionCompletionHandler = WTF::move(policyDecisionCompletionHandler), overrideData = WTF::move(overrideData)]() mutable { RefPtr coreLoader = m_coreLoader; @@ -18111,7 +18448,7 @@ index 24c2649b31ce74a91911c8d8ff9be630dc29d576..11e1b09f7fe0573374f887bcbe57e25e if (!m_coreLoader || !coreLoader->identifier()) { m_interceptController.continueResponse(interceptedRequestIdentifier); return; -@@ -255,6 +252,8 @@ void WebResourceLoader::didReceiveResponse(ResourceResponse&& response, PrivateR +@@ -256,6 +253,8 @@ void WebResourceLoader::didReceiveResponse(ResourceResponse&& response, PrivateR } }); }); @@ -18121,10 +18458,10 @@ index 24c2649b31ce74a91911c8d8ff9be630dc29d576..11e1b09f7fe0573374f887bcbe57e25e } diff --git a/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp b/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp -index 83519d3b8b16ad84a162f99ec6e3c9f9a0525e73..5cb5b17e983ef538c12bcaa81fe9e425cd622f06 100644 +index 2007a79dda001fd64c35715e4f1e7bf125d68679..3ca9f5a101b13fd85ce4a77bde92caf4ef0c1bf8 100644 --- a/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp +++ b/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp -@@ -503,6 +503,9 @@ void WebChromeClient::addMessageToConsole(MessageSource source, MessageLevel lev +@@ -504,6 +504,9 @@ void WebChromeClient::addMessageToConsole(MessageSource source, MessageLevel lev if (!page) return; @@ -18250,7 +18587,7 @@ index 5ac78eb5880335f49a9a6f2690626e46fbcac7f4..aa12d9c96279054e01cc9e82e78547a7 #include "WebPage.h" #include "WebPageCreationParameters.h" diff --git a/Source/WebKit/WebProcess/WebPage/WebCookieJar.cpp b/Source/WebKit/WebProcess/WebPage/WebCookieJar.cpp -index 42644873c3a43e84452298c131d1d356eed6b36c..9718fb79e8918809099bc3eedd1cdfc7d751a575 100644 +index d2a69a979ac3f6f3507ef71b9f23c105d110ec52..7702064e4428a32ab5c613b8bc6aff2d064fc69b 100644 --- a/Source/WebKit/WebProcess/WebPage/WebCookieJar.cpp +++ b/Source/WebKit/WebProcess/WebPage/WebCookieJar.cpp @@ -43,6 +43,7 @@ @@ -18288,12 +18625,12 @@ index a7ad18fc1201e5de2cc1528539a765599ecfc41e..ebea233bc54ab6e56fb7093ff63e2368 WebCookieJar(); diff --git a/Source/WebKit/WebProcess/WebPage/WebFrame.cpp b/Source/WebKit/WebProcess/WebPage/WebFrame.cpp -index 2a3aa30f08da8e00e790dbdbac574c986a3a4f0a..9b70be2682defadef10da8b69af50ca115878cf6 100644 +index c0fec67356b0c5c39b6c3c81a693029b0ef75723..220a2eaa4a8574a0dbb3d7166bd3cd318b0f50f6 100644 --- a/Source/WebKit/WebProcess/WebPage/WebFrame.cpp +++ b/Source/WebKit/WebProcess/WebPage/WebFrame.cpp -@@ -181,6 +181,9 @@ Ref WebFrame::createSubframe(WebPage& page, WebFrame& parent, const At - ASSERT(ownerElement.document().frame()); - coreFrame->init(); +@@ -184,6 +184,9 @@ Ref WebFrame::createSubframe(WebPage& page, WebFrame& parent, const At + if (RefPtr backend = Ref { page }->inspector(WebPage::LazyCreationPolicy::UseExistingOnly)) + backend->ensureInstrumentationForFrame(coreFrame.get()); + if (parent.m_inspectorTarget) + parent.m_inspectorTarget->didCreateSubframe(frame); @@ -18302,10 +18639,10 @@ index 2a3aa30f08da8e00e790dbdbac574c986a3a4f0a..9b70be2682defadef10da8b69af50ca1 } diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.cpp b/Source/WebKit/WebProcess/WebPage/WebPage.cpp -index 423d720a28e28a0fa83d0abf226f1bca2a0a27ee..0c46b23f4aec72f73f9bf39119b3ce1ac3ff72ba 100644 +index 5a600c0a20459f0b90bc5235d81958927ab635f2..5f4da25bd0cd308c0952ff19b55ae0ca1d94ad82 100644 --- a/Source/WebKit/WebProcess/WebPage/WebPage.cpp +++ b/Source/WebKit/WebProcess/WebPage/WebPage.cpp -@@ -264,6 +264,7 @@ +@@ -267,6 +267,7 @@ #include #include #include @@ -18313,7 +18650,7 @@ index 423d720a28e28a0fa83d0abf226f1bca2a0a27ee..0c46b23f4aec72f73f9bf39119b3ce1a #include #include #include -@@ -1227,6 +1228,14 @@ WebPage::WebPage(PageIdentifier pageID, WebPageCreationParameters&& parameters) +@@ -1261,6 +1262,14 @@ WebPage::WebPage(PageIdentifier pageID, WebPageCreationParameters&& parameters) setLinkDecorationFilteringData(WTF::move(parameters.linkDecorationFilteringData)); setAllowedQueryParametersForAdvancedPrivacyProtections(WTF::move(parameters.allowedQueryParametersForAdvancedPrivacyProtections)); #endif @@ -18328,7 +18665,7 @@ index 423d720a28e28a0fa83d0abf226f1bca2a0a27ee..0c46b23f4aec72f73f9bf39119b3ce1a if (parameters.windowFeatures) { page->applyWindowFeatures(*parameters.windowFeatures); page->chrome().show(); -@@ -2222,6 +2231,22 @@ void WebPage::loadDidCommitInAnotherProcess(WebCore::FrameIdentifier frameID, st +@@ -2384,6 +2393,22 @@ void WebPage::loadDidCommitInAnotherProcess(WebCore::FrameIdentifier frameID, st } } @@ -18351,7 +18688,7 @@ index 423d720a28e28a0fa83d0abf226f1bca2a0a27ee..0c46b23f4aec72f73f9bf39119b3ce1a void WebPage::loadRequest(LoadParameters&& loadParameters) { WEBPAGE_RELEASE_LOG_FORWARDABLE(Loading, WebPageLoadRequest, loadParameters.navigationID ? loadParameters.navigationID->toUInt64() : 0, static_cast(loadParameters.shouldTreatAsContinuingLoad), loadParameters.request.isAppInitiated(), loadParameters.existingNetworkResourceLoadIdentifierToResume ? loadParameters.existingNetworkResourceLoadIdentifierToResume->toUInt64() : 0); -@@ -2413,7 +2438,9 @@ void WebPage::stopLoading() +@@ -2578,7 +2603,9 @@ void WebPage::stopLoading() void WebPage::stopLoadingDueToProcessSwap() { SetForScope isStoppingLoadingDueToProcessSwap(m_isStoppingLoadingDueToProcessSwap, true); @@ -18360,8 +18697,8 @@ index 423d720a28e28a0fa83d0abf226f1bca2a0a27ee..0c46b23f4aec72f73f9bf39119b3ce1a + InspectorInstrumentationWebKit::setStoppingLoadingDueToProcessSwap(m_page.get(), false); } - bool WebPage::defersLoading() const -@@ -3002,7 +3029,7 @@ void WebPage::viewportPropertiesDidChange(const ViewportArguments& viewportArgum + void WebPage::keepBlobURLAliveForNewWindowNavigation(URL&& blobURL, std::optional&& topOrigin) +@@ -3180,7 +3207,7 @@ void WebPage::viewportPropertiesDidChange(const ViewportArguments& viewportArgum #if PLATFORM(IOS_FAMILY) if (m_viewportConfiguration.setViewportArguments(viewportArguments)) viewportConfigurationChanged(); @@ -18370,7 +18707,7 @@ index 423d720a28e28a0fa83d0abf226f1bca2a0a27ee..0c46b23f4aec72f73f9bf39119b3ce1a // Adjust view dimensions when using fixed layout. RefPtr localMainFrame = this->localMainFrame(); RefPtr view = localMainFrame ? localMainFrame->view() : nullptr; -@@ -3863,6 +3890,13 @@ void WebPage::flushDeferredIntersectionObservations() +@@ -4041,6 +4068,13 @@ void WebPage::flushDeferredIntersectionObservations() protect(corePage())->flushDeferredIntersectionObservations(); } @@ -18384,7 +18721,7 @@ index 423d720a28e28a0fa83d0abf226f1bca2a0a27ee..0c46b23f4aec72f73f9bf39119b3ce1a void WebPage::flushDeferredDidReceiveMouseEvent() { if (auto info = std::exchange(m_deferredDidReceiveMouseEvent, std::nullopt)) -@@ -4152,6 +4186,100 @@ void WebPage::touchEvent(const WebTouchEvent& touchEvent, CompletionHandlersendMessageToTargetBackend(message); } @@ -18502,7 +18839,7 @@ index 423d720a28e28a0fa83d0abf226f1bca2a0a27ee..0c46b23f4aec72f73f9bf39119b3ce1a void WebPage::insertNewlineInQuotedContent() { RefPtr frame = corePage()->focusController().focusedOrMainFrame(); -@@ -4494,6 +4632,7 @@ void WebPage::setMainFrameDocumentVisualUpdatesAllowed(bool allowed) +@@ -4654,6 +4792,7 @@ void WebPage::setMainFrameDocumentVisualUpdatesAllowed(bool allowed) void WebPage::show() { send(Messages::WebPageProxy::ShowPage()); @@ -18510,7 +18847,7 @@ index 423d720a28e28a0fa83d0abf226f1bca2a0a27ee..0c46b23f4aec72f73f9bf39119b3ce1a } void WebPage::setIsTakingSnapshotsForApplicationSuspension(bool isTakingSnapshotsForApplicationSuspension) -@@ -5583,7 +5722,7 @@ NotificationPermissionRequestManager* WebPage::notificationPermissionRequestMana +@@ -5761,7 +5900,7 @@ NotificationPermissionRequestManager* WebPage::notificationPermissionRequestMana #if ENABLE(DRAG_SUPPORT) @@ -18519,7 +18856,7 @@ index 423d720a28e28a0fa83d0abf226f1bca2a0a27ee..0c46b23f4aec72f73f9bf39119b3ce1a void WebPage::performDragControllerAction(DragControllerAction action, const IntPoint& clientPosition, const IntPoint& globalPosition, OptionSet draggingSourceOperationMask, SelectionData&& selectionData, OptionSet flags, CompletionHandler, DragHandlingMethod, bool, unsigned, IntRect, IntRect, std::optional)>&& completionHandler) { if (!m_page) -@@ -7958,6 +8097,10 @@ void WebPage::didCommitLoad(WebFrame* frame) +@@ -8150,6 +8289,10 @@ void WebPage::didCommitLoad(WebFrame* frame) if (frame && frame->isMainFrame()) m_networkResourceRequestIdentifiersForPageLoadTiming.clear(); @@ -18530,7 +18867,7 @@ index 423d720a28e28a0fa83d0abf226f1bca2a0a27ee..0c46b23f4aec72f73f9bf39119b3ce1a } void WebPage::didFinishDocumentLoad(WebFrame& frame) -@@ -8260,6 +8403,9 @@ Ref WebPage::createDocumentLoader(LocalFrame& frame, ResourceReq +@@ -8453,6 +8596,9 @@ Ref WebPage::createDocumentLoader(LocalFrame& frame, ResourceReq m_allowsContentJavaScriptFromMostRecentNavigation = m_internals->pendingWebsitePolicies->allowsContentJavaScript; WebsitePoliciesData::applyToDocumentLoader(*std::exchange(m_internals->pendingWebsitePolicies, std::nullopt), documentLoader); } @@ -18541,7 +18878,7 @@ index 423d720a28e28a0fa83d0abf226f1bca2a0a27ee..0c46b23f4aec72f73f9bf39119b3ce1a return documentLoader; diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.h b/Source/WebKit/WebProcess/WebPage/WebPage.h -index 811658f19c79994e1b624ee5c0fe285ee563e7a7..87a666dc8953b5ca530f87cfb86f055949100612 100644 +index 28cfe18e24fd74cf652b7fcc417d46ec13017c49..abdb3f70b0cde7217a398b58eb29494571290ff2 100644 --- a/Source/WebKit/WebProcess/WebPage/WebPage.h +++ b/Source/WebKit/WebProcess/WebPage/WebPage.h @@ -49,6 +49,7 @@ @@ -18552,7 +18889,7 @@ index 811658f19c79994e1b624ee5c0fe285ee563e7a7..87a666dc8953b5ca530f87cfb86f0559 #include #include #include -@@ -1403,11 +1404,11 @@ public: +@@ -1414,11 +1415,11 @@ public: void clearSelection(); void restoreSelectionInFocusedEditableElement(); @@ -18566,7 +18903,7 @@ index 811658f19c79994e1b624ee5c0fe285ee563e7a7..87a666dc8953b5ca530f87cfb86f0559 void performDragControllerAction(std::optional, DragControllerAction, WebCore::DragData&&, CompletionHandler, WebCore::DragHandlingMethod, bool, unsigned, WebCore::IntRect, WebCore::IntRect, std::optional)>&&); void performDragOperation(std::optional, WebCore::DragData&&, SandboxExtension::Handle&&, Vector&&, CompletionHandler&&); #endif -@@ -1425,6 +1426,9 @@ public: +@@ -1436,6 +1437,9 @@ public: #if ENABLE(MODEL_PROCESS) void modelDragEnded(WebCore::NodeIdentifier); #endif @@ -18576,7 +18913,7 @@ index 811658f19c79994e1b624ee5c0fe285ee563e7a7..87a666dc8953b5ca530f87cfb86f0559 #endif #if ENABLE(MODEL_PROCESS) -@@ -1527,8 +1531,11 @@ public: +@@ -1538,8 +1542,11 @@ public: void gestureEvent(WebCore::FrameIdentifier, const WebGestureEvent&, CompletionHandler, bool, std::optional)>&&); #endif @@ -18589,7 +18926,7 @@ index 811658f19c79994e1b624ee5c0fe285ee563e7a7..87a666dc8953b5ca530f87cfb86f0559 void dynamicViewportSizeUpdate(const DynamicViewportSizeUpdate&); bool scaleWasSetByUIProcess() const { return m_scaleWasSetByUIProcess; } void willStartUserTriggeredZooming(); -@@ -1689,6 +1696,8 @@ public: +@@ -1700,6 +1707,8 @@ public: void connectInspector(Inspector::FrontendChannel::ConnectionType); void disconnectInspector(); void sendMessageToTargetBackend(const String& message); @@ -18598,7 +18935,7 @@ index 811658f19c79994e1b624ee5c0fe285ee563e7a7..87a666dc8953b5ca530f87cfb86f0559 void insertNewlineInQuotedContent(); -@@ -2127,6 +2136,7 @@ public: +@@ -2141,6 +2150,7 @@ public: void showContextMenuFromFrame(const FrameInfoData&, const ContextMenuContextData&, const UserData&); #endif void loadRequest(LoadParameters&&); @@ -18606,7 +18943,7 @@ index 811658f19c79994e1b624ee5c0fe285ee563e7a7..87a666dc8953b5ca530f87cfb86f0559 void setObscuredContentInsets(const WebCore::FloatBoxExtent&); -@@ -2349,6 +2359,7 @@ private: +@@ -2365,6 +2375,7 @@ private: void updatePotentialTapSecurityOrigin(const WebTouchEvent&, bool wasHandled); #elif ENABLE(TOUCH_EVENTS) void touchEvent(const WebTouchEvent&, CompletionHandler, bool)>&&); @@ -18614,7 +18951,7 @@ index 811658f19c79994e1b624ee5c0fe285ee563e7a7..87a666dc8953b5ca530f87cfb86f0559 #endif void cancelPointer(WebCore::PointerID, const WebCore::IntPoint&); -@@ -3176,6 +3187,7 @@ private: +@@ -3201,6 +3212,7 @@ private: bool m_isAppNapEnabled { true }; Markable m_pendingNavigationID; @@ -18623,7 +18960,7 @@ index 811658f19c79994e1b624ee5c0fe285ee563e7a7..87a666dc8953b5ca530f87cfb86f0559 bool m_mainFrameProgressCompleted { false }; bool m_shouldDispatchFakeMouseMoveEvents { true }; diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.messages.in b/Source/WebKit/WebProcess/WebPage/WebPage.messages.in -index 5ab12f0d119cd46d4b327b1332add5b4a7e27ec0..291e354bcfcf428ef4410ec757d4d8dc8745dce3 100644 +index 0803df95c4b72349235f0f1acdba5f2eda6fa164..20a6e43ecdfa1246f47edc1edd2cf1ef07265d25 100644 --- a/Source/WebKit/WebProcess/WebPage/WebPage.messages.in +++ b/Source/WebKit/WebProcess/WebPage/WebPage.messages.in @@ -84,10 +84,13 @@ messages -> WebPage WantsAsyncDispatchMessage { @@ -18657,15 +18994,15 @@ index 5ab12f0d119cd46d4b327b1332add5b4a7e27ec0..291e354bcfcf428ef4410ec757d4d8dc #endif CancelPointer(WebCore::PointerID pointerId, WebCore::IntPoint documentPoint) -@@ -210,6 +215,7 @@ messages -> WebPage WantsAsyncDispatchMessage { - LoadDataInFrame(std::span data, String MIMEType, String encodingName, URL baseURL, WebCore::FrameIdentifier frameID) +@@ -213,6 +218,7 @@ messages -> WebPage WantsAsyncDispatchMessage { + LoadRequest(struct WebKit::LoadParameters loadParameters) - LoadDidCommitInAnotherProcess(WebCore::FrameIdentifier frameID, std::optional layerHostingContextIdentifier, std::optional mainFrameDocumentURL) + LoadDidCommitInAnotherProcess(WebCore::FrameIdentifier frameID, std::optional layerHostingContextIdentifier, RefPtr topDocumentSyncData) + LoadRequestInFrameForInspector(struct WebKit::LoadParameters loadParameters, WebCore::FrameIdentifier frameID) LoadRequestWaitingForProcessLaunch(struct WebKit::LoadParameters loadParameters, URL resourceDirectoryURL, WebKit::WebPageProxyIdentifier pageID, bool checkAssumedReadAccessToResourceURL) LoadData(struct WebKit::LoadParameters loadParameters) LoadSimulatedRequestAndResponse(struct WebKit::LoadParameters loadParameters, WebCore::ResourceResponse simulatedResponse) -@@ -376,10 +382,10 @@ messages -> WebPage WantsAsyncDispatchMessage { +@@ -391,10 +397,10 @@ messages -> WebPage WantsAsyncDispatchMessage { RemoveLayerForFindOverlay() -> () # Drag and drop. @@ -18678,7 +19015,7 @@ index 5ab12f0d119cd46d4b327b1332add5b4a7e27ec0..291e354bcfcf428ef4410ec757d4d8dc PerformDragControllerAction(std::optional frameID, enum:uint8_t WebKit::DragControllerAction action, WebCore::DragData dragData) -> (enum:uint8_t std::optional dragOperation, enum:uint8_t WebCore::DragHandlingMethod dragHandlingMethod, bool mouseIsOverFileInput, unsigned numberOfItemsToBeAccepted, WebCore::IntRect insertionRect, WebCore::IntRect editableElementRect, struct std::optional remoteUserInputEventData) PerformDragOperation(std::optional frameID, WebCore::DragData dragData, WebKit::SandboxExtensionHandle sandboxExtensionHandle, Vector sandboxExtensionsForUpload) -> (WebKit::DragOperationResult dragOperationResult) #endif -@@ -399,6 +405,10 @@ messages -> WebPage WantsAsyncDispatchMessage { +@@ -414,6 +420,10 @@ messages -> WebPage WantsAsyncDispatchMessage { ModelDragEnded(WebCore::NodeIdentifier nodeID) #endif @@ -18728,10 +19065,10 @@ index 081c8dd9bc9402d909fba5359f90c686deb9987a..b9e548e8886861fa8013f5d8af1c893f const auto& availableInputs = WebProcess::singleton().availableInputDevices(); if (availableInputs.contains(AvailableInputDevices::Mouse)) diff --git a/Source/WebKit/WebProcess/WebPage/mac/WebPageMac.mm b/Source/WebKit/WebProcess/WebPage/mac/WebPageMac.mm -index ffa97bb7530ddcda26e9680d352026d01301f10d..a4de9c29209e2ed2ce8aced5008211c33d8178d2 100644 +index da7ee3f33f1f158f1c7d57647be27aacf66c0941..f6b4252e20fcfac11aa22a74d5e4bb2156e80c01 100644 --- a/Source/WebKit/WebProcess/WebPage/mac/WebPageMac.mm +++ b/Source/WebKit/WebProcess/WebPage/mac/WebPageMac.mm -@@ -732,21 +732,37 @@ String WebPage::platformUserAgent(const URL&) const +@@ -755,21 +755,37 @@ String WebPage::platformUserAgent(const URL&) const bool WebPage::hoverSupportedByPrimaryPointingDevice() const { @@ -18770,7 +19107,7 @@ index ffa97bb7530ddcda26e9680d352026d01301f10d..a4de9c29209e2ed2ce8aced5008211c3 } diff --git a/Source/WebKit/WebProcess/WebPage/win/WebPageWin.cpp b/Source/WebKit/WebProcess/WebPage/win/WebPageWin.cpp -index c177e632d86f01cd59c3a8c6d922da5ddbaf6e55..516472f97a378ebe3cd8bd9fd30700a26d4a6085 100644 +index 506ae38c6c54a2866ce44271575d8731ba1fea2c..52791bcd240e5eeb68c739de361fb62b4a424e43 100644 --- a/Source/WebKit/WebProcess/WebPage/win/WebPageWin.cpp +++ b/Source/WebKit/WebProcess/WebPage/win/WebPageWin.cpp @@ -45,6 +45,7 @@ @@ -18820,10 +19157,10 @@ index c177e632d86f01cd59c3a8c6d922da5ddbaf6e55..516472f97a378ebe3cd8bd9fd30700a2 } diff --git a/Source/WebKit/WebProcess/WebProcess.cpp b/Source/WebKit/WebProcess/WebProcess.cpp -index 09651c68a3694eda504e88f3dda0cad5cd97a3c6..f4a0d15d58a30b4fa059659ba99bd4aa91ed327d 100644 +index fb4336ddab5501feecd6f5e555f5fbe23e3db309..77b6e8e0cad2d610fb39c40f50c355ccdb898773 100644 --- a/Source/WebKit/WebProcess/WebProcess.cpp +++ b/Source/WebKit/WebProcess/WebProcess.cpp -@@ -95,6 +95,7 @@ +@@ -96,6 +96,7 @@ #include "WebsiteData.h" #include "WebsiteDataStoreParameters.h" #include "WebsiteDataType.h" @@ -18831,7 +19168,7 @@ index 09651c68a3694eda504e88f3dda0cad5cd97a3c6..f4a0d15d58a30b4fa059659ba99bd4aa #include #include #include -@@ -427,6 +428,14 @@ void WebProcess::initializeProcess(const AuxiliaryProcessInitializationParameter +@@ -429,6 +430,14 @@ void WebProcess::initializeProcess(const AuxiliaryProcessInitializationParameter { JSC::Options::AllowUnfinalizedAccessScope scope; JSC::Options::allowNonSPTagging() = false; @@ -18846,7 +19183,7 @@ index 09651c68a3694eda504e88f3dda0cad5cd97a3c6..f4a0d15d58a30b4fa059659ba99bd4aa JSC::Options::notifyOptionsChanged(); } -@@ -434,6 +443,8 @@ void WebProcess::initializeProcess(const AuxiliaryProcessInitializationParameter +@@ -436,6 +445,8 @@ void WebProcess::initializeProcess(const AuxiliaryProcessInitializationParameter platformInitializeProcess(parameters); updateCPULimit(); @@ -18855,7 +19192,7 @@ index 09651c68a3694eda504e88f3dda0cad5cd97a3c6..f4a0d15d58a30b4fa059659ba99bd4aa } void WebProcess::initializeConnection(IPC::Connection* connection) -@@ -1058,6 +1069,7 @@ void WebProcess::createWebPage(PageIdentifier pageID, WebPageCreationParameters& +@@ -1067,6 +1078,7 @@ void WebProcess::createWebPage(PageIdentifier pageID, WebPageCreationParameters& m_hasPendingAccessibilityUnsuspension = false; accessibilityRelayProcessSuspended(false); } @@ -18864,7 +19201,7 @@ index 09651c68a3694eda504e88f3dda0cad5cd97a3c6..f4a0d15d58a30b4fa059659ba99bd4aa Awaitable WebProcess::countWebPagesForTesting() diff --git a/Source/WebKitLegacy/mac/WebView/WebHTMLView.mm b/Source/WebKitLegacy/mac/WebView/WebHTMLView.mm -index ba7911b2efe2ffdde8ab90f3c81817f65a43b2f0..259418a4eca2bb6ed9af2a62fbda155849c2f349 100644 +index 18cb263c7e49a6ad8735ac0318692f15e3548bf3..84bb31229c2baf7c34b7ccbf43ac974ef32e5736 100644 --- a/Source/WebKitLegacy/mac/WebView/WebHTMLView.mm +++ b/Source/WebKitLegacy/mac/WebView/WebHTMLView.mm @@ -4207,7 +4207,7 @@ ALLOW_DEPRECATED_DECLARATIONS_END @@ -18877,10 +19214,10 @@ index ba7911b2efe2ffdde8ab90f3c81817f65a43b2f0..259418a4eca2bb6ed9af2a62fbda1558 - (void)touch:(WebEvent *)event { diff --git a/Source/WebKitLegacy/mac/WebView/WebView.mm b/Source/WebKitLegacy/mac/WebView/WebView.mm -index bec7313812c922a7175d0f9aca27818066d54df8..8af7c061efe469493d5e2f38679b7038fb483c34 100644 +index d78a0df60635c236f5f58c9166551230af89c6ca..b1c01c4a741f53b4870a5cf36faf441c12d1771e 100644 --- a/Source/WebKitLegacy/mac/WebView/WebView.mm +++ b/Source/WebKitLegacy/mac/WebView/WebView.mm -@@ -3976,7 +3976,7 @@ + (void)_doNotStartObservingNetworkReachability +@@ -3974,7 +3974,7 @@ + (void)_doNotStartObservingNetworkReachability } #endif // PLATFORM(IOS_FAMILY) @@ -18889,7 +19226,7 @@ index bec7313812c922a7175d0f9aca27818066d54df8..8af7c061efe469493d5e2f38679b7038 - (NSArray *)_touchEventRegions { -@@ -4018,7 +4018,7 @@ - (NSArray *)_touchEventRegions +@@ -4016,7 +4016,7 @@ - (NSArray *)_touchEventRegions }).autorelease(); } @@ -18899,10 +19236,10 @@ index bec7313812c922a7175d0f9aca27818066d54df8..8af7c061efe469493d5e2f38679b7038 // For backwards compatibility with the WebBackForwardList API, we honor both // a per-WebView and a per-preferences setting for whether to use the back/forward cache. diff --git a/Source/cmake/OptionsGTK.cmake b/Source/cmake/OptionsGTK.cmake -index 1f1b576f84c8ad254868d8e2b2f4f0429ea59d06..11e5f0d982166f5f5c6e44984d48b912409cfff4 100644 +index 98b94f2d211c6546087f8aff57798ff6caadde56..5d32fffbdce8568e320dc4a864b81808f3ec6638 100644 --- a/Source/cmake/OptionsGTK.cmake +++ b/Source/cmake/OptionsGTK.cmake -@@ -9,6 +9,10 @@ set(USER_AGENT_BRANDING "" CACHE STRING "Branding to add to user agent string") +@@ -12,6 +12,10 @@ list(APPEND WEBKIT_UNSAFE_BUFFER_WARNING_FLAGS -Wno-unsafe-buffer-usage-in-forma # Update Source/WTF/wtf/Platform.h to match required GLib versions. find_package(GLib 2.70.0 REQUIRED COMPONENTS GioUnix Thread Module) @@ -18913,7 +19250,7 @@ index 1f1b576f84c8ad254868d8e2b2f4f0429ea59d06..11e5f0d982166f5f5c6e44984d48b912 find_package(Cairo 1.16.0 REQUIRED) find_package(LibGcrypt 1.7.0 REQUIRED) find_package(Soup3 3.0.0 REQUIRED) -@@ -72,6 +76,10 @@ WEBKIT_OPTION_DEFINE(USE_SYSTEM_UNIFDEF "Whether to use a system-provided unifde +@@ -76,6 +80,10 @@ WEBKIT_OPTION_DEFINE(USE_SYSTEM_UNIFDEF "Whether to use a system-provided unifde WEBKIT_OPTION_DEPEND(USE_SYSTEM_SYSPROF_CAPTURE USE_SYSPROF_CAPTURE) @@ -18924,7 +19261,7 @@ index 1f1b576f84c8ad254868d8e2b2f4f0429ea59d06..11e5f0d982166f5f5c6e44984d48b912 SET_AND_EXPOSE_TO_BUILD(ENABLE_DEVELOPER_MODE ${DEVELOPER_MODE}) if (DEVELOPER_MODE) WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_API_TESTS PRIVATE ON) -@@ -150,6 +158,21 @@ WEBKIT_OPTION_DEFAULT_PORT_VALUE(USE_SKIA PRIVATE ON) +@@ -154,6 +162,21 @@ WEBKIT_OPTION_DEFAULT_PORT_VALUE(USE_SKIA PRIVATE ON) WEBKIT_OPTION_DEPEND(ENABLE_GPU_PROCESS USE_GBM) @@ -18947,10 +19284,10 @@ index 1f1b576f84c8ad254868d8e2b2f4f0429ea59d06..11e5f0d982166f5f5c6e44984d48b912 WEBKIT_OPTION_DEPEND(ENABLE_WEBXR ENABLE_GAMEPAD) diff --git a/Source/cmake/OptionsWPE.cmake b/Source/cmake/OptionsWPE.cmake -index 1c76c10e05d9af72dd9e82ced3e045c5ab9634f3..ec3dfda6e819846cab8c51600214789c27cc47a7 100644 +index 6300268001d84bd43fb337d6aeabc4a2dba837ca..0afd7bfcbb4016cd4b544294d9e9c42858301836 100644 --- a/Source/cmake/OptionsWPE.cmake +++ b/Source/cmake/OptionsWPE.cmake -@@ -37,6 +37,9 @@ else () +@@ -40,6 +40,9 @@ else () set(ENABLE_MEDIA_SESSION_DEFAULT ON) endif () @@ -18960,7 +19297,7 @@ index 1c76c10e05d9af72dd9e82ced3e045c5ab9634f3..ec3dfda6e819846cab8c51600214789c WEBKIT_OPTION_BEGIN() SET_AND_EXPOSE_TO_BUILD(ENABLE_DEVELOPER_MODE ${DEVELOPER_MODE}) -@@ -94,6 +97,22 @@ WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_WEBXR PRIVATE ${ENABLE_EXPERIMENTAL_FEAT +@@ -98,6 +101,22 @@ WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_WEBXR PRIVATE ${ENABLE_EXPERIMENTAL_FEAT WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_WEBXR_HIT_TEST PRIVATE ${ENABLE_EXPERIMENTAL_FEATURES}) WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_WEBXR_LAYERS PRIVATE ${ENABLE_EXPERIMENTAL_FEATURES}) @@ -18983,7 +19320,7 @@ index 1c76c10e05d9af72dd9e82ced3e045c5ab9634f3..ec3dfda6e819846cab8c51600214789c # Public options specific to the WPE port. Do not add any options here unless # there is a strong reason we should support changing the value of the option, # and the option is not relevant to other WebKit ports. -@@ -131,6 +150,11 @@ WEBKIT_OPTION_DEPEND(ENABLE_DOCUMENTATION ENABLE_INTROSPECTION) +@@ -136,6 +155,11 @@ WEBKIT_OPTION_DEPEND(ENABLE_DOCUMENTATION ENABLE_INTROSPECTION) WEBKIT_OPTION_DEPEND(ENABLE_WPE_QT_API ENABLE_WPE_PLATFORM) WEBKIT_OPTION_DEPEND(USE_SYSTEM_SYSPROF_CAPTURE USE_SYSPROF_CAPTURE) @@ -18996,10 +19333,10 @@ index 1c76c10e05d9af72dd9e82ced3e045c5ab9634f3..ec3dfda6e819846cab8c51600214789c WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_BUBBLEWRAP_SANDBOX PUBLIC ON) WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_MEMORY_SAMPLER PRIVATE ON) diff --git a/Source/cmake/OptionsWin.cmake b/Source/cmake/OptionsWin.cmake -index da6c3b5fd4901b64c616d2b38cb1d9b70c810e71..99380f834b6b3ac5b4afdf68dfa002c0cdf606d3 100644 +index 49734f2d6e59d7410951f9aa2eaa2684fb88c849..2194af76de8cc2cfcb0f3d8fffa9acc5660ffccd 100644 --- a/Source/cmake/OptionsWin.cmake +++ b/Source/cmake/OptionsWin.cmake -@@ -113,6 +113,14 @@ WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_FTPDIR PRIVATE OFF) +@@ -115,6 +115,14 @@ WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_FTPDIR PRIVATE OFF) SET_AND_EXPOSE_TO_BUILD(ENABLE_WEBDRIVER_KEYBOARD_INTERACTIONS ON) SET_AND_EXPOSE_TO_BUILD(ENABLE_WEBDRIVER_MOUSE_INTERACTIONS ON) @@ -19015,12 +19352,12 @@ index da6c3b5fd4901b64c616d2b38cb1d9b70c810e71..99380f834b6b3ac5b4afdf68dfa002c0 set(USE_ANGLE_EGL ON) diff --git a/Source/cmake/WebKitCompilerFlags.cmake b/Source/cmake/WebKitCompilerFlags.cmake -index 1f63435d0bf53312c627b9a8dab590613fbb93ff..cb1442264f62338a7b8a681527098348103794e8 100644 +index 6c0a4215df55df968e78769b425de7163c08b735..23d83d88fee4fa14bf710c0f6720ad7d1b32cb3f 100644 --- a/Source/cmake/WebKitCompilerFlags.cmake +++ b/Source/cmake/WebKitCompilerFlags.cmake -@@ -152,7 +152,7 @@ macro(WEBKIT_ADD_TARGET_CXX_FLAGS _target) - endmacro() - +@@ -161,7 +161,7 @@ set(WEBKIT_UNSAFE_BUFFER_WARNING_FLAGS + ) + option(ENABLE_UNSAFE_BUFFER_USAGE_WARNING "Build with -Wunsafe-buffer-usage" OFF) -option(DEVELOPER_MODE_FATAL_WARNINGS "Build with warnings as errors if DEVELOPER_MODE is also enabled" ON) +option(DEVELOPER_MODE_FATAL_WARNINGS "Build with warnings as errors if DEVELOPER_MODE is also enabled" OFF) @@ -19119,7 +19456,7 @@ index 0e7f9c7bb7ccb52ccb579ff464a5904aaab2baed..65999b8d7c2dfd1aecc58fe93504aa9c } diff --git a/Tools/MiniBrowser/gtk/BrowserWindow.c b/Tools/MiniBrowser/gtk/BrowserWindow.c -index d39b809a879babcdbbf4b5f7687204df5ccc43f3..a784ea4e693332aee731c4b8949ddb33af7dff9f 100644 +index e5c3084cb308dc236bb9f74d1ed792decbeab908..6b47ca77354bba7859ed3820a4c5cae68b764906 100644 --- a/Tools/MiniBrowser/gtk/BrowserWindow.c +++ b/Tools/MiniBrowser/gtk/BrowserWindow.c @@ -73,7 +73,7 @@ struct _BrowserWindowClass { @@ -19152,7 +19489,7 @@ index d39b809a879babcdbbf4b5f7687204df5ccc43f3..a784ea4e693332aee731c4b8949ddb33 gtk_window_set_title(GTK_WINDOW(window), privateTitle ? privateTitle : title); g_free(privateTitle); } -@@ -524,8 +518,12 @@ static gboolean webViewDecidePolicy(WebKitWebView *webView, WebKitPolicyDecision +@@ -527,8 +521,12 @@ static gboolean webViewDecidePolicy(WebKitWebView *webView, WebKitPolicyDecision return FALSE; WebKitNavigationAction *navigationAction = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(decision)); @@ -19167,7 +19504,7 @@ index d39b809a879babcdbbf4b5f7687204df5ccc43f3..a784ea4e693332aee731c4b8949ddb33 return FALSE; /* Multiple tabs are not allowed in editor mode. */ -@@ -1502,6 +1500,20 @@ static gboolean browserWindowDeleteEvent(GtkWidget *widget, GdkEventAny* event) +@@ -1505,6 +1503,20 @@ static gboolean browserWindowDeleteEvent(GtkWidget *widget, GdkEventAny* event) } #endif @@ -19188,7 +19525,7 @@ index d39b809a879babcdbbf4b5f7687204df5ccc43f3..a784ea4e693332aee731c4b8949ddb33 static void browser_window_class_init(BrowserWindowClass *klass) { GObjectClass *gobjectClass = G_OBJECT_CLASS(klass); -@@ -1515,6 +1527,13 @@ static void browser_window_class_init(BrowserWindowClass *klass) +@@ -1518,6 +1530,13 @@ static void browser_window_class_init(BrowserWindowClass *klass) GtkWidgetClass *widgetClass = GTK_WIDGET_CLASS(klass); widgetClass->delete_event = browserWindowDeleteEvent; #endif @@ -19216,10 +19553,10 @@ index 1fd07efb828b85b6d8def6c6cd92a0c11debfe1b..da9fac7975d477857ead2adb1d67108d typedef struct _BrowserWindow BrowserWindow; diff --git a/Tools/MiniBrowser/gtk/main.c b/Tools/MiniBrowser/gtk/main.c -index dc20509081346978e0da4a7833f6d4ce733717ac..817bd32756f0c8279beb4c23bee4b1e6f2ded960 100644 +index 8722194f5a3007d7eeb90e5517c129afcfbef45e..068d50bdd954578b9cd7dc091059976dd1f9f216 100644 --- a/Tools/MiniBrowser/gtk/main.c +++ b/Tools/MiniBrowser/gtk/main.c -@@ -66,9 +66,15 @@ static char* timeZone; +@@ -68,9 +68,15 @@ static char* timeZone; static gboolean enableITP; static gboolean exitAfterLoad; static gboolean webProcessCrashed; @@ -19235,7 +19572,7 @@ index dc20509081346978e0da4a7833f6d4ce733717ac..817bd32756f0c8279beb4c23bee4b1e6 #if !GTK_CHECK_VERSION(3, 98, 0) static gboolean enableSandbox; -@@ -174,6 +180,11 @@ static const GOptionEntry commandLineOptions[] = +@@ -178,6 +184,11 @@ static const GOptionEntry commandLineOptions[] = { "time-zone", 't', 0, G_OPTION_ARG_STRING, &timeZone, "Set time zone", "TIMEZONE" }, { "version", 'v', 0, G_OPTION_ARG_NONE, &printVersion, "Print the WebKitGTK version", NULL }, { "config", 'C', 0, G_OPTION_ARG_FILENAME, &configFile, "Path to a configuration file", "PATH" }, @@ -19247,7 +19584,7 @@ index dc20509081346978e0da4a7833f6d4ce733717ac..817bd32756f0c8279beb4c23bee4b1e6 { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &uriArguments, 0, "[URL…]" }, { 0, 0, 0, 0, 0, 0, 0 } }; -@@ -730,6 +741,64 @@ static void filterSavedCallback(WebKitUserContentFilterStore *store, GAsyncResul +@@ -734,6 +745,64 @@ static void filterSavedCallback(WebKitUserContentFilterStore *store, GAsyncResul g_main_loop_quit(data->mainLoop); } @@ -19312,7 +19649,7 @@ index dc20509081346978e0da4a7833f6d4ce733717ac..817bd32756f0c8279beb4c23bee4b1e6 static void startup(GApplication *application) { const char *actionAccels[] = { -@@ -788,22 +857,39 @@ static void setupDarkMode(GtkSettings *settings) +@@ -805,22 +874,39 @@ static void addUserScript(WebKitUserContentManager *userContentManager, const gc static void activate(GApplication *application, WebKitSettings *webkitSettings) { @@ -19356,7 +19693,7 @@ index dc20509081346978e0da4a7833f6d4ce733717ac..817bd32756f0c8279beb4c23bee4b1e6 webkit_network_session_set_itp_enabled(networkSession, enableITP); if (!automationMode) { -@@ -838,9 +924,12 @@ static void activate(GApplication *application, WebKitSettings *webkitSettings) +@@ -855,9 +941,12 @@ static void activate(GApplication *application, WebKitSettings *webkitSettings) } #else WebKitWebsiteDataManager *manager; @@ -19371,7 +19708,7 @@ index dc20509081346978e0da4a7833f6d4ce733717ac..817bd32756f0c8279beb4c23bee4b1e6 g_autofree char *dataDirectory = profileDirectory ? g_build_filename(profileDirectory, "data", NULL) : g_build_filename(g_get_user_data_dir(), "webkitgtk-" WEBKITGTK_API_VERSION, "MiniBrowser", NULL); g_autofree char *cacheDirectory = profileDirectory ? g_build_filename(profileDirectory, "cache", NULL) : g_build_filename(g_get_user_cache_dir(), "webkitgtk-" WEBKITGTK_API_VERSION, "MiniBrowser", NULL); manager = webkit_website_data_manager_new("base-data-directory", dataDirectory, "base-cache-directory", cacheDirectory, NULL); -@@ -888,6 +977,7 @@ static void activate(GApplication *application, WebKitSettings *webkitSettings) +@@ -905,6 +994,7 @@ static void activate(GApplication *application, WebKitSettings *webkitSettings) // Enable the favicon database. webkit_web_context_set_favicon_database_directory(webContext, NULL); #endif @@ -19379,7 +19716,7 @@ index dc20509081346978e0da4a7833f6d4ce733717ac..817bd32756f0c8279beb4c23bee4b1e6 webkit_web_context_register_uri_scheme(webContext, BROWSER_ABOUT_SCHEME, (WebKitURISchemeRequestCallback)aboutURISchemeRequestCallback, NULL, NULL); -@@ -952,9 +1042,7 @@ static void activate(GApplication *application, WebKitSettings *webkitSettings) +@@ -978,9 +1068,7 @@ static void activate(GApplication *application, WebKitSettings *webkitSettings) if (exitAfterLoad) exitAfterWebViewLoadFinishes(webView, application); } @@ -19390,7 +19727,7 @@ index dc20509081346978e0da4a7833f6d4ce733717ac..817bd32756f0c8279beb4c23bee4b1e6 } } else { WebKitWebView *webView = createBrowserTab(mainWindow, webkitSettings, userContentManager, defaultWebsitePolicies); -@@ -1004,7 +1092,7 @@ int main(int argc, char *argv[]) +@@ -1030,7 +1118,7 @@ int main(int argc, char *argv[]) g_option_context_add_group(context, gst_init_get_option_group()); #endif @@ -19399,7 +19736,7 @@ index dc20509081346978e0da4a7833f6d4ce733717ac..817bd32756f0c8279beb4c23bee4b1e6 webkit_settings_set_enable_developer_extras(webkitSettings, TRUE); webkit_settings_set_enable_webgl(webkitSettings, TRUE); webkit_settings_set_enable_media_stream(webkitSettings, TRUE); -@@ -1056,9 +1144,11 @@ int main(int argc, char *argv[]) +@@ -1082,9 +1170,11 @@ int main(int argc, char *argv[]) } GtkApplication *application = gtk_application_new("org.webkitgtk.MiniBrowser", G_APPLICATION_NON_UNIQUE); @@ -19412,30 +19749,32 @@ index dc20509081346978e0da4a7833f6d4ce733717ac..817bd32756f0c8279beb4c23bee4b1e6 g_clear_object(&interfaceSettings); diff --git a/Tools/MiniBrowser/wpe/main.cpp b/Tools/MiniBrowser/wpe/main.cpp -index c8cd8719368bc3be7b97b315c64162ced5b7c351..7a7b2d3b0ebc31690f5877694118ddfc311ee5cf 100644 +index c8cd8719368bc3be7b97b315c64162ced5b7c351..69d25479b7a0a1afcffcdad6a62f4f3857c92cac 100644 --- a/Tools/MiniBrowser/wpe/main.cpp +++ b/Tools/MiniBrowser/wpe/main.cpp -@@ -56,6 +56,9 @@ static gboolean privateMode; +@@ -56,6 +56,10 @@ static gboolean privateMode; static const char* profileDirectory; static gboolean automationMode; static gboolean ignoreTLSErrors; +static gboolean inspectorPipe; ++static gint remoteDebuggingPort = -1; +static gboolean noStartupWindow; +static const char* userDataDir; static const char* contentFilter; static const char* cookiesFile; static const char* cookiesPolicy; -@@ -141,6 +144,9 @@ static const GOptionEntry commandLineOptions[] = +@@ -141,6 +145,10 @@ static const GOptionEntry commandLineOptions[] = { "config-file", 0, 0, G_OPTION_ARG_FILENAME, &configFile, "Config file to load for settings", "FILE" }, { "size", 's', 0, G_OPTION_ARG_CALLBACK, reinterpret_cast(parseWindowSize), "Specify the window size to use, e.g. --size=\"800x600\"", nullptr }, { "version", 'v', 0, G_OPTION_ARG_NONE, &printVersion, "Print the WPE version", nullptr }, + { "inspector-pipe", 'v', 0, G_OPTION_ARG_NONE, &inspectorPipe, "Expose remote debugging protocol over pipe", nullptr }, ++ { "remote-debugging-port", 0, 0, G_OPTION_ARG_INT, &remoteDebuggingPort, "Start remote debugging server on the specified port", NULL }, + { "user-data-dir", 0, 0, G_OPTION_ARG_STRING, &userDataDir, "Default profile persistence folder location", "FILE" }, + { "no-startup-window", 0, 0, G_OPTION_ARG_NONE, &noStartupWindow, "Do not open default page", nullptr }, { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &uriArguments, nullptr, "[URL]" }, { nullptr, 0, 0, G_OPTION_ARG_NONE, nullptr, nullptr, nullptr } }; -@@ -331,15 +337,38 @@ static void filterSavedCallback(WebKitUserContentFilterStore *store, GAsyncResul +@@ -331,15 +339,38 @@ static void filterSavedCallback(WebKitUserContentFilterStore *store, GAsyncResul g_main_loop_quit(data->mainLoop); } @@ -19476,7 +19815,7 @@ index c8cd8719368bc3be7b97b315c64162ced5b7c351..7a7b2d3b0ebc31690f5877694118ddfc { #if defined(USE_LIBWPE) && USE_LIBWPE auto backend = createViewBackend(defaultWindowWidthLegacyAPI, defaultWindowHeightLegacyAPI); -@@ -356,14 +385,31 @@ static WebKitWebView* createWebView(WebKitWebView* webView, WebKitNavigationActi +@@ -356,14 +387,31 @@ static WebKitWebView* createWebView(WebKitWebView* webView, WebKitNavigationActi } #endif @@ -19514,7 +19853,7 @@ index c8cd8719368bc3be7b97b315c64162ced5b7c351..7a7b2d3b0ebc31690f5877694118ddfc #if ENABLE_WPE_PLATFORM if (auto* wpeView = webkit_web_view_get_wpe_view(newWebView)) { -@@ -375,9 +421,13 @@ static WebKitWebView* createWebView(WebKitWebView* webView, WebKitNavigationActi +@@ -375,9 +423,13 @@ static WebKitWebView* createWebView(WebKitWebView* webView, WebKitNavigationActi g_signal_connect(newWebView, "create", G_CALLBACK(createWebView), user_data); g_signal_connect(newWebView, "close", G_CALLBACK(webViewClose), user_data); @@ -19530,7 +19869,7 @@ index c8cd8719368bc3be7b97b315c64162ced5b7c351..7a7b2d3b0ebc31690f5877694118ddfc return newWebView; } -@@ -468,24 +518,112 @@ static void loadConfigFile(WebKitSettings* webkitSettings +@@ -468,24 +520,120 @@ static void loadConfigFile(WebKitSettings* webkitSettings } #if defined(USE_LIBWPE) && USE_LIBWPE @@ -19616,6 +19955,14 @@ index c8cd8719368bc3be7b97b315c64162ced5b7c351..7a7b2d3b0ebc31690f5877694118ddfc + g_signal_connect(browserInspector, "quit-application", G_CALLBACK(quitBroserApplication), application); + webkit_browser_inspector_initialize_pipe(proxy, ignoreHosts); +} ++ ++static void configureBrowserInspectorPort(GApplication* application) ++{ ++ WebKitBrowserInspector* browserInspector = webkit_browser_inspector_get_default(); ++ g_signal_connect(browserInspector, "create-new-page", G_CALLBACK(createNewPage), NULL); ++ g_signal_connect(browserInspector, "quit-application", G_CALLBACK(quitBroserApplication), application); ++ webkit_browser_inspector_initialize_web_socket(remoteDebuggingPort, proxy, ignoreHosts); ++} + static void activate(GApplication* application, WPEToolingBackends::ViewBackend* backend) #else @@ -19632,7 +19979,7 @@ index c8cd8719368bc3be7b97b315c64162ced5b7c351..7a7b2d3b0ebc31690f5877694118ddfc + if (userDataDir) { + networkSession = webkit_network_session_new(userDataDir, userDataDir); + cookiesFile = g_build_filename(userDataDir, "cookies.txt", nullptr); -+ } else if (inspectorPipe || privateMode || automationMode) { ++ } else if (inspectorPipe || remoteDebuggingPort != -1 || privateMode || automationMode) { networkSession = webkit_network_session_new_ephemeral(); - else if (profileDirectory) { + } else if (profileDirectory) { @@ -19647,7 +19994,7 @@ index c8cd8719368bc3be7b97b315c64162ced5b7c351..7a7b2d3b0ebc31690f5877694118ddfc webkit_network_session_set_itp_enabled(networkSession, enableITP); if (proxy) { -@@ -512,19 +650,22 @@ static void activate(GApplication* application, gpointer) +@@ -512,19 +660,22 @@ static void activate(GApplication* application, gpointer) webkit_cookie_manager_set_persistent_storage(cookieManager, cookiesFile, storageType); } } @@ -19661,7 +20008,7 @@ index c8cd8719368bc3be7b97b315c64162ced5b7c351..7a7b2d3b0ebc31690f5877694118ddfc + if (userDataDir) { + manager = webkit_website_data_manager_new("base-data-directory", userDataDir, "base-cache-directory", userDataDir, NULL); + cookiesFile = g_build_filename(userDataDir, "cookies.txt", NULL); -+ } else if (inspectorPipe || privateMode || automationMode) { ++ } else if (inspectorPipe || remoteDebuggingPort != -1 || privateMode || automationMode) { manager = webkit_website_data_manager_new_ephemeral(); - else if (profileDirectory) { + } else if (profileDirectory) { @@ -19677,7 +20024,7 @@ index c8cd8719368bc3be7b97b315c64162ced5b7c351..7a7b2d3b0ebc31690f5877694118ddfc webkit_website_data_manager_set_itp_enabled(manager, enableITP); if (proxy) { -@@ -555,6 +696,7 @@ static void activate(GApplication* application, gpointer) +@@ -555,6 +706,7 @@ static void activate(GApplication* application, gpointer) } #endif @@ -19685,7 +20032,7 @@ index c8cd8719368bc3be7b97b315c64162ced5b7c351..7a7b2d3b0ebc31690f5877694118ddfc g_autoptr(WebKitUserContentManager) userContentManager = nullptr; if (contentFilter) { g_autoptr(GFile) contentFilterFile = g_file_new_for_commandline_arg(contentFilter); -@@ -637,6 +779,15 @@ static void activate(GApplication* application, gpointer) +@@ -637,6 +789,15 @@ static void activate(GApplication* application, gpointer) "autoplay", WEBKIT_AUTOPLAY_ALLOW, nullptr); @@ -19701,7 +20048,7 @@ index c8cd8719368bc3be7b97b315c64162ced5b7c351..7a7b2d3b0ebc31690f5877694118ddfc auto* webView = WEBKIT_WEB_VIEW(g_object_new(WEBKIT_TYPE_WEB_VIEW, #if defined(USE_LIBWPE) && USE_LIBWPE "backend", viewBackend, -@@ -699,12 +850,16 @@ static void activate(GApplication* application, gpointer) +@@ -699,12 +860,16 @@ static void activate(GApplication* application, gpointer) #endif } @@ -19720,7 +20067,7 @@ index c8cd8719368bc3be7b97b315c64162ced5b7c351..7a7b2d3b0ebc31690f5877694118ddfc g_hash_table_add(openViews, webView); WebKitColor color; -@@ -712,16 +867,11 @@ static void activate(GApplication* application, gpointer) +@@ -712,16 +877,11 @@ static void activate(GApplication* application, gpointer) webkit_web_view_set_background_color(webView, &color); if (uriArguments) { @@ -19742,7 +20089,7 @@ index c8cd8719368bc3be7b97b315c64162ced5b7c351..7a7b2d3b0ebc31690f5877694118ddfc webkit_web_view_load_uri(webView, "https://wpewebkit.org"); g_object_unref(webContext); -@@ -816,12 +966,18 @@ int main(int argc, char *argv[]) +@@ -816,12 +976,20 @@ int main(int argc, char *argv[]) } #endif @@ -19757,6 +20104,8 @@ index c8cd8719368bc3be7b97b315c64162ced5b7c351..7a7b2d3b0ebc31690f5877694118ddfc + + if (inspectorPipe) + configureBrowserInspector(application); ++ else if (remoteDebuggingPort != -1) ++ configureBrowserInspectorPort(application); + g_application_run(application, 0, nullptr); g_object_unref(application); @@ -19773,40 +20122,8 @@ index 1067b31bc989748dfcc5502209d36d001b9b239e..7629263fb8bc93dca6dfc01c75eed8d2 +if (ENABLE_WEBKIT) + add_subdirectory(Playwright/win) +endif () -diff --git a/Tools/Scripts/generate-bundle b/Tools/Scripts/generate-bundle -index 2050bc1c4e2f94461cfe25dbc53762afdc6b9f53..cd43f62318d661bf2558a1b8005f37de19eb0f15 100755 ---- a/Tools/Scripts/generate-bundle -+++ b/Tools/Scripts/generate-bundle -@@ -41,7 +41,6 @@ sys.path.insert(0, os.path.join(top_level_directory, 'Tools', 'flatpak')) - sys.path.insert(0, os.path.join(top_level_directory, 'Tools', 'jhbuild')) - sys.path.insert(0, os.path.join(top_level_directory, 'Tools', 'Scripts', 'webkitpy')) - import jhbuildutils --import flatpakutils - from binary_bundling.ldd import SharedObjectResolver - from binary_bundling.bundle import BinaryBundler - -@@ -896,7 +895,7 @@ class BundleCreator(object): - _log.info('Copy basic GTK icons.') - icons_target_dir = os.path.join(target_sys_share_dir, 'icons') - os.makedirs(icons_target_dir) -- gtk_icon_basedir = '/run/host/share/icons' if flatpakutils.is_sandboxed() else '/usr/share/icons' -+ gtk_icon_basedir = '/usr/share/icons' - gtk_target_icon_dir = os.path.join(icons_target_dir, 'hicolor') - gtk_icon_dirs_copied = 0 - for gtk_icon_theme in ['Adwaita', 'hicolor', 'gnome']: -@@ -975,9 +974,7 @@ def main(): - parser.add_argument('--builder-name', action='store', dest='builder_name') - options = parser.parse_args() - -- flatpakutils.run_in_sandbox_if_available([sys.argv[0], '--flatpak-' + options.platform] + sys.argv[1:]) -- if not flatpakutils.is_sandboxed(): -- jhbuildutils.enter_jhbuild_environment_if_available(options.platform) -+ jhbuildutils.enter_jhbuild_environment_if_available(options.platform) - - configure_logging(options.log_level) - bundle_creator = BundleCreator(options.configuration, options.platform, options.bundle_binary, options.syslibs, options.ldd, diff --git a/Tools/WebKitTestRunner/CMakeLists.txt b/Tools/WebKitTestRunner/CMakeLists.txt -index ba703658cf8c56ad1768333b70938e67e8b36777..804487a7082024733d5012dcc62b9784c268bb55 100644 +index a2c72d2d46f90d1851dfd64b718b55580321adaf..4877227f61591a1bc437cacaa5e792ebdb014af8 100644 --- a/Tools/WebKitTestRunner/CMakeLists.txt +++ b/Tools/WebKitTestRunner/CMakeLists.txt @@ -99,6 +99,10 @@ set(TestRunnerInjectedBundle_PRIVATE_LIBRARIES @@ -19821,10 +20138,10 @@ index ba703658cf8c56ad1768333b70938e67e8b36777..804487a7082024733d5012dcc62b9784 "${WebKitTestRunner_DIR}/InjectedBundle/Bindings/AccessibilityController.idl" "${WebKitTestRunner_DIR}/InjectedBundle/Bindings/AccessibilityTextMarker.idl" diff --git a/Tools/WebKitTestRunner/TestController.cpp b/Tools/WebKitTestRunner/TestController.cpp -index 949b6db7d5946a3981d87ef808c83592532b6a69..fb72fabc3e1b485b2479bfbb2ecb05c909427aa6 100644 +index a77f218b68b637c92571a1b5c5b39cbb0631f5a1..db2ebbf139c827855c215144e18fef3d222047c7 100644 --- a/Tools/WebKitTestRunner/TestController.cpp +++ b/Tools/WebKitTestRunner/TestController.cpp -@@ -792,6 +792,7 @@ PlatformWebView* TestController::createOtherPlatformWebView(PlatformWebView* par +@@ -810,6 +810,7 @@ PlatformWebView* TestController::createOtherPlatformWebView(PlatformWebView* par nullptr, // requestStorageAccessConfirm nullptr, // shouldAllowDeviceOrientationAndMotionAccess nullptr, // runWebAuthenticationPanel @@ -19832,7 +20149,7 @@ index 949b6db7d5946a3981d87ef808c83592532b6a69..fb72fabc3e1b485b2479bfbb2ecb05c9 nullptr, // decidePolicyForSpeechRecognitionPermissionRequest nullptr, // decidePolicyForMediaKeySystemPermissionRequest nullptr, // queryPermission -@@ -1272,6 +1273,7 @@ void TestController::createWebViewWithOptions(const TestOptions& options) +@@ -1298,6 +1299,7 @@ void TestController::createWebViewWithOptions(const TestOptions& options) nullptr, // requestStorageAccessConfirm shouldAllowDeviceOrientationAndMotionAccess, runWebAuthenticationPanel, @@ -19841,7 +20158,7 @@ index 949b6db7d5946a3981d87ef808c83592532b6a69..fb72fabc3e1b485b2479bfbb2ecb05c9 decidePolicyForMediaKeySystemPermissionRequest, queryPermission, diff --git a/Tools/WebKitTestRunner/mac/EventSenderProxy.mm b/Tools/WebKitTestRunner/mac/EventSenderProxy.mm -index 12a89be783e49c44edfca8d27f1fc3ddd24255e9..2ba827c958fa3558b0cd36f9e5b7537493fef362 100644 +index 13a94ad7d71f65b38408e0cf52558fbc4610a649..e2a69c365de4cd4aaabfdd19fb3893317b8d7269 100644 --- a/Tools/WebKitTestRunner/mac/EventSenderProxy.mm +++ b/Tools/WebKitTestRunner/mac/EventSenderProxy.mm @@ -1031,4 +1031,51 @@ void EventSenderProxy::scaleGestureEnd(double scale) @@ -19897,11 +20214,11 @@ index 12a89be783e49c44edfca8d27f1fc3ddd24255e9..2ba827c958fa3558b0cd36f9e5b75374 + } // namespace WTR diff --git a/Tools/jhbuild/jhbuild-minimal.modules b/Tools/jhbuild/jhbuild-minimal.modules -index d526231f288ca82f4928d75ee9847b919b72bbfb..c904fac94196a8eb9888abf83b0db5ab69b3a993 100644 +index 8e1df1545699ccf902471cd7e35c0945da9144d7..519fdf07d9c55ed4c2b940e392d1f559cc5c01b2 100644 --- a/Tools/jhbuild/jhbuild-minimal.modules +++ b/Tools/jhbuild/jhbuild-minimal.modules @@ -69,8 +69,8 @@ - + + + +```csharp +using Microsoft.Playwright; +using Microsoft.Playwright.MSTest; + +namespace PlaywrightTests; + +[TestClass] +public class TestGitHubAPI : PlaywrightTest +{ + static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + + private IAPIRequestContext Request = null!; + + [TestInitialize] + public async Task SetUpAPITesting() + { + await CreateAPIRequestContext(); + } + + private async Task CreateAPIRequestContext() + { + var headers = new Dictionary(); + // We set this header per GitHub guidelines. + headers.Add("Accept", "application/vnd.github.v3+json"); + // Add authorization token to all requests. + // Assuming personal access token available in the environment. + headers.Add("Authorization", "token " + API_TOKEN); + + Request = await this.Playwright.APIRequest.NewContextAsync(new() { + // All requests we send go to this API endpoint. + BaseURL = "https://api.github.com", + ExtraHTTPHeaders = headers, + }); + } + + [TestCleanup] + public async Task TearDownAPITesting() + { + await Request.DisposeAsync(); + } +} +``` + + + + +```csharp +using Microsoft.Playwright; +using Microsoft.Playwright.NUnit; +using NUnit.Framework; + +namespace PlaywrightTests; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class TestGitHubAPI : PlaywrightTest +{ + static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + + private IAPIRequestContext Request = null!; + + [SetUp] + public async Task SetUpAPITesting() + { + await CreateAPIRequestContext(); + } + + private async Task CreateAPIRequestContext() + { + var headers = new Dictionary(); + // We set this header per GitHub guidelines. + headers.Add("Accept", "application/vnd.github.v3+json"); + // Add authorization token to all requests. + // Assuming personal access token available in the environment. + headers.Add("Authorization", "token " + API_TOKEN); + + Request = await this.Playwright.APIRequest.NewContextAsync(new() { + // All requests we send go to this API endpoint. + BaseURL = "https://api.github.com", + ExtraHTTPHeaders = headers, + }); + } + + [TearDown] + public async Task TearDownAPITesting() + { + await Request.DisposeAsync(); + } +} +``` + + + + +```csharp +using Microsoft.Playwright; +using Microsoft.Playwright.Xunit; + +namespace PlaywrightTests; + +public class TestGitHubAPI : PlaywrightTest +{ + static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + + private IAPIRequestContext Request = null!; + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + await CreateAPIRequestContext(); + } + + private async Task CreateAPIRequestContext() + { + var headers = new Dictionary(); + // We set this header per GitHub guidelines. + headers.Add("Accept", "application/vnd.github.v3+json"); + // Add authorization token to all requests. + // Assuming personal access token available in the environment. + headers.Add("Authorization", "token " + API_TOKEN); + + Request = await this.Playwright.APIRequest.NewContextAsync(new() { + // All requests we send go to this API endpoint. + BaseURL = "https://api.github.com", + ExtraHTTPHeaders = headers, + }); + } + + public override async Task DisposeAsync() + { + await Request.DisposeAsync(); + await base.DisposeAsync(); + } +} +``` + + + + +```csharp +using Microsoft.Playwright; +using Microsoft.Playwright.Xunit.v3; + +namespace PlaywrightTests; + +public class TestGitHubAPI : PlaywrightTest +{ + static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + + private IAPIRequestContext Request = null!; + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + await CreateAPIRequestContext(); + } + + private async Task CreateAPIRequestContext() + { + var headers = new Dictionary(); + // We set this header per GitHub guidelines. + headers.Add("Accept", "application/vnd.github.v3+json"); + // Add authorization token to all requests. + // Assuming personal access token available in the environment. + headers.Add("Authorization", "token " + API_TOKEN); + + Request = await this.Playwright.APIRequest.NewContextAsync(new() { + // All requests we send go to this API endpoint. + BaseURL = "https://api.github.com", + ExtraHTTPHeaders = headers, + }); + } + + public override async Task DisposeAsync() + { + await Request.DisposeAsync(); + await base.DisposeAsync(); + } +} +``` + + + + +### Write tests + +Now that we initialized request object we can add a few tests that will create new issues in the repository. + + + + +```csharp +using System.Text.Json; +using Microsoft.Playwright; +using Microsoft.Playwright.MSTest; + +namespace PlaywrightTests; + +[TestClass] +public class TestGitHubAPI : PlaywrightTest +{ + static string REPO = "test"; + static string USER = Environment.GetEnvironmentVariable("GITHUB_USER"); + static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + + private IAPIRequestContext Request = null!; + + [TestMethod] + public async Task ShouldCreateBugReport() + { + var data = new Dictionary + { + { "title", "[Bug] report 1" }, + { "body", "Bug description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Bug] report 1") + { + issue = issueObj; + } + } + } + Assert.IsNotNull(issue); + Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString()); + } + + [TestMethod] + public async Task ShouldCreateFeatureRequests() + { + var data = new Dictionary + { + { "title", "[Feature] request 1" }, + { "body", "Feature description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Feature] request 1") + { + issue = issueObj; + } + } + } + Assert.IsNotNull(issue); + Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString()); + } + + // ... +} +``` + + + + +```csharp +using System.Text.Json; +using Microsoft.Playwright; +using Microsoft.Playwright.NUnit; +using NUnit.Framework; + +namespace PlaywrightTests; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class TestGitHubAPI : PlaywrightTest +{ + static string REPO = "test"; + static string USER = Environment.GetEnvironmentVariable("GITHUB_USER"); + static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + + private IAPIRequestContext Request = null!; + + [Test] + public async Task ShouldCreateBugReport() + { + var data = new Dictionary + { + { "title", "[Bug] report 1" }, + { "body", "Bug description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Bug] report 1") + { + issue = issueObj; + } + } + } + Assert.That(issue, Is.Not.Null); + Assert.That(issue?.GetProperty("body").GetString(), Is.EqualTo("Bug description")); + } + + [Test] + public async Task ShouldCreateFeatureRequests() + { + var data = new Dictionary + { + { "title", "[Feature] request 1" }, + { "body", "Feature description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Feature] request 1") + { + issue = issueObj; + } + } + } + Assert.That(issue, Is.Not.Null); + Assert.That(issue?.GetProperty("body").GetString(), Is.EqualTo("Feature description")); + } + + // ... +} +``` + + + + +```csharp +using System.Text.Json; +using Microsoft.Playwright; +using Microsoft.Playwright.Xunit; + +namespace PlaywrightTests; + +public class TestGitHubAPI : PlaywrightTest +{ + static string REPO = "test"; + static string USER = Environment.GetEnvironmentVariable("GITHUB_USER"); + static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + + private IAPIRequestContext Request = null!; + + [Fact] + public async Task ShouldCreateBugReport() + { + var data = new Dictionary + { + { "title", "[Bug] report 1" }, + { "body", "Bug description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Bug] report 1") + { + issue = issueObj; + } + } + } + Assert.NotNull(issue); + Assert.Equal("Bug description", issue?.GetProperty("body").GetString()); + } + + [Fact] + public async Task ShouldCreateFeatureRequests() + { + var data = new Dictionary + { + { "title", "[Feature] request 1" }, + { "body", "Feature description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Feature] request 1") + { + issue = issueObj; + } + } + } + Assert.NotNull(issue); + Assert.Equal("Feature description", issue?.GetProperty("body").GetString()); + } + + // ... +} +``` + + + + +```csharp +using System.Text.Json; +using Microsoft.Playwright; +using Microsoft.Playwright.Xunit.v3; + +namespace PlaywrightTests; + +public class TestGitHubAPI : PlaywrightTest +{ + static string REPO = "test"; + static string USER = Environment.GetEnvironmentVariable("GITHUB_USER"); + static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + + private IAPIRequestContext Request = null!; + + [Fact] + public async Task ShouldCreateBugReport() + { + var data = new Dictionary + { + { "title", "[Bug] report 1" }, + { "body", "Bug description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Bug] report 1") + { + issue = issueObj; + } + } + } + Assert.NotNull(issue); + Assert.Equal("Bug description", issue?.GetProperty("body").GetString()); + } + + [Fact] + public async Task ShouldCreateFeatureRequests() + { + var data = new Dictionary + { + { "title", "[Feature] request 1" }, + { "body", "Feature description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Feature] request 1") + { + issue = issueObj; + } + } + } + Assert.NotNull(issue); + Assert.Equal("Feature description", issue?.GetProperty("body").GetString()); + } + + // ... +} +``` + + + + +### Setup and teardown + +These tests assume that repository exists. You probably want to create a new one before running tests and delete it afterwards. + + + + +Use `[TestInitialize]` and `[TestCleanup]` hooks for that. + +```csharp +using System.Text.Json; +using Microsoft.Playwright; +using Microsoft.Playwright.MSTest; + +namespace PlaywrightTests; + +[TestClass] +public class TestGitHubAPI : PlaywrightTest +{ + // ... + [TestInitialize] + public async Task SetUpAPITesting() + { + await CreateAPIRequestContext(); + await CreateTestRepository(); + } + + private async Task CreateTestRepository() + { + var resp = await Request.PostAsync("/user/repos", new() + { + DataObject = new Dictionary() + { + ["name"] = REPO, + }, + }); + await Expect(resp).ToBeOKAsync(); + } + + [TestCleanup] + public async Task TearDownAPITesting() + { + await DeleteTestRepository(); + await Request.DisposeAsync(); + } + + private async Task DeleteTestRepository() + { + var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO); + await Expect(resp).ToBeOKAsync(); + } +} +``` + + + + +Use `[SetUp]` and `[TearDown]` hooks for that. + +```csharp +using System.Text.Json; +using Microsoft.Playwright; +using Microsoft.Playwright.NUnit; +using NUnit.Framework; + +namespace PlaywrightTests; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class TestGitHubAPI : PlaywrightTest +{ + // ... + [SetUp] + public async Task SetUpAPITesting() + { + await CreateAPIRequestContext(); + await CreateTestRepository(); + } + + private async Task CreateTestRepository() + { + var resp = await Request.PostAsync("/user/repos", new() + { + DataObject = new Dictionary() + { + ["name"] = REPO, + }, + }); + await Expect(resp).ToBeOKAsync(); + } + + [TearDown] + public async Task TearDownAPITesting() + { + await DeleteTestRepository(); + await Request.DisposeAsync(); + } + + private async Task DeleteTestRepository() + { + var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO); + await Expect(resp).ToBeOKAsync(); + } +} +``` + + + + +Override the `InitializeAsync` and `DisposeAsync` methods for that. + +```csharp +using System.Text.Json; +using Microsoft.Playwright; +using Microsoft.Playwright.Xunit; + +namespace PlaywrightTests; + +public class TestGitHubAPI : PlaywrightTest +{ + // ... + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + await CreateAPIRequestContext(); + await CreateTestRepository(); + } + + private async Task CreateTestRepository() + { + var resp = await Request.PostAsync("/user/repos", new() + { + DataObject = new Dictionary() + { + ["name"] = REPO, + }, + }); + await Expect(resp).ToBeOKAsync(); + } + + public override async Task DisposeAsync() + { + await DeleteTestRepository(); + await Request.DisposeAsync(); + await base.DisposeAsync(); + } + + private async Task DeleteTestRepository() + { + var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO); + await Expect(resp).ToBeOKAsync(); + } +} +``` + + + + +Override the `InitializeAsync` and `DisposeAsync` methods for that. + +```csharp +using System.Text.Json; +using Microsoft.Playwright; +using Microsoft.Playwright.Xunit.v3; + +namespace PlaywrightTests; + +public class TestGitHubAPI : PlaywrightTest +{ + // ... + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + await CreateAPIRequestContext(); + await CreateTestRepository(); + } + + private async Task CreateTestRepository() + { + var resp = await Request.PostAsync("/user/repos", new() + { + DataObject = new Dictionary() + { + ["name"] = REPO, + }, + }); + await Expect(resp).ToBeOKAsync(); + } + + public override async Task DisposeAsync() + { + await DeleteTestRepository(); + await Request.DisposeAsync(); + await base.DisposeAsync(); + } + + private async Task DeleteTestRepository() + { + var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO); + await Expect(resp).ToBeOKAsync(); + } +} +``` + + + + +### Complete test example + +Here is the complete example of an API test: + + + + ```csharp +using System.Text.Json; using Microsoft.Playwright; using Microsoft.Playwright.MSTest; @@ -40,60 +813,276 @@ namespace PlaywrightTests; [TestClass] public class TestGitHubAPI : PlaywrightTest { + static string REPO = "test-repo-2"; + static string USER = Environment.GetEnvironmentVariable("GITHUB_USER"); + static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + + private IAPIRequestContext Request = null!; + + [TestMethod] + public async Task ShouldCreateBugReport() + { + var data = new Dictionary + { + { "title", "[Bug] report 1" }, + { "body", "Bug description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Bug] report 1") + { + issue = issueObj; + } + } + } + Assert.IsNotNull(issue); + Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString()); + } + + [TestMethod] + public async Task ShouldCreateFeatureRequests() + { + var data = new Dictionary + { + { "title", "[Feature] request 1" }, + { "body", "Feature description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Feature] request 1") + { + issue = issueObj; + } + } + } + Assert.IsNotNull(issue); + Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString()); + } + + [TestInitialize] + public async Task SetUpAPITesting() + { + await CreateAPIRequestContext(); + await CreateTestRepository(); + } + + private async Task CreateAPIRequestContext() + { + var headers = new Dictionary + { + // We set this header per GitHub guidelines. + { "Accept", "application/vnd.github.v3+json" }, + // Add authorization token to all requests. + // Assuming personal access token available in the environment. + { "Authorization", "token " + API_TOKEN } + }; + + Request = await Playwright.APIRequest.NewContextAsync(new() + { + // All requests we send go to this API endpoint. + BaseURL = "https://api.github.com", + ExtraHTTPHeaders = headers, + }); + } + + private async Task CreateTestRepository() + { + var resp = await Request.PostAsync("/user/repos", new() + { + DataObject = new Dictionary() + { + ["name"] = REPO, + }, + }); + await Expect(resp).ToBeOKAsync(); + } + + [TestCleanup] + public async Task TearDownAPITesting() + { + await DeleteTestRepository(); + await Request.DisposeAsync(); + } + + private async Task DeleteTestRepository() + { + var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO); + await Expect(resp).ToBeOKAsync(); + } +} +``` + + + + +```csharp +using System.Text.Json; +using Microsoft.Playwright; +using Microsoft.Playwright.NUnit; +using NUnit.Framework; + +namespace PlaywrightTests; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class TestGitHubAPI : PlaywrightTest +{ + static string REPO = "test-repo-2"; + static string USER = Environment.GetEnvironmentVariable("GITHUB_USER"); static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); - private IAPIRequestContext Request = null!; + private IAPIRequestContext Request = null!; + + [Test] + public async Task ShouldCreateBugReport() + { + var data = new Dictionary + { + { "title", "[Bug] report 1" }, + { "body", "Bug description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Bug] report 1") + { + issue = issueObj; + } + } + } + Assert.That(issue, Is.Not.Null); + Assert.That(issue?.GetProperty("body").GetString(), Is.EqualTo("Bug description")); + } + + [Test] + public async Task ShouldCreateFeatureRequests() + { + var data = new Dictionary + { + { "title", "[Feature] request 1" }, + { "body", "Feature description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); - [TestInitialize] + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Feature] request 1") + { + issue = issueObj; + } + } + } + Assert.That(issue, Is.Not.Null); + Assert.That(issue?.GetProperty("body").GetString(), Is.EqualTo("Feature description")); + } + + [SetUp] public async Task SetUpAPITesting() { await CreateAPIRequestContext(); + await CreateTestRepository(); } private async Task CreateAPIRequestContext() { - var headers = new Dictionary(); - // We set this header per GitHub guidelines. - headers.Add("Accept", "application/vnd.github.v3+json"); - // Add authorization token to all requests. - // Assuming personal access token available in the environment. - headers.Add("Authorization", "token " + API_TOKEN); + var headers = new Dictionary + { + // We set this header per GitHub guidelines. + { "Accept", "application/vnd.github.v3+json" }, + // Add authorization token to all requests. + // Assuming personal access token available in the environment. + { "Authorization", "token " + API_TOKEN } + }; - Request = await this.Playwright.APIRequest.NewContextAsync(new() { + Request = await Playwright.APIRequest.NewContextAsync(new() + { // All requests we send go to this API endpoint. BaseURL = "https://api.github.com", ExtraHTTPHeaders = headers, }); } - [TestCleanup] + private async Task CreateTestRepository() + { + var resp = await Request.PostAsync("/user/repos", new() + { + DataObject = new Dictionary() + { + ["name"] = REPO, + }, + }); + await Expect(resp).ToBeOKAsync(); + } + + [TearDown] public async Task TearDownAPITesting() { + await DeleteTestRepository(); await Request.DisposeAsync(); } + + private async Task DeleteTestRepository() + { + var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO); + await Expect(resp).ToBeOKAsync(); + } } ``` -### Write tests + + -Now that we initialized request object we can add a few tests that will create new issues in the repository. ```csharp using System.Text.Json; using Microsoft.Playwright; -using Microsoft.Playwright.MSTest; +using Microsoft.Playwright.Xunit; namespace PlaywrightTests; -[TestClass] public class TestGitHubAPI : PlaywrightTest { - static string REPO = "test"; + static string REPO = "test-repo-2"; static string USER = Environment.GetEnvironmentVariable("GITHUB_USER"); static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); private IAPIRequestContext Request = null!; - [TestMethod] + [Fact] public async Task ShouldCreateBugReport() { var data = new Dictionary @@ -118,11 +1107,11 @@ public class TestGitHubAPI : PlaywrightTest } } } - Assert.IsNotNull(issue); - Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString()); + Assert.NotNull(issue); + Assert.Equal("Bug description", issue?.GetProperty("body").GetString()); } - [TestMethod] + [Fact] public async Task ShouldCreateFeatureRequests() { var data = new Dictionary @@ -148,36 +1137,36 @@ public class TestGitHubAPI : PlaywrightTest } } } - Assert.IsNotNull(issue); - Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString()); + Assert.NotNull(issue); + Assert.Equal("Feature description", issue?.GetProperty("body").GetString()); } - // ... -} -``` - -### Setup and teardown - -These tests assume that repository exists. You probably want to create a new one before running tests and delete it afterwards. Use `[SetUp]` and `[TearDown]` hooks for that. - -```csharp -using System.Text.Json; -using Microsoft.Playwright; -using Microsoft.Playwright.MSTest; - -namespace PlaywrightTests; - -[TestClass] -public class TestGitHubAPI : PlaywrightTest -{ - // ... - [TestInitialize] - public async Task SetUpAPITesting() + public override async Task InitializeAsync() { + await base.InitializeAsync(); await CreateAPIRequestContext(); await CreateTestRepository(); } + private async Task CreateAPIRequestContext() + { + var headers = new Dictionary + { + // We set this header per GitHub guidelines. + { "Accept", "application/vnd.github.v3+json" }, + // Add authorization token to all requests. + // Assuming personal access token available in the environment. + { "Authorization", "token " + API_TOKEN } + }; + + Request = await Playwright.APIRequest.NewContextAsync(new() + { + // All requests we send go to this API endpoint. + BaseURL = "https://api.github.com", + ExtraHTTPHeaders = headers, + }); + } + private async Task CreateTestRepository() { var resp = await Request.PostAsync("/user/repos", new() @@ -190,11 +1179,11 @@ public class TestGitHubAPI : PlaywrightTest await Expect(resp).ToBeOKAsync(); } - [TestCleanup] - public async Task TearDownAPITesting() + public override async Task DisposeAsync() { await DeleteTestRepository(); await Request.DisposeAsync(); + await base.DisposeAsync(); } private async Task DeleteTestRepository() @@ -205,18 +1194,16 @@ public class TestGitHubAPI : PlaywrightTest } ``` -### Complete test example - -Here is the complete example of an API test: + + ```csharp using System.Text.Json; using Microsoft.Playwright; -using Microsoft.Playwright.MSTest; +using Microsoft.Playwright.Xunit.v3; namespace PlaywrightTests; -[TestClass] public class TestGitHubAPI : PlaywrightTest { static string REPO = "test-repo-2"; @@ -225,7 +1212,7 @@ public class TestGitHubAPI : PlaywrightTest private IAPIRequestContext Request = null!; - [TestMethod] + [Fact] public async Task ShouldCreateBugReport() { var data = new Dictionary @@ -250,11 +1237,11 @@ public class TestGitHubAPI : PlaywrightTest } } } - Assert.IsNotNull(issue); - Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString()); + Assert.NotNull(issue); + Assert.Equal("Bug description", issue?.GetProperty("body").GetString()); } - [TestMethod] + [Fact] public async Task ShouldCreateFeatureRequests() { var data = new Dictionary @@ -280,13 +1267,13 @@ public class TestGitHubAPI : PlaywrightTest } } } - Assert.IsNotNull(issue); - Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString()); + Assert.NotNull(issue); + Assert.Equal("Feature description", issue?.GetProperty("body").GetString()); } - [TestInitialize] - public async Task SetUpAPITesting() + public override async Task InitializeAsync() { + await base.InitializeAsync(); await CreateAPIRequestContext(); await CreateTestRepository(); } @@ -322,11 +1309,11 @@ public class TestGitHubAPI : PlaywrightTest await Expect(resp).ToBeOKAsync(); } - [TestCleanup] - public async Task TearDownAPITesting() + public override async Task DisposeAsync() { await DeleteTestRepository(); await Request.DisposeAsync(); + await base.DisposeAsync(); } private async Task DeleteTestRepository() @@ -337,13 +1324,29 @@ public class TestGitHubAPI : PlaywrightTest } ``` + + + ## Prepare server state via API calls The following test creates a new issue via API and then navigates to the list of all issues in the project to check that it appears at the top of the list. The check is performed using [LocatorAssertions]. + + + ```csharp -class TestGitHubAPI : PageTest +[TestClass] +public class TestGitHubAPI : PageTest { [TestMethod] public async Task LastCreatedIssueShouldBeFirstInTheList() @@ -365,14 +1368,110 @@ class TestGitHubAPI : PageTest } ``` + + + +```csharp +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class TestGitHubAPI : PageTest +{ + [Test] + public async Task LastCreatedIssueShouldBeFirstInTheList() + { + var data = new Dictionary + { + { "title", "[Feature] request 1" }, + { "body", "Feature description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + // When inheriting from 'PlaywrightTest' it only gives you a Playwright instance. To get a Page instance, either start + // a browser, context, and page manually or inherit from 'PageTest' which will launch it for you. + await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues"); + var firstIssue = Page.Locator("a[data-hovercard-type='issue']").First; + await Expect(firstIssue).ToHaveTextAsync("[Feature] request 1"); + } +} +``` + + + + +```csharp +public class TestGitHubAPI : PageTest +{ + [Fact] + public async Task LastCreatedIssueShouldBeFirstInTheList() + { + var data = new Dictionary + { + { "title", "[Feature] request 1" }, + { "body", "Feature description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + // When inheriting from 'PlaywrightTest' it only gives you a Playwright instance. To get a Page instance, either start + // a browser, context, and page manually or inherit from 'PageTest' which will launch it for you. + await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues"); + var firstIssue = Page.Locator("a[data-hovercard-type='issue']").First; + await Expect(firstIssue).ToHaveTextAsync("[Feature] request 1"); + } +} +``` + + + + +```csharp +public class TestGitHubAPI : PageTest +{ + [Fact] + public async Task LastCreatedIssueShouldBeFirstInTheList() + { + var data = new Dictionary + { + { "title", "[Feature] request 1" }, + { "body", "Feature description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + // When inheriting from 'PlaywrightTest' it only gives you a Playwright instance. To get a Page instance, either start + // a browser, context, and page manually or inherit from 'PageTest' which will launch it for you. + await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues"); + var firstIssue = Page.Locator("a[data-hovercard-type='issue']").First; + await Expect(firstIssue).ToHaveTextAsync("[Feature] request 1"); + } +} +``` + + + + ## Check the server state after running user actions The following test creates a new issue via user interface in the browser and then checks via API if it was created: + + + ```csharp // Make sure to extend from PageTest if you want to use the Page class. -class GitHubTests : PageTest +[TestClass] +public class GitHubTests : PageTest { [TestMethod] public async Task LastCreatedIssueShouldBeOnTheServer() @@ -391,6 +1490,83 @@ class GitHubTests : PageTest } ``` + + + +```csharp +// Make sure to extend from PageTest if you want to use the Page class. +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class GitHubTests : PageTest +{ + [Test] + public async Task LastCreatedIssueShouldBeOnTheServer() + { + await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues"); + await Page.Locator("text=New Issue").ClickAsync(); + await Page.Locator("[aria-label='Title']").FillAsync("Bug report 1"); + await Page.Locator("[aria-label='Comment body']").FillAsync("Bug description"); + await Page.Locator("text=Submit new issue").ClickAsync(); + var issueId = Page.Url.Substring(Page.Url.LastIndexOf('/')); + + var newIssue = await Request.GetAsync("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId); + await Expect(newIssue).ToBeOKAsync(); + Assert.That(await newIssue.TextAsync(), Does.Contain("Bug report 1")); + } +} +``` + + + + +```csharp +// Make sure to extend from PageTest if you want to use the Page class. +public class GitHubTests : PageTest +{ + [Fact] + public async Task LastCreatedIssueShouldBeOnTheServer() + { + await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues"); + await Page.Locator("text=New Issue").ClickAsync(); + await Page.Locator("[aria-label='Title']").FillAsync("Bug report 1"); + await Page.Locator("[aria-label='Comment body']").FillAsync("Bug description"); + await Page.Locator("text=Submit new issue").ClickAsync(); + var issueId = Page.Url.Substring(Page.Url.LastIndexOf('/')); + + var newIssue = await Request.GetAsync("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId); + await Expect(newIssue).ToBeOKAsync(); + Assert.Contains("Bug report 1", await newIssue.TextAsync()); + } +} +``` + + + + +```csharp +// Make sure to extend from PageTest if you want to use the Page class. +public class GitHubTests : PageTest +{ + [Fact] + public async Task LastCreatedIssueShouldBeOnTheServer() + { + await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues"); + await Page.Locator("text=New Issue").ClickAsync(); + await Page.Locator("[aria-label='Title']").FillAsync("Bug report 1"); + await Page.Locator("[aria-label='Comment body']").FillAsync("Bug description"); + await Page.Locator("text=Submit new issue").ClickAsync(); + var issueId = Page.Url.Substring(Page.Url.LastIndexOf('/')); + + var newIssue = await Request.GetAsync("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId); + await Expect(newIssue).ToBeOKAsync(); + Assert.Contains("Bug report 1", await newIssue.TextAsync()); + } +} +``` + + + + ## Reuse authentication state Web apps use cookie-based or token-based authentication, where authenticated diff --git a/docs/src/api-testing-java.md b/docs/src/api-testing-java.md index e8020e12ce4f3..551ea9b42cde7 100644 --- a/docs/src/api-testing-java.md +++ b/docs/src/api-testing-java.md @@ -393,9 +393,12 @@ public class TestGitHubAPI { RequestOptions.create().setData(data)); assertTrue(newIssue.ok()); - page.navigate("https://github.com/" + USER + "/" + REPO + "/issues"); - Locator firstIssue = page.locator("a[data-hovercard-type='issue']").first(); - assertThat(firstIssue).hasText("[Feature] request 1"); + try (Browser browser = playwright.chromium().launch()) { + Page page = browser.newPage(); + page.navigate("https://github.com/" + USER + "/" + REPO + "/issues"); + Locator firstIssue = page.locator("a[data-hovercard-type='issue']").first(); + assertThat(firstIssue).hasText("[Feature] request 1"); + } } } ``` @@ -409,16 +412,19 @@ it was created: public class TestGitHubAPI { @Test void lastCreatedIssueShouldBeOnTheServer() { - page.navigate("https://github.com/" + USER + "/" + REPO + "/issues"); - page.locator("text=New Issue").click(); - page.locator("[aria-label='Title']").fill("Bug report 1"); - page.locator("[aria-label='Comment body']").fill("Bug description"); - page.locator("text=Submit new issue").click(); - String issueId = page.url().substring(page.url().lastIndexOf('/')); - - APIResponse newIssue = request.get("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId); - assertThat(newIssue).isOK(); - assertTrue(newIssue.text().contains("Bug report 1")); + try (Browser browser = playwright.chromium().launch()) { + Page page = browser.newPage(); + page.navigate("https://github.com/" + USER + "/" + REPO + "/issues"); + page.locator("text=New Issue").click(); + page.locator("[aria-label='Title']").fill("Bug report 1"); + page.locator("[aria-label='Comment body']").fill("Bug description"); + page.locator("text=Submit new issue").click(); + String issueId = page.url().substring(page.url().lastIndexOf('/')); + + APIResponse newIssue = request.get("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId); + assertThat(newIssue).isOK(); + assertTrue(newIssue.text().contains("Bug report 1")); + } } } ``` diff --git a/docs/src/api/class-apirequestcontext.md b/docs/src/api/class-apirequestcontext.md index 28399a0b031b8..0adb320d9c75f 100644 --- a/docs/src/api/class-apirequestcontext.md +++ b/docs/src/api/class-apirequestcontext.md @@ -915,6 +915,14 @@ Returns storage state for this request context, contains current cookies and loc Set to `true` to include IndexedDB in the storage state snapshot. +### option: APIRequestContext.storageState.opfs +* since: v1.63 +- `opfs` ? + +Set to `true` to include the origin private file system in the storage state snapshot. + ## property: APIRequestContext.tracing * since: v1.60 - type: <[Tracing]> + +Tracing recorder for requests made through this API request context. diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 37ebb8f05f0db..993a0d0e9325d 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -198,6 +198,12 @@ Context.Dialog += async (_, dialog) => When no [`event: Page.dialog`] or [`event: BrowserContext.dialog`] listeners are present, all dialogs are automatically dismissed. ::: +## event: BrowserContext.dialogClosed +* since: v1.63 +- argument: <[Dialog]> + +Emitted when a JavaScript dialog in any page belonging to this context has been closed, either by [`method: Dialog.accept`], by [`method: Dialog.dismiss`], or manually by the user in the headed browser. + ## event: BrowserContext.download * since: v1.60 - argument: <[Download]> @@ -1310,12 +1316,6 @@ When set to `minimal`, only record information necessary for routing from HAR. T Optional setting to control resource content management. If `attach` is specified, resources are persisted as separate files or entries in the ZIP archive. If `embed` is specified, content is stored inline the HAR file. -### option: BrowserContext.routeFromHAR.interceptAPIRequests -* since: v1.62 -- `interceptAPIRequests` <[boolean]> - -If set to `true`, requests made via [APIRequestContext] (such as [`property: BrowserContext.request`] or [`property: Page.request`]) are also served from the HAR file. By default these requests are sent to the network, matching the behavior prior to v1.62. Defaults to `false` for backward compatibility. - ## async method: BrowserContext.routeWebSocket * since: v1.48 @@ -1522,13 +1522,21 @@ its geolocation. ## async method: BrowserContext.setHTTPCredentials * since: v1.8 * langs: js -* deprecated: Browsers may cache credentials after successful authentication. Create a new browser context instead. + +Sets the credentials for HTTP authentication for this browser context. + +:::note +Browsers may cache credentials per origin after a successful authentication, so changing credentials for an origin that has already been authenticated may have no effect. +::: ### param: BrowserContext.setHTTPCredentials.httpCredentials * since: v1.8 -- `httpCredentials` <[null]|[Object]> +- `httpCredentials` <[null]|[Object]|[Array]<[Object]>> - `username` <[string]> - `password` <[string]> + - `origin` ?<[string]> Restrain sending http credentials on specific origin (scheme://host:port). + +Pass an array to use different credentials for different origins. The first entry that matches the request origin is used, and entries with no origin match any request. ## async method: BrowserContext.setOffline * since: v1.8 @@ -1558,7 +1566,7 @@ Whether to emulate network being offline for the browser context. - `name` <[string]> - `value` <[string]> -Returns storage state for this browser context, contains current cookies, local storage snapshot, IndexedDB snapshot and virtual WebAuthn credentials. +Returns storage state for this browser context, contains current cookies, local storage snapshot, IndexedDB snapshot, origin private file system snapshot and virtual WebAuthn credentials. ## async method: BrowserContext.storageState * since: v1.8 @@ -1575,6 +1583,17 @@ Returns storage state for this browser context, contains current cookies, local Set to `true` to include [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) in the storage state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase Authentication, enable this. +### option: BrowserContext.storageState.opfs +* since: v1.63 +- `opfs` ? + +Set to `true` to include the [origin private file system](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system) +in the storage state snapshot. + +:::note +OPFS is currently not supported in ephemeral WebKit contexts. +::: + ### option: BrowserContext.storageState.credentials * since: v1.61 - `credentials` ? @@ -1587,7 +1606,7 @@ Note that restoring the storage state that contains credentials will automatical ## async method: BrowserContext.setStorageState * since: v1.59 -Clears the existing cookies, local storage, IndexedDB entries and virtual WebAuthn credentials, and sets the new storage +Clears the existing cookies, local storage, IndexedDB entries, origin private file system entries and virtual WebAuthn credentials, and sets the new storage state. When the storage state contains credentials, the virtual WebAuthn authenticator is installed (equivalent to [`method: Credentials.install`]), preventing all real authenticators from working in this context. diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md index 6a1242084837b..2d846912c6d2e 100644 --- a/docs/src/api/class-frame.md +++ b/docs/src/api/class-frame.md @@ -970,6 +970,10 @@ Console.WriteLine(frame == contentFrame); // -> True When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements in that iframe. +When called without [`param: selector`], the search starts in this frame or in any of the iframes inside it, +so that you don't need to locate each iframe first. Note that the rest of the locator is resolved inside a single +frame, just like any other locator. If it matches elements inside multiple frames, an error is thrown. + **Usage** Following snippet locates element with text "Submit" in the iframe with id `my-frame`, like `"> + + `, server.EMPTY_PAGE, 5); + + // The bare selector is ambiguous across frames, so a bare frameLocator() cannot pinpoint + // the target frame and the full frame chain is used instead. + const frame = page.mainFrame().childFrames()[0].childFrames()[0]; + const [sources] = await Promise.all([ + recorder.waitForOutput('JavaScript', 'click'), + frame.click('text=Hello'), + ]); + + expect.soft(sources.get('JavaScript')!.text).toContain(` + await page.locator('#frame1').contentFrame().locator('iframe').contentFrame().getByRole('button', { name: 'Hello' }).click();`); + + const clickAction = sources.get('JSON')!.actions.map(l => JSON.parse(l)).find(a => a.name === 'click'); + expect.soft(clickAction.selector).toBe('#frame1 >> internal:control=enter-frame >> iframe >> internal:control=enter-frame >> internal:role=button[name="Hello"i]'); + }); + + test('should not use frameLocator() when it saves a single frameLocator(selector)', async ({ openRecorder, server }) => { + const { page, recorder } = await openRecorder(); + await recorder.setContentAndWait(` + + `, server.EMPTY_PAGE, 2); + + // "Hello" is unique across frames, but a bare frameLocator() would only replace a single + // frameLocator(selector) call, so the plain frame chain is not any longer. + const frame = page.mainFrame().childFrames()[0]; + const [sources] = await Promise.all([ + recorder.waitForOutput('JavaScript', 'click'), + frame.click('text=Hello'), + ]); + + expect.soft(sources.get('JavaScript')!.text).toContain(` + await page.locator('#frame1').contentFrame().getByRole('button', { name: 'Hello' }).click();`); + }); +}); + async function createFrameHierarchy(page: Page, recorder: Recorder, server: TestServer) { /* iframe diff --git a/tests/library/inspector/cli-codegen-javascript.spec.ts b/tests/library/inspector/cli-codegen-javascript.spec.ts index 31eddfa5f5a1f..29d7114a61ccb 100644 --- a/tests/library/inspector/cli-codegen-javascript.spec.ts +++ b/tests/library/inspector/cli-codegen-javascript.spec.ts @@ -48,6 +48,26 @@ test('should print the correct context options for custom settings', async ({ br }); +test('should work with --http-credentials', async ({ browserName, channel, runCLI, server }) => { + server.setAuth('/empty.html', 'user', 'pass'); + const cli = runCLI(['--http-credentials=user:pass', '--target=javascript', server.EMPTY_PAGE]); + const expectedResult = `const { ${browserName} } = require('playwright'); + +(async () => { + const browser = await ${browserName}.launch({ + ${launchOptions(channel)} + }); + const context = await browser.newContext({ + httpCredentials: { + password: 'pass', + username: 'user' + } + }); + const page = await context.newPage(); + await page.goto('${server.EMPTY_PAGE}');`; + await cli.waitFor(expectedResult); +}); + test('should print the correct context options when using a device', async ({ browserName, channel, runCLI, server }) => { test.skip(browserName !== 'chromium'); diff --git a/tests/library/inspector/console-api.spec.ts b/tests/library/inspector/console-api.spec.ts index ebc9cef72ac95..2951f7bcb7cbf 100644 --- a/tests/library/inspector/console-api.spec.ts +++ b/tests/library/inspector/console-api.spec.ts @@ -98,6 +98,7 @@ it('should support playwright.getBy*', async ({ page }) => { expect(await page.evaluate(`playwright.locator('span').last().element.innerHTML`)).toContain('World'); expect(await page.evaluate(`playwright.locator('span').nth(1).element.innerHTML`)).toContain('World'); expect(await page.evaluate(`playwright.locator('div').filter({ visible: false }).element.innerHTML`)).toContain('two'); + expect(await page.evaluate(`playwright.locator('div').visible().element.innerHTML`)).toContain('one'); }); it('expected properties on playwright object', async ({ page }) => { diff --git a/tests/library/inspector/pause.spec.ts b/tests/library/inspector/pause.spec.ts index 74c5dfd61e43a..3e79cdc400a6f 100644 --- a/tests/library/inspector/pause.spec.ts +++ b/tests/library/inspector/pause.spec.ts @@ -276,7 +276,7 @@ it.describe('pause', () => { await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause({ __testHookKeepTestTimeout: true }); // 2")'); expect(await sanitizeLog(recorderPage)).toEqual([ 'Pause- XXms', - 'Click(page.locator(\'button\'))- XXms', + 'Click locator(\'button\')- XXms', 'Pause', ]); await recorderPage.click('[title="Resume (F8)"]'); @@ -324,8 +324,8 @@ it.describe('pause', () => { await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause({ __testHookKeepTestTimeout: true }); // 2")'); expect(await sanitizeLog(recorderPage)).toEqual([ 'Pause- XXms', - 'Expect "toHaveText"(page.locator(\'button\'))- XXms', - 'Expect "not toHaveText"(page.locator(\'button\'))- XXms', + 'Expect "toHaveText" locator(\'button\')- XXms', + 'Expect "not toHaveText" locator(\'button\')- XXms', 'Pause', ]); await recorderPage.click('[title="Resume (F8)"]'); @@ -368,7 +368,7 @@ it.describe('pause', () => { expect(await sanitizeLog(recorderPage)).toEqual([ 'Pause- XXms', 'Wait for event "console"- XXms', - 'Click(page.getByRole(\'button\', { name: \'Submit\' }))- XXms', + 'Click getByRole(\'button\', { name: \'Submit\' })- XXms', 'Pause', ]); await recorderPage.click('[title="Resume (F8)"]'); @@ -387,7 +387,7 @@ it.describe('pause', () => { await recorderPage.waitForSelector('.source-line-error-underline'); expect(await sanitizeLog(recorderPage)).toEqual([ 'Pause- XXms', - 'Is checked(page.getByRole(\'button\'))- XXms', + 'Is checked getByRole(\'button\')- XXms', 'waiting for getByRole(\'button\')', 'error: Error: Not a checkbox or radio button', ]); @@ -407,8 +407,8 @@ it.describe('pause', () => { await recorderPage.waitForSelector('.source-line-error-underline'); expect(await sanitizeLog(recorderPage)).toEqual([ 'Pause- XXms', - 'Expect "toHaveText"(page.getByRole(\'button\'))- XXms', - 'Expect "toHaveText" with timeout 1ms', + 'Expect "toHaveText" getByRole(\'button\')- XXms', + 'Expect "toHaveText" getByRole(\'button\') with timeout 1ms', 'waiting for getByRole(\'button\')', 'error: Expect failed', ]); @@ -567,7 +567,10 @@ it.describe('pause', () => { const box1Promise = waitForTestLog(page, 'Highlight box for test: '); await recorderPage.click('[title="Step over (F10)"]'); - const box2 = roundBox((await page.locator('#target').boundingBox())!); + // Use an internal call to avoid pausing on it instead of the stepped-over click. + const box2 = await (page as any)._wrapApiCall(async () => { + return roundBox((await page.locator('#target').boundingBox())!); + }, { internal: true }); const box1 = roundBox(await box1Promise); expect(box1).toEqual(box2); diff --git a/tests/library/inspector/recorder-api.spec.ts b/tests/library/inspector/recorder-api.spec.ts index 568c09df57663..17d6eeb956718 100644 --- a/tests/library/inspector/recorder-api.spec.ts +++ b/tests/library/inspector/recorder-api.spec.ts @@ -18,16 +18,22 @@ import { test, expect } from './inspectorTest'; import type { Page } from '@playwright/test'; import type * as actions from '@isomorphic/codegen/actions'; +import type { BrowserContextInternalApi } from '../../../packages/playwright-core/src/tools/backend/browserContextEx'; class RecorderLog { - actions: (actions.ActionInContext & { code: string })[] = []; + actions: { action: actions.Action, code: string }[] = []; + signals: { signal: actions.Signal, code: string }[] = []; - actionAdded(page: Page, actionInContext: actions.ActionInContext, code: string): void { - this.actions.push({ ...actionInContext, code }); + actionAdded(page: Page, action: actions.Action, code: string): void { + this.actions.push({ action, code }); } - actionUpdated(page: Page, actionInContext: actions.ActionInContext, code: string): void { - this.actions[this.actions.length - 1] = { ...actionInContext, code }; + actionUpdated(page: Page, action: actions.Action, code: string): void { + this.actions[this.actions.length - 1] = { action, code }; + } + + signalAdded(page: Page, signal: actions.Signal, code: string): void { + this.signals.push({ signal, code }); } } @@ -39,6 +45,7 @@ async function startRecording(context) { }, log); return { action: (name: string) => log.actions.filter(a => a.action.name === name), + signals: () => log.signals, }; } @@ -46,14 +53,23 @@ function normalizeCode(code: string): string { return code.replace(/\s+/g, ' ').trim(); } +test('context should implement the internal api used by the tools', async ({ context }) => { + // Listing a method here is enforced by the type, so adding one to the interface breaks compilation until it is covered. + const methods: Record = { + _enableRecorder: true, + _disableRecorder: true, + }; + for (const method of Object.keys(methods)) + expect(typeof context[method], method).toBe('function'); +}); + test('should click', async ({ context, browserName, platform, channel }) => { const log = await startRecording(context); const page = await context.newPage(); await page.setContent(``); await page.getByRole('button', { name: 'Submit' }).click(); - const clickActions = log.action('click'); - expect(clickActions).toEqual([ + await expect.poll(() => log.action('click')).toEqual([ expect.objectContaining({ action: expect.objectContaining({ name: 'click', @@ -62,11 +78,10 @@ test('should click', async ({ context, browserName, platform, channel }) => { // Safari does not focus after a click: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#clicking_and_focus ariaSnapshot: (browserName === 'webkit' && (platform === 'darwin' || (platform === 'win32' && channel !== 'webkit-wsl'))) ? '- button "Submit" [ref=e2]' : '- button "Submit" [active] [ref=e2]', }), - startTime: expect.any(Number), }) ]); - expect(normalizeCode(clickActions[0].code)).toEqual(`await page.getByRole('button', { name: 'Submit' }).click();`); + expect(normalizeCode(log.action('click')[0].code)).toEqual(`await page.getByRole('button', { name: 'Submit' }).click();`); }); test('should double click', async ({ context, browserName, platform, channel }) => { @@ -75,8 +90,7 @@ test('should double click', async ({ context, browserName, platform, channel }) await page.setContent(``); await page.getByRole('button', { name: 'Submit' }).dblclick(); - const clickActions = log.action('click'); - expect(clickActions).toEqual([ + await expect.poll(() => log.action('click')).toEqual([ expect.objectContaining({ action: expect.objectContaining({ name: 'click', @@ -86,11 +100,10 @@ test('should double click', async ({ context, browserName, platform, channel }) // Safari does not focus after a click: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#clicking_and_focus ariaSnapshot: (browserName === 'webkit' && (platform === 'darwin' || (platform === 'win32' && channel !== 'webkit-wsl'))) ? '- button "Submit" [ref=e2]' : '- button "Submit" [active] [ref=e2]', }), - startTime: expect.any(Number), }) ]); - expect(normalizeCode(clickActions[0].code)).toEqual(`await page.getByRole('button', { name: 'Submit' }).dblclick();`); + expect(normalizeCode(log.action('click')[0].code)).toEqual(`await page.getByRole('button', { name: 'Submit' }).dblclick();`); }); test('should right click', async ({ context, browserName, platform, channel }) => { @@ -110,13 +123,42 @@ test('should right click', async ({ context, browserName, platform, channel }) = // Safari does not focus after a click: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button#clicking_and_focus ariaSnapshot: (browserName === 'webkit' && (platform === 'darwin' || (platform === 'win32' && channel !== 'webkit-wsl'))) ? '- button "Submit" [ref=e2]' : '- button "Submit" [active] [ref=e2]', }), - startTime: expect.any(Number), }) ]); expect(normalizeCode(clickActions[0].code)).toEqual(`await page.getByRole('button', { name: 'Submit' }).click({ button: 'right' });`); }); +test('should send updated code with the signal', async ({ context, server }) => { + const recorder = await startRecording(context); + const page = await context.newPage(); + await page.setContent(`link`); + await page.getByRole('link', { name: 'link' }).click(); + + // The popup signal attaches to the click, so the click's code is re-generated to await it. + await expect.poll(() => recorder.signals().map(s => s.signal.name)).toContain('popup'); + const code = recorder.signals().find(s => s.signal.name === 'popup')!.code; + expect(normalizeCode(code)).toContain(`const page1Promise = page.waitForEvent('popup');`); + expect(normalizeCode(code)).toContain(`await page.getByRole('link', { name: 'link' }).click();`); + expect(normalizeCode(code)).toContain(`const page1 = await page1Promise;`); +}); + +test('should not amend the last action with a signal from another page', async ({ context }) => { + const recorder = await startRecording(context); + const page1 = await context.newPage(); + await page1.setContent(``); + const page2 = await context.newPage(); + await page2.setContent(`
Second page
`); + + await page1.getByRole('button', { name: 'Submit' }).click(); + await expect.poll(() => recorder.action('click').length).toBe(1); + + // Dialog on page2 must not attach to the click on page1. + void page2.evaluate(() => alert('hello')).catch(() => {}); + await expect.poll(() => recorder.signals().map(s => s.signal.name)).toContain('dialog'); + expect(recorder.signals().find(s => s.signal.name === 'dialog')!.code).toBe(''); +}); + test('should type', async ({ context }) => { const log = await startRecording(context); const page = await context.newPage(); @@ -124,8 +166,7 @@ test('should type', async ({ context }) => { await page.getByRole('textbox').pressSequentially('Hello'); - const fillActions = log.action('fill'); - expect(fillActions).toEqual([ + await expect.poll(() => log.action('fill')).toEqual([ expect.objectContaining({ action: expect.objectContaining({ name: 'fill', @@ -133,11 +174,10 @@ test('should type', async ({ context }) => { ref: 'e2', ariaSnapshot: '- textbox [active] [ref=e2]: Hello', }), - startTime: expect.any(Number), }) ]); - expect(normalizeCode(fillActions[0].code)).toEqual(`await page.getByRole('textbox').fill('Hello');`); + expect(normalizeCode(log.action('fill')[0].code)).toEqual(`await page.getByRole('textbox').fill('Hello');`); }); test('should disable recorder', async ({ context }) => { @@ -146,12 +186,43 @@ test('should disable recorder', async ({ context }) => { await page.setContent(``); await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click(); - expect(log.action('click')).toHaveLength(2); + await expect.poll(() => log.action('click').length).toBe(2); await (context as any)._disableRecorder(); await page.getByRole('button', { name: 'Submit' }).click(); + // Give it some time to produce more actions - there should be none. + await page.waitForTimeout(2000); expect(log.action('click')).toHaveLength(2); }); +test('should record again after disable', async ({ context }) => { + const log = await startRecording(context); + const page = await context.newPage(); + await page.setContent(``); + await page.getByRole('button', { name: 'Submit' }).click(); + await expect.poll(() => log.action('click').length).toBe(1); + await (context as any)._disableRecorder(); + + const log2 = await startRecording(context); + await page.getByRole('button', { name: 'Submit' }).click(); + await expect.poll(() => log2.action('click').length).toBe(1); + // Give it some time to produce duplicate actions - there should be none. + await page.waitForTimeout(1000); + expect(log2.action('click')).toHaveLength(1); +}); + +test('disable should close the inspector window', async ({ context, openRecorder }) => { + const { recorder } = await openRecorder(); + await (context as any)._disableRecorder(); + await expect.poll(() => recorder.recorderPage.isClosed()).toBe(true); + + // With the window closed, programmatic recording can start on the same context. + const log = await startRecording(context); + const page = await context.newPage(); + await page.setContent(``); + await page.getByRole('button', { name: 'Submit' }).click(); + await expect.poll(() => log.action('click').length).toBe(1); +}); + test('page.pickLocator should return locator for picked element', async ({ page }) => { await page.setContent(``); diff --git a/tests/library/launcher.spec.ts b/tests/library/launcher.spec.ts index ba29451d5c4f0..1bbe08616625a 100644 --- a/tests/library/launcher.spec.ts +++ b/tests/library/launcher.spec.ts @@ -45,7 +45,6 @@ it('should kill browser process on timeout after close', async ({ browserType, m it('should throw a friendly error if its headed and there is no xserver on linux running', async ({ mode, browserType, platform, channel }) => { it.skip(platform !== 'linux'); it.skip(channel === 'chromium-headless-shell', 'shell is never headed'); - it.skip(channel === 'chromium-tip-of-tree-headless-shell', 'shell is never headed'); const error: Error = await browserType.launch({ headless: false, diff --git a/tests/library/locator-generator.spec.ts b/tests/library/locator-generator.spec.ts index fa9a863592e9c..5f3c7e541dcb2 100644 --- a/tests/library/locator-generator.spec.ts +++ b/tests/library/locator-generator.spec.ts @@ -357,11 +357,11 @@ it('reverse engineer hasNotText', async ({ page }) => { }); it('reverse engineer visible', async ({ page }) => { - expect.soft(generate(page.getByText('Hello').filter({ visible: true }).locator('div'))).toEqual({ - csharp: `GetByText("Hello").Filter(new() { Visible = true }).Locator("div")`, - java: `getByText("Hello").filter(new Locator.FilterOptions().setVisible(true)).locator("div")`, - javascript: `getByText('Hello').filter({ visible: true }).locator('div')`, - python: `get_by_text("Hello").filter(visible=True).locator("div")`, + expect.soft(generate(page.getByText('Hello').visible().locator('div'))).toEqual({ + csharp: `GetByText("Hello").Visible.Locator("div")`, + java: `getByText("Hello").visible().locator("div")`, + javascript: `getByText('Hello').visible().locator('div')`, + python: `get_by_text("Hello").visible.locator("div")`, }); expect.soft(generate(page.getByText('Hello').filter({ visible: false }).locator('div'))).toEqual({ csharp: `GetByText("Hello").Filter(new() { Visible = false }).Locator("div")`, @@ -369,6 +369,11 @@ it('reverse engineer visible', async ({ page }) => { javascript: `getByText('Hello').filter({ visible: false }).locator('div')`, python: `get_by_text("Hello").filter(visible=False).locator("div")`, }); + const selector = (page.getByText('Hello').visible() as any)._selector; + expect.soft(parseLocator('javascript', `getByText('Hello').filter({ visible: true })`, 'data-testid')).toBe(selector); + expect.soft(parseLocator('java', `getByText("Hello").filter(new Locator.FilterOptions().setVisible(true))`, 'data-testid')).toBe(selector); + expect.soft(parseLocator('python', `get_by_text("Hello").filter(visible=True)`, 'data-testid')).toBe(selector); + expect.soft(parseLocator('csharp', `GetByText("Hello").Filter(new() { Visible = true })`, 'data-testid')).toBe(selector); }); it('reverse engineer has', async ({ page }) => { @@ -442,6 +447,20 @@ it('reverse engineer frameLocator', async ({ page }) => { expect.soft(asLocator('javascript', selector)).toBe(`locator('div').locator('iframe').contentFrame().locator('span')`); }); +it('reverse engineer frameLocator without a selector', async ({ page }) => { + expect.soft(generate(page.frameLocator().getByText('foo').locator('span'))).toEqual({ + csharp: `FrameLocator().GetByText("foo").Locator("span")`, + java: `frameLocator().getByText("foo").locator("span")`, + javascript: `frameLocator().getByText('foo').locator('span')`, + python: `frame_locator().get_by_text("foo").locator("span")`, + }); + + expect.soft(asLocator('javascript', 'internal:control=any-frame')).toBe(`frameLocator()`); + expect.soft(asLocator('python', 'internal:control=any-frame')).toBe(`frame_locator()`); + expect.soft(asLocator('java', 'internal:control=any-frame')).toBe(`frameLocator()`); + expect.soft(asLocator('csharp', 'internal:control=any-frame')).toBe(`FrameLocator()`); +}); + it('generate multiple locators', async ({ page }) => { const selector = (page.locator('div', { hasText: 'foo' }).nth(0).filter({ has: page.locator('span', { hasNotText: 'bar' }).nth(-1) }) as any)._selector; const locators = { diff --git a/tests/library/locator-highlight.spec.ts b/tests/library/locator-highlight.spec.ts index 6e4fd5bf7ab0f..7ed5ca94e07b6 100644 --- a/tests/library/locator-highlight.spec.ts +++ b/tests/library/locator-highlight.spec.ts @@ -86,6 +86,24 @@ test('hideHighlight removes a styled highlight', async ({ browser, server }) => await context.close(); }); +test('highlight should survive navigation', async ({ browser, server }) => { + const context = await browser.newContext(); + const page = await context.newPage(); + await page.setContent(``); + + await page.getByRole('button').highlight(); + await expect(page.locator('x-pw-highlight')).toHaveCount(1); + + // Highlights are resolved again after the navigation. + await page.goto(server.PREFIX + '/input/button.html'); + await expect(page.locator('x-pw-highlight')).toHaveCount(1); + + await page.hideHighlight(); + await expect(page.locator('x-pw-highlight')).toHaveCount(0); + + await context.close(); +}); + test('Page.hideHighlight clears all locator highlights', async ({ browser, server }) => { const context = await browser.newContext(); const page = await context.newPage(); diff --git a/tests/library/page-event-crash.spec.ts b/tests/library/page-event-crash.spec.ts index 5560049b60d8f..fc1a3bf2a6e63 100644 --- a/tests/library/page-event-crash.spec.ts +++ b/tests/library/page-event-crash.spec.ts @@ -37,6 +37,7 @@ test.beforeEach(({ platform, browserName, channel }) => { test.slow(platform === 'linux' && (browserName === 'webkit'), 'WebKit/Linux tests are consistently slower on some Linux environments. Most likely WebContent process is not getting terminated properly and is causing the slowdown.'); test.skip(channel === 'webkit-wsl', 'WebKit on WSL is even slower than above ^^ - skipping for now'); test.skip(browserName === 'chromium' && utils.hostPlatform.startsWith('ubuntu24.04'), 'never dispatches the crash event'); + test.skip(browserName === 'webkit' && utils.hostPlatform.startsWith('debian13'), 'never dispatches the crash event'); }); test('should emit crash event when page crashes', async ({ page, crash }) => { diff --git a/tests/library/permissions.spec.ts b/tests/library/permissions.spec.ts index d66340f46e85d..64a414296aa7a 100644 --- a/tests/library/permissions.spec.ts +++ b/tests/library/permissions.spec.ts @@ -192,11 +192,9 @@ it.describe('permissions', () => { }); }); -it('should support clipboard read', async ({ page, context, server, browserName, isWindows, isLinux, headless, isHeadlessShell }) => { +it('should support clipboard read', async ({ page, context, server, browserName, isWindows, isHeadlessShell }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/27475' }); it.fail(browserName === 'firefox', 'No such permissions (requires flag) in Firefox'); - it.fixme(browserName === 'webkit' && isWindows, 'WebPasteboardProxy::allPasteboardItemInfo not implemented for Windows.'); - it.fixme(browserName === 'webkit' && isLinux && headless, 'WebPasteboardProxy::allPasteboardItemInfo not implemented for WPE.'); await page.goto(server.EMPTY_PAGE); // There is no 'clipboard-read' permission in WebKit Web API. @@ -219,6 +217,37 @@ it('should support clipboard read', async ({ page, context, server, browserName, expect(await page.evaluate(() => navigator.clipboard.readText())).toBe('test content'); }); +it('should isolate the headless clipboard from the operating system', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/13097' }, +}, async ({ browserType, server, browserName, isFrozenWebkit }) => { + it.skip(isFrozenWebkit, 'needs recent webkit'); + + // Each headless browser gets its own clipboard, so neither can see the clipboard of the operating system nor of another browser. + const browser1 = await browserType.launch({ headless: true }); + const browser2 = await browserType.launch({ headless: true }); + const [page1, page2] = await Promise.all([browser1, browser2].map(async browser => { + const context = await browser.newContext(); + // There is no 'clipboard-write' permission in WebKit Web API and no clipboard permission at all in Firefox. + if (browserName === 'chromium') + await context.grantPermissions(['clipboard-read', 'clipboard-write']); + else if (browserName === 'webkit') + await context.grantPermissions(['clipboard-read']); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + return page; + })); + + await page1.evaluate(() => navigator.clipboard.writeText('first')); + expect(await page1.evaluate(() => navigator.clipboard.readText())).toBe('first'); + + await page2.evaluate(() => navigator.clipboard.writeText('second')); + expect(await page2.evaluate(() => navigator.clipboard.readText())).toBe('second'); + + expect(await page1.evaluate(() => navigator.clipboard.readText())).toBe('first'); + + await Promise.all([browser1.close(), browser2.close()]); +}); + it('storage access', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31227' } }, async ({ page, context, server, browserName }) => { diff --git a/tests/library/role-utils.spec.ts b/tests/library/role-utils.spec.ts index 81be2a9f163c7..a20e9329bc6fe 100644 --- a/tests/library/role-utils.spec.ts +++ b/tests/library/role-utils.spec.ts @@ -85,7 +85,7 @@ for (let range = 0; range <= ranges.length; range++) { if (!element) throw new Error(`Unable to resolve "${step.selector}"`); const injected = (window as any).__injectedScript; - const received = step.property === 'name' ? injected.utils.getElementAccessibleNameText(element) : injected.utils.getElementAccessibleDescription(element); + const received = step.property === 'name' ? injected.utils.getElementAccessibleNameText(element) : injected.utils.getElementAccessibleDescription(element).text; result.push({ selector: step.selector, expected: step.value, received }); } return result; @@ -471,6 +471,22 @@ test('control embedded in a target element', async ({ page }) => { expect.soft(await getNameAndRole(page, 'h1')).toEqual({ role: 'heading', name: 'Foo bar' }); }); +test('searchbox embedded control should contribute its value', async ({ page }) => { + test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42341' }); + + await page.setContent(` + + + + +

+ `); + expect.soft(await getNameAndRole(page, '#b1')).toEqual({ role: 'button', name: 'Query' }); + expect.soft(await getNameAndRole(page, '#b2')).toEqual({ role: 'button', name: 'Query' }); + expect.soft(await getNameAndRole(page, '#c1')).toEqual({ role: 'checkbox', name: 'Flash the screen 5 times.' }); + expect.soft(await getNameAndRole(page, 'h1')).toEqual({ role: 'heading', name: 'Foo bar' }); +}); + test('svg role=presentation', async ({ page, server }) => { test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/26809' }); diff --git a/tests/library/selector-generator.spec.ts b/tests/library/selector-generator.spec.ts index a8a233940c9a8..f81171f15d01d 100644 --- a/tests/library/selector-generator.spec.ts +++ b/tests/library/selector-generator.spec.ts @@ -28,8 +28,8 @@ async function generate(pageOrFrame: Page | Frame, target: string, expected?: st }, expected); } -async function generateMultiple(pageOrFrame: Page | Frame, target: string): Promise { - return pageOrFrame.$eval(target, e => (window as any).__injectedScript.generateSelector(e, { multiple: true, testIdAttributeName: 'data-testid' }).selectors); +async function generateNoText(pageOrFrame: Page | Frame, target: string): Promise { + return pageOrFrame.$eval(target, e => (window as any).__injectedScript.generateSelector(e, { noText: true, testIdAttributeName: 'data-testid' }).selector); } it.describe('selector generator', () => { @@ -670,44 +670,51 @@ it.describe('selector generator', () => { }); }); - it('should generate multiple: noText in role', async ({ page }) => { - await page.setContent(` - - `); - expect(await generateMultiple(page, 'button')).toEqual([`internal:role=button[name="Click me"i]`, `internal:role=button`]); + it('should generate noText: no text engine', async ({ page }) => { + await page.setContent(`
Some text
`); + expect(await generateNoText(page, 'div')).toBe(`div`); }); - it('should generate multiple: noText in text', async ({ page }) => { - await page.setContent(` -
Some div
- `); - expect(await generateMultiple(page, 'div')).toEqual([`internal:text="Some div"i`, `div`]); + it('should generate noText: no name from content', async ({ page }) => { + await page.setContent(``); + expect(await generateNoText(page, 'button')).toBe(`internal:role=button`); + }); + + it('should generate noText: name from aria-label', async ({ page }) => { + await page.setContent(``); + expect(await generateNoText(page, 'button')).toBe(`internal:role=button[name="Send message"i]`); + }); + + it('should generate noText: name from external aria-labelledby', async ({ page }) => { + await page.setContent(`Editor
Text
`); + expect(await generateNoText(page, 'div[role=textbox]')).toBe(`internal:role=textbox[name="Editor"i]`); }); - it('should generate multiple: noId', async ({ page }) => { + it('should generate noText: ignore aria-labelledby pointing inside the element', async ({ page }) => { + await page.setContent(`

Title

Text

`); + expect(await generateNoText(page, 'div[role=textbox]')).toBe(`internal:role=textbox`); + }); + + it('should generate noText: contenteditable heading', async ({ page }) => { + await page.setContent(`

Page title

`); + expect(await generateNoText(page, 'h1')).toBe(`internal:role=heading`); + }); + + it('should generate noText: description from external aria-describedby', async ({ page }) => { await page.setContent(` -
-
+ FirstSecond +
foo
+
bar
`); - expect(await generateMultiple(page, '#second button')).toEqual([ - `#second >> internal:role=button[name="Click me"i]`, - `#second >> internal:role=button`, - `internal:role=button[name="Click me"i] >> nth=1`, - `internal:role=button >> nth=1`, - ]); + expect(await generateNoText(page, 'div[aria-describedby=desc1]')).toBe(`internal:role=textbox[name="Editor"i][description="First"i]`); }); - it('should generate multiple: noId noText', async ({ page }) => { + it('should generate noText: ignore aria-describedby pointing inside the element', async ({ page }) => { await page.setContent(` -
Some span
-
Some span
+

First

+
Second
`); - expect(await generateMultiple(page, '#second span')).toEqual([ - `#second >> internal:text="Some span"i`, - `#second span`, - `internal:text="Some span"i >> nth=1`, - `span >> nth=1`, - ]); + expect(await generateNoText(page, 'div[aria-describedby=child]')).toBe(`internal:role=textbox[name="Editor"i] >> nth=0`); }); it('should prefer role with hasText to css with hasText', async ({ page }) => { @@ -723,10 +730,7 @@ it.describe('selector generator', () => { `); - expect(await generateMultiple(page, 'input')).toEqual([ - `internal:role=listitem >> internal:has-text=\"buy flowers\"i >> internal:label=\"Toggle Todo\"i`, - `internal:label=\"Toggle Todo\"i >> nth=0`, - ]); + expect(await generate(page, 'input')).toBe(`internal:role=listitem >> internal:has-text=\"buy flowers\"i >> internal:label=\"Toggle Todo\"i`); }); it('should not use icon fonts aria name', async ({ page }) => { diff --git a/tests/library/snapshot-renderer.spec.ts b/tests/library/snapshot-renderer.spec.ts index f512a6933b57c..9dce93b7d463a 100644 --- a/tests/library/snapshot-renderer.spec.ts +++ b/tests/library/snapshot-renderer.spec.ts @@ -18,7 +18,7 @@ import { test, expect } from '@playwright/test'; import { SnapshotRenderer } from '../../packages/isomorphic/trace/snapshotRenderer'; import { LRUCache } from '../../packages/isomorphic/lruCache'; import { stripAnsiEscapes } from '../../packages/isomorphic/stringUtils'; -import type { FrameSnapshot } from '../../packages/trace/src/snapshot'; +import type { FrameSnapshot } from '../../packages/isomorphic/trace/trace'; function makeSnapshot(overrides: Partial = {}): FrameSnapshot { return { @@ -43,11 +43,24 @@ for (const [name, overrides] of [ test(`snapshot renderer escapes attacker-controlled ${name} in script context`, () => { const renderer = new SnapshotRenderer(new LRUCache(1_000_000), [], [makeSnapshot(overrides)], [], 0); const { html } = renderer.render(); - expect(html.match(/ + `, 'text/html'); + + const front = testInfo.outputPath('front.txt'); + const back = testInfo.outputPath('back.txt'); + await fs.promises.writeFile(front, 'front'); + await fs.promises.writeFile(back, 'back'); + + await cli('open', server.PREFIX); + await cli('click', 'e2'); + const { output, snapshot } = await cli('upload', front, back); + expect(output).toContain('await fileChooser.setFiles('); + expect(snapshot).toContain('Received: front.txt, back.txt'); + + await cli('click', 'e2'); + const single = await cli('upload', back); + expect(single.snapshot).toContain('Received: back.txt'); +}); + test('eval', async ({ cli, server }) => { await cli('open', server.HELLO_WORLD); const { output } = await cli('eval', '() => document.title'); @@ -165,9 +192,12 @@ test('eval ', async ({ cli, server }) => { expect(output).toContain('"BUTTON"'); }); -test('dialog-accept', async ({ cli, server }) => { +// Firefox can deliver Page.dialogOpened after the default 5s action timeout +// (seen after the FF 153 roll, especially on Windows), so the click loses the +// modal race and the response never includes the dialog. Keep the race open longer. +test('dialog-accept', async ({ cli, server, mcpBrowser }) => { server.setContent('/', ``, 'text/html'); - await cli('open', server.PREFIX); + await cli('open', server.PREFIX, { env: { PLAYWRIGHT_MCP_TIMEOUT_ACTION: mcpBrowser === 'firefox' ? '30000' : '' } }); const { output } = await cli('click', 'e2'); expect(output).toContain('MyAlert'); expect(output).toContain('["alert" dialog with message "MyAlert"]: can be handled by dialog-accept or dialog-dismiss'); @@ -176,9 +206,9 @@ test('dialog-accept', async ({ cli, server }) => { expect(inlineSnapshot).not.toContain('MyAlert'); }); -test('dialog-dismiss', async ({ cli, server }) => { +test('dialog-dismiss', async ({ cli, server, mcpBrowser }) => { server.setContent('/', ``, 'text/html'); - await cli('open', server.PREFIX); + await cli('open', server.PREFIX, { env: { PLAYWRIGHT_MCP_TIMEOUT_ACTION: mcpBrowser === 'firefox' ? '30000' : '' } }); const { output } = await cli('click', 'e2'); expect(output).toContain('MyAlert'); await cli('dialog-dismiss'); @@ -186,9 +216,9 @@ test('dialog-dismiss', async ({ cli, server }) => { expect(inlineSnapshot).not.toContain('MyAlert'); }); -test('dialog-accept ', async ({ cli, server }) => { +test('dialog-accept ', async ({ cli, server, mcpBrowser }) => { server.setContent('/', ``, 'text/html'); - await cli('open', server.PREFIX); + await cli('open', server.PREFIX, { env: { PLAYWRIGHT_MCP_TIMEOUT_ACTION: mcpBrowser === 'firefox' ? '30000' : '' } }); await cli('click', 'e2'); await cli('dialog-accept', 'my reply'); const { inlineSnapshot } = await cli('snapshot'); @@ -368,3 +398,22 @@ test('--raw on command without output', async ({ cli, server }) => { expect(output).not.toContain('### '); expect(output).not.toContain('Page URL'); }); + +test('tool error exits with non-zero code', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42028' } }, async ({ cli, server }) => { + await cli('open', server.HELLO_WORLD); + + const { output, exitCode } = await cli('click', 'e999'); + expect(output).toContain('Ref e999 not found in the current page snapshot.'); + expect(exitCode).toBe(1); + + const { output: jsonOutput, exitCode: jsonExitCode } = await cli('--json', 'click', 'e999'); + expect(JSON.parse(jsonOutput).isError).toBe(true); + expect(jsonExitCode).toBe(1); +}); + +test('codegen escapes single quotes in user input', async ({ cli, server }) => { + server.setContent('/', ``, 'text/html'); + await cli('open', server.PREFIX); + const { output } = await cli('type', "it's working"); + expect(output).toContain(`await page.keyboard.type('it\\'s working');`); +}); diff --git a/tests/mcp/cli-devtools.spec.ts b/tests/mcp/cli-devtools.spec.ts index 4be4836af79c2..7fc023f5e35a2 100644 --- a/tests/mcp/cli-devtools.spec.ts +++ b/tests/mcp/cli-devtools.spec.ts @@ -196,6 +196,25 @@ function escapeRegExp(text: string): string { return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } +test('recording-start-stop', async ({ cli, server }) => { + server.setContent('/', `Title`, 'text/html'); + + const { snapshot } = await cli('open', server.PREFIX); + expect(snapshot).toContain(`- button "Submit" [ref=e2]`); + + const { output } = await cli('recording-start'); + expect(output).toContain('Recording started'); + + await cli('click', 'e2'); + + const { output: stopOutput } = await cli('recording-stop'); + expect(stopOutput).toContain(`Recording stopped. Recorded actions: + +\`\`\`js +await page.getByRole('button', { name: 'Submit' }).click(); +\`\`\``); +}); + test('tracing-start-stop', async ({ cli, server }, testInfo) => { await cli('open', server.HELLO_WORLD); const { output } = await cli('tracing-start'); diff --git a/tests/mcp/cli-fixtures.ts b/tests/mcp/cli-fixtures.ts index 408018e6338de..f19627745a1bf 100644 --- a/tests/mcp/cli-fixtures.ts +++ b/tests/mcp/cli-fixtures.ts @@ -54,7 +54,8 @@ export const test = baseTest.extend<{ return page; }); }, - connectToDashboard: async ({ cli, playwright }, use) => { + connectToDashboard: async ({ cli, playwright }, use, testInfo) => { + testInfo.slow(); await use(async (bindTitle: string) => { let endpoint = ''; await expect(async () => { diff --git a/tests/mcp/cli-help.spec.ts b/tests/mcp/cli-help.spec.ts index 0b3150c82f671..be25ffbe13296 100644 --- a/tests/mcp/cli-help.spec.ts +++ b/tests/mcp/cli-help.spec.ts @@ -31,6 +31,11 @@ test('prints command help', async ({ cli }) => { expect(output).toContain('playwright-cli click [button]'); }); +test('prints variadic command help', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42047' } }, async ({ cli }) => { + const { output } = await cli('upload', '--help'); + expect(output).toContain('playwright-cli upload '); +}); + test('prints agent skill path when running under a coding agent', async ({ cli }) => { const { output } = await cli('--help', { env: { CLAUDECODE: '1' } }); expect(output).toContain('Agent skill:'); diff --git a/tests/mcp/cli-json.spec.ts b/tests/mcp/cli-json.spec.ts index d702d5ecbfffc..31060c2cb3f09 100644 --- a/tests/mcp/cli-json.spec.ts +++ b/tests/mcp/cli-json.spec.ts @@ -151,13 +151,41 @@ test('tab-close closes a tab and returns remaining tabs', async ({ cli, server } ]); }); -test('snapshot returns inline snapshot yaml', async ({ cli, server }) => { +test('snapshot returns structured inline snapshot', async ({ cli, server }) => { server.setContent('/', '

Hi

', 'text/html'); await cli('open', server.PREFIX); const { output } = await cli('--json', 'snapshot'); const parsed = JSON.parse(output); - expect(typeof parsed.snapshot).toBe('string'); - expect(parsed.snapshot).toContain('heading "Hi"'); + expect(parsed.snapshot).toEqual([ + { role: 'heading', name: 'Hi', level: 1, ref: expect.stringMatching(/^e\d+$/) }, + ]); +}); + +test('snapshot returns structured children and flags', async ({ cli, server }) => { + server.setContent('/', ` + Link + + `, 'text/html'); + await cli('open', server.PREFIX); + const { output } = await cli('--json', 'snapshot'); + const parsed = JSON.parse(output); + expect(parsed.snapshot).toEqual([ + { + role: 'generic', + active: true, + ref: expect.stringMatching(/^e\d+$/), + children: [ + { + role: 'link', + name: 'Link', + url: 'https://example.com/', + ref: expect.stringMatching(/^e\d+$/), + cursor: 'pointer', + }, + { role: 'button', name: 'Click', disabled: true, ref: expect.stringMatching(/^e\d+$/) }, + ], + }, + ]); }); test('tool error on bad navigation returns JSON error', async ({ cli, server }) => { diff --git a/tests/mcp/cli-misc.spec.ts b/tests/mcp/cli-misc.spec.ts index 67ba1e004040f..c99f422704f1f 100644 --- a/tests/mcp/cli-misc.spec.ts +++ b/tests/mcp/cli-misc.spec.ts @@ -40,6 +40,23 @@ test('install workspace', async ({ cli }, testInfo) => { expect(fs.existsSync(playwrightDir)).toBe(true); }); +test('install adds .playwright-cli/ to .gitignore', async ({ cli }, testInfo) => { + const outsideGitRepo = await cli('install'); + expect(outsideGitRepo.output).not.toContain('.gitignore'); + expect(fs.existsSync(testInfo.outputPath('.gitignore'))).toBe(false); + + await fs.promises.mkdir(testInfo.outputPath('.git'), { recursive: true }); + await fs.promises.writeFile(testInfo.outputPath('.gitignore'), 'node_modules/'); + const insideGitRepo = await cli('install'); + expect(insideGitRepo.output).toContain('Added `.playwright-cli/` to `.gitignore`.'); + const expectedContent = 'node_modules/\n# Playwright CLI output (may contain credentials)\n.playwright-cli/\n'; + expect(await fs.promises.readFile(testInfo.outputPath('.gitignore'), 'utf8')).toBe(expectedContent); + + const secondRun = await cli('install'); + expect(secondRun.output).not.toContain('.gitignore'); + expect(await fs.promises.readFile(testInfo.outputPath('.gitignore'), 'utf8')).toBe(expectedContent); +}); + test('install workspace w/skills', async ({ cli }, testInfo) => { const { output } = await cli('install', '--skills'); expect(output).toContain(`Skill installed to \`.claude${path.sep}skills${path.sep}playwright-cli\`.`); @@ -60,6 +77,32 @@ test('install workspace w/--skills=agents', async ({ cli }, testInfo) => { expect(fs.existsSync(skillFile)).toBe(true); }); +test('install w/--skills -g installs into the home directory', async ({ cli }, testInfo) => { + const fakeHome = testInfo.outputPath('fake-home'); + await fs.promises.mkdir(fakeHome, { recursive: true }); + const { output } = await cli('install', '--skills', '-g', { env: { HOME: fakeHome, USERPROFILE: fakeHome } }); + expect(output).toContain('Skill installed to'); + expect(output).not.toContain('Workspace initialized'); + + const skillFile = path.join(fakeHome, '.claude', 'skills', 'playwright-cli', 'SKILL.md'); + expect(fs.existsSync(skillFile)).toBe(true); +}); + +test('install w/--skills=agents --global installs into the home directory', async ({ cli }, testInfo) => { + const fakeHome = testInfo.outputPath('fake-home'); + await fs.promises.mkdir(fakeHome, { recursive: true }); + await cli('install', '--skills=agents', '--global', { env: { HOME: fakeHome, USERPROFILE: fakeHome } }); + + const skillFile = path.join(fakeHome, '.agents', 'skills', 'playwright-cli', 'SKILL.md'); + expect(fs.existsSync(skillFile)).toBe(true); +}); + +test('install -g without --skills errors', async ({ cli }) => { + const result = await cli('install', '-g'); + expect(result.exitCode).toBe(1); + expect(result.error).toContain('--global requires --skills'); +}); + test('install handles browser detection', async ({ cli }) => { const { output } = await cli('install'); // Verify that one of the browser detection outcomes occurred @@ -76,3 +119,10 @@ test('open with very long session name (issue 40878)', async ({ cli, server }) = expect(result.exitCode).toBe(0); expect(result.output).toContain('Page URL'); }); + +test('open with long multi-byte session name (issue 42153)', async ({ cli, server }) => { + const result = await cli('-s=セッション名がとても長い場合の動作を確認するためのテスト', 'open', server.PREFIX); + expect(result.error).toBe(''); + expect(result.exitCode).toBe(0); + expect(result.output).toContain('Page URL'); +}); diff --git a/tests/mcp/cli-parsing.spec.ts b/tests/mcp/cli-parsing.spec.ts index 1c9957b9436bc..947da89cb216e 100644 --- a/tests/mcp/cli-parsing.spec.ts +++ b/tests/mcp/cli-parsing.spec.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { test, expect } from './cli-fixtures'; +import { test, expect, eventsPage } from './cli-fixtures'; test('unknown option', async ({ cli, server }) => { const { error, exitCode } = await cli('open', '--some-option', 'value', 'about:blank'); @@ -57,6 +57,13 @@ test('missing argument', async ({ cli, server }) => { expect(error).toContain(`error: 'key' argument: expected string, received undefined`); }); +test('missing variadic argument', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42047' } }, async ({ cli, server }) => { + await cli('open', server.HELLO_WORLD); + const { error, exitCode } = await cli('upload'); + expect(exitCode).toBe(1); + expect(error).toContain(`error: 'files' argument: expected string, received undefined`); +}); + test('wrong argument type', async ({ cli, server }) => { await cli('open', server.HELLO_WORLD); const { error, exitCode } = await cli('mousemove', '12', 'foo'); @@ -66,6 +73,18 @@ test('wrong argument type', async ({ cli, server }) => { expect(press.exitCode).toBe(0); }); +test('negative number arguments', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42321' } }, async ({ cli, server }) => { + server.setContent('/', eventsPage, 'text/html'); + await cli('open', server.PREFIX); + await cli('mousemove', '50', '50'); + + expect((await cli('mousewheel', '0', '-100')).exitCode).toBe(0); + await expect.poll(() => cli('snapshot').then(result => result.inlineSnapshot)).toContain('wheel 0 -100'); + + const { error } = await cli('mousewheel', '-.5'); + expect(error).toContain(`error: 'dy' argument: expected number, received 'undefined'`); +}); + test('should preserve leading zeros in string arguments', async ({ cli, server }) => { server.setContent('/', ``, 'text/html'); await cli('open', server.PREFIX); diff --git a/tests/mcp/cli-session.spec.ts b/tests/mcp/cli-session.spec.ts index 30bd50abd3eb3..08a2b13125469 100644 --- a/tests/mcp/cli-session.spec.ts +++ b/tests/mcp/cli-session.spec.ts @@ -293,7 +293,7 @@ workspace1: await page.setContent('My Page'); const { output: openOutput } = await cli('attach', 'foobar'); expect(openOutput).toContain('### Session `foobar` created, attached to `foobar`.'); - expect(openOutput).toContain('Run commands with: playwright-cli --s=foobar '); + expect(openOutput).toContain('Run commands with: playwright-cli -s=foobar '); const { output: listOutput } = await cli('list', '--all'); expect(listOutput).toBe(`### Browsers /: @@ -326,7 +326,7 @@ workspace1: await page.setContent('Alias Page'); const { output: openOutput } = await cli('attach', 'foobar', '--session=mybrowser'); expect(openOutput).toContain('### Session `mybrowser` created, attached to `foobar`.'); - expect(openOutput).toContain('Run commands with: playwright-cli --s=mybrowser '); + expect(openOutput).toContain('Run commands with: playwright-cli -s=mybrowser '); await cli('-s', 'mybrowser', 'close'); }); diff --git a/tests/mcp/cli-test.spec.ts b/tests/mcp/cli-test.spec.ts index 7a246c7d05f8d..1e156e0ade14e 100644 --- a/tests/mcp/cli-test.spec.ts +++ b/tests/mcp/cli-test.spec.ts @@ -58,14 +58,14 @@ test('debug test and snapshot', async ({ cliEnv, cli, childProcess }) => { const { output: stepOutput } = await cli(`--session=${session}`, 'step-over'); expect(stepOutput).toContain('### Paused'); - expect(stepOutput).toContain(`- Expect "toBeVisible" at subdir${path.sep}a.test.ts:5`); + expect(stepOutput).toContain(`- Expect "toBeVisible" getByRole('button', { name: 'Submit' }) at subdir${path.sep}a.test.ts:5`); const snapshotResult = await cli(`--session=${session}`, 'snapshot'); expect(snapshotResult.inlineSnapshot).toContain('button "Submit"'); const { output: pauseAtOutput } = await cli(`--session=${session}`, 'pause-at', 'a.test.ts:7'); expect(pauseAtOutput).toContain('### Paused'); - expect(pauseAtOutput).toContain(`- Expect "toBeVisible" at subdir${path.sep}a.test.ts:7`); + expect(pauseAtOutput).toContain(`- Expect "toBeVisible" getByRole('button', { name: 'Close' }) at subdir${path.sep}a.test.ts:7`); await cli(`--session=${session}`, 'resume'); @@ -114,3 +114,82 @@ test('debug test with custom fixture using browser.newContext', async ({ cliEnv, await cli(`--session=${session}`, 'resume'); }); + +test('debug test that creates multiple contexts', async ({ cliEnv, cli, childProcess }) => { + await writeFiles({ + 'subdir/a.test.ts': ` + import { test as base, expect } from '@playwright/test'; + const test = base.extend<{}, { precreatedContext: import('@playwright/test').BrowserContext }>({ + precreatedContext: [async ({ browser }, use) => { + const context = await browser.newContext(); + await use(context); + await context.close(); + }, { scope: 'worker' }], + }); + test('example test', async ({ page, precreatedContext }) => { + await page.setContent('My Page'); + await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible(); + }); + `, + }); + + const testProcess = childProcess({ + command: [process.argv[0], testEntrypoint, 'test', '--debug=cli'], + cwd: test.info().outputPath('subdir'), + env: cliEnv, + }); + + await testProcess.waitForOutput('playwright-cli attach'); + const session = testProcess.output.match(/attach ([a-zA-Z0-9-_]+)/)[1]; + + const { output: attachOutput } = await cli('attach', session); + expect(attachOutput).toContain('### Paused'); + expect(attachOutput).toContain(`- Set content at subdir${path.sep}a.test.ts:11`); + + const { output: stepOutput } = await cli(`--session=${session}`, 'step-over'); + expect(stepOutput).toContain('### Paused'); + + const snapshotResult = await cli(`--session=${session}`, 'snapshot'); + expect(snapshotResult.inlineSnapshot).toContain('button "Submit"'); + + await cli(`--session=${session}`, 'resume'); + await testProcess.exited; + expect(testProcess.output).toContain('1 passed'); +}); + +test('debug multiple tests in the same worker', async ({ cliEnv, cli, childProcess }) => { + await writeFiles({ + 'subdir/a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('test one', async ({ page }) => { + await page.setContent('My Page'); + }); + test('test two', async ({ page }) => { + await page.setContent('My Page'); + }); + `, + }); + + const testProcess = childProcess({ + command: [process.argv[0], testEntrypoint, 'test', '--debug=cli'], + cwd: test.info().outputPath('subdir'), + env: cliEnv, + }); + + await testProcess.waitForOutput('playwright-cli attach'); + const session = testProcess.output.match(/attach ([a-zA-Z0-9-_]+)/)[1]; + + for (const buttonName of ['One', 'Two']) { + await expect(async () => { + const { output } = await cli('attach', session); + expect(output).toContain('### Paused'); + }).toPass(); + await cli(`--session=${session}`, 'step-over'); + const snapshotResult = await cli(`--session=${session}`, 'snapshot'); + expect(snapshotResult.inlineSnapshot).toContain(`button "${buttonName}"`); + await cli(`--session=${session}`, 'resume'); + } + + await testProcess.exited; + expect(testProcess.output).toContain('2 passed'); +}); diff --git a/tests/mcp/click.spec.ts b/tests/mcp/click.spec.ts index 67b950fa05bc9..b61303133a9d4 100644 --- a/tests/mcp/click.spec.ts +++ b/tests/mcp/click.spec.ts @@ -136,7 +136,7 @@ test('browser_click (modifiers)', async ({ client, server, mcpBrowser }) => { })).toHaveResponse({ code: [ `await page.getByRole('button', { name: 'Submit' }).click({`, - ` modifiers: ['Control']`, + ` modifiers: ['ControlOrMeta']`, `});` ].join('\n'), snapshot: expect.stringContaining(`generic [ref=e3]: ctrlKey:true metaKey:false shiftKey:false altKey:false`), @@ -169,7 +169,7 @@ test('browser_click (modifiers)', async ({ client, server, mcpBrowser }) => { })).toHaveResponse({ code: [ `await page.getByRole('button', { name: 'Submit' }).click({`, - ` modifiers: ['Shift', 'Alt']`, + ` modifiers: ['Alt', 'Shift']`, `});` ].join('\n'), snapshot: expect.stringContaining(`generic [ref=e3]: ctrlKey:false metaKey:false shiftKey:true altKey:true`), diff --git a/tests/mcp/clipboard.spec.ts b/tests/mcp/clipboard.spec.ts index 264948b924474..bc17f026ed453 100644 --- a/tests/mcp/clipboard.spec.ts +++ b/tests/mcp/clipboard.spec.ts @@ -17,9 +17,10 @@ import { test, expect } from './fixtures'; test('clipboard write without permission dialog', async ({ startClient, server, mcpBrowser }) => { - test.skip(mcpBrowser === 'firefox' || mcpBrowser === 'webkit', 'Clipboard permissions are fully supported only in Chromium'); + test.skip(mcpBrowser === 'firefox', 'No such permissions (requires flag) in Firefox'); + const permissions = mcpBrowser === 'webkit' ? 'clipboard-read' : 'clipboard-read,clipboard-write'; const { client } = await startClient({ - args: [`--grant-permissions=clipboard-read,clipboard-write`] + args: [`--grant-permissions=${permissions}`] }); await client.callTool({ name: 'browser_navigate', @@ -36,6 +37,12 @@ test('clipboard write without permission dialog', async ({ startClient, server, expect(writeResult).toHaveResponse({ result: '"Write successful"', }); + // Chromium 153+ only allows reading the clipboard once the page has been + // activated by a real input event, so interact with it first. + await client.callTool({ + name: 'browser_press_key', + arguments: { key: 'a' }, + }); const readResult = await client.callTool({ name: 'browser_evaluate', arguments: { diff --git a/tests/mcp/codegen.spec.ts b/tests/mcp/codegen.spec.ts new file mode 100644 index 0000000000000..c4c689133d516 --- /dev/null +++ b/tests/mcp/codegen.spec.ts @@ -0,0 +1,172 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import { test, expect } from './fixtures'; + +import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; + +async function navigateToForm(client: Client, server: any) { + server.setContent('/', ` + Title + + + `, 'text/html'); + return await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); +} + +test('codegen defaults to PW_LANG_NAME', async ({ startClient, server }) => { + const { client } = await startClient({ env: { PW_LANG_NAME: 'python' } }); + expect(await navigateToForm(client, server)).toHaveResponse({ + code: `page.goto("${server.PREFIX}")`, + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { element: 'Submit button', target: 'e2' }, + })).toHaveResponse({ + code: `page.get_by_role("button", name="Submit").click()`, + }); +}); + +test('codegen python', async ({ startClient, server }) => { + const { client } = await startClient({ args: ['--codegen=python'] }); + expect(await navigateToForm(client, server)).toHaveResponse({ + code: `page.goto("${server.PREFIX}")`, + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { element: 'Submit button', target: 'e2' }, + })).toHaveResponse({ + code: `page.get_by_role("button", name="Submit").click()`, + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { element: 'Submit button', target: 'e2', modifiers: ['Control'] }, + })).toHaveResponse({ + code: `page.get_by_role("button", name="Submit").click(modifiers=["ControlOrMeta"])`, + }); + + expect(await client.callTool({ + name: 'browser_type', + arguments: { element: 'textbox', target: 'e3', text: `it's a secret`, submit: true }, + })).toHaveResponse({ + code: `page.get_by_role("textbox").fill("it's a secret")\npage.get_by_role("textbox").press("Enter")`, + }); + + // Page-level keyboard input has no action equivalent and stays as JavaScript. + expect(await client.callTool({ + name: 'browser_press_key', + arguments: { key: 'Escape' }, + })).toHaveResponse({ + code: `// Press Escape\nawait page.keyboard.press('Escape');`, + }); +}); + +test('codegen java', async ({ startClient, server }) => { + const { client } = await startClient({ args: ['--codegen=java'] }); + expect(await navigateToForm(client, server)).toHaveResponse({ + code: `page.navigate("${server.PREFIX}");`, + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { element: 'Submit button', target: 'e2' }, + })).toHaveResponse({ + code: `page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Submit")).click();`, + }); + + expect(await client.callTool({ + name: 'browser_type', + arguments: { element: 'textbox', target: 'e3', text: 'hello', submit: false }, + })).toHaveResponse({ + code: `page.getByRole(AriaRole.TEXTBOX).fill("hello");`, + }); +}); + +test('codegen csharp', async ({ startClient, server }) => { + const { client } = await startClient({ args: ['--codegen=csharp'] }); + expect(await navigateToForm(client, server)).toHaveResponse({ + code: `await page.GotoAsync("${server.PREFIX}");`, + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { element: 'Submit button', target: 'e2' }, + })).toHaveResponse({ + code: `await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`, + }); + + expect(await client.callTool({ + name: 'browser_type', + arguments: { element: 'textbox', target: 'e3', text: 'hello', submit: false }, + })).toHaveResponse({ + code: `await page.GetByRole(AriaRole.Textbox).FillAsync("hello");`, + }); +}); + +test('codegen verify tools', async ({ startClient, server }) => { + const { client } = await startClient({ args: ['--codegen=python', '--caps=testing'] }); + await navigateToForm(client, server); + await client.callTool({ + name: 'browser_type', + arguments: { element: 'textbox', target: 'e3', text: 'hello', submit: false }, + }); + + expect(await client.callTool({ + name: 'browser_verify_value', + arguments: { type: 'textbox', element: 'textbox', target: 'e3', value: 'hello' }, + })).toHaveResponse({ + code: `expect(page.get_by_role("textbox")).to_have_value("hello")`, + }); +}); + +test('codegen renders secrets as environment lookups', async ({ startClient, server }) => { + const secretsFile = test.info().outputPath('secrets.env'); + await fs.promises.writeFile(secretsFile, 'X-PASSWORD=password123'); + + for (const [language, code] of [ + ['typescript', `await page.getByRole('textbox').fill(process.env['X-PASSWORD']);`], + ['python', `page.get_by_role("textbox").fill(os.environ["X-PASSWORD"])`], + ['java', `page.getByRole(AriaRole.TEXTBOX).fill(System.getenv("X-PASSWORD"));`], + ['csharp', `await page.GetByRole(AriaRole.Textbox).FillAsync(Environment.GetEnvironmentVariable("X-PASSWORD"));`], + ] as const) { + const { client } = await startClient({ args: [`--codegen=${language}`, '--secrets', secretsFile] }); + await navigateToForm(client, server); + expect(await client.callTool({ + name: 'browser_type', + arguments: { element: 'textbox', target: 'e3', text: 'X-PASSWORD', submit: false }, + })).toHaveResponse({ code }); + await client.close(); + } +}); + +test('codegen falls back to JavaScript for scripted lines', async ({ startClient, server }) => { + const { client } = await startClient({ args: ['--codegen=python'] }); + await navigateToForm(client, server); + + expect(await client.callTool({ + name: 'browser_evaluate', + arguments: { function: '() => document.title' }, + })).toHaveResponse({ + code: `await page.evaluate('() => document.title');`, + }); +}); diff --git a/tests/mcp/config-resolve.spec.ts b/tests/mcp/config-resolve.spec.ts index 25c0910252e50..92c9a61c70e6f 100644 --- a/tests/mcp/config-resolve.spec.ts +++ b/tests/mcp/config-resolve.spec.ts @@ -18,12 +18,24 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; +import { Command } from 'commander'; + import { test, expect } from './fixtures'; import { tools } from '../../packages/playwright-core/lib/coreBundle'; import type { Config } from '../../packages/playwright-core/src/tools/mcp/config.d'; -const { resolveCLIConfigForCLI, resolveCLIConfigForMCP, isSystemDirectory, outputDir } = tools; +const { decorateMCPCommand, resolveCLIConfigForCLI, resolveCLIConfigForMCP, isSystemDirectory, outputDir } = tools; + +// Parses the command line the same way the mcp server entry point does, without starting the server. +async function parseCLIOptions(argv: string[]): Promise { + const command = new Command(); + decorateMCPCommand(command); + let options: any; + command.action(o => { options = o; }); + await command.parseAsync(argv, { from: 'user' }); + return options; +} // Empty env to isolate tests from the host environment. const emptyEnv = {}; @@ -145,6 +157,19 @@ test.describe('sandbox', () => { expect(config.browser.launchOptions.chromiumSandbox).toBe(true); }); + test('chromium sandbox disabled for browserName chromium without channel', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42452' }, + }, async ({}, testInfo) => { + const configFile = testInfo.outputPath('config.json'); + await fs.promises.writeFile(configFile, JSON.stringify({ browser: { browserName: 'chromium' } })); + const config = await resolveCLIConfigForMCP({ config: configFile }, emptyEnv); + expect(config.browser.launchOptions.channel).toBeUndefined(); + if (process.platform === 'linux') + expect(config.browser.launchOptions.chromiumSandbox).toBe(false); + else + expect(config.browser.launchOptions.chromiumSandbox).toBe(true); + }); + test('sandbox not set for non-chromium browsers', async () => { const config = await resolveCLIConfigForMCP({ browser: 'firefox' }, emptyEnv); expect(config.browser.launchOptions.chromiumSandbox).toBeUndefined(); @@ -159,6 +184,22 @@ test.describe('sandbox', () => { const config = await resolveCLIConfigForMCP({ browser: 'chrome', sandbox: false }, emptyEnv); expect(config.browser.launchOptions.chromiumSandbox).toBe(false); }); + + test('--sandbox on the command line enables the sandbox', async () => { + const config = await resolveCLIConfigForMCP(await parseCLIOptions(['--browser=chromium', '--sandbox']), emptyEnv); + expect(config.browser.launchOptions.channel).toBe('chrome-for-testing'); + expect(config.browser.launchOptions.chromiumSandbox).toBe(true); + }); + + test('--no-sandbox on the command line disables the sandbox', async () => { + const config = await resolveCLIConfigForMCP(await parseCLIOptions(['--browser=chrome', '--no-sandbox']), emptyEnv); + expect(config.browser.launchOptions.chromiumSandbox).toBe(false); + }); + + test('no sandbox flag on the command line leaves the value unset', async () => { + const options = await parseCLIOptions(['--browser=chrome']); + expect(options.sandbox).toBeUndefined(); + }); }); // --------------------------------------------------------------------------- diff --git a/tests/mcp/config.spec.ts b/tests/mcp/config.spec.ts index cb42d5214555c..c042e50d21ca6 100644 --- a/tests/mcp/config.spec.ts +++ b/tests/mcp/config.spec.ts @@ -173,3 +173,30 @@ test('browser_get_config returns merged config from file, env and cli', async ({ // From CLI arg (--isolated). expect(config.browser.isolated).toBe(true); }); + +test.describe('chromiumSandbox', () => { + test.skip(({ mcpBrowser }) => mcpBrowser !== 'chrome', 'Channel-agnostic tests.'); + + test('config file value is respected', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/1716' } }, async ({ startClient }) => { + const { client } = await startClient({ + config: { + capabilities: ['config'], + browser: { launchOptions: { chromiumSandbox: true } }, + }, + }); + const config = JSON.parse(parseResponse(await client.callTool({ name: 'browser_get_config' })).result); + expect(config.browser.launchOptions.chromiumSandbox).toBe(true); + }); + + test('--no-sandbox overrides config file value', async ({ startClient }) => { + const { client } = await startClient({ + config: { + capabilities: ['config'], + browser: { launchOptions: { chromiumSandbox: true } }, + }, + args: ['--no-sandbox'], + }); + const config = JSON.parse(parseResponse(await client.callTool({ name: 'browser_get_config' })).result); + expect(config.browser.launchOptions.chromiumSandbox).toBe(false); + }); +}); diff --git a/tests/mcp/dashboard.spec.ts b/tests/mcp/dashboard.spec.ts index 4437988873f2c..3f4ae73679511 100644 --- a/tests/mcp/dashboard.spec.ts +++ b/tests/mcp/dashboard.spec.ts @@ -110,7 +110,7 @@ test('should activate session when show is called with -s', async ({ cli, server await cli('-s=sessB', 'open', server.EMPTY_PAGE); const dashboard = await startDashboardServer({ session: 'sessB' }); - await expect(activeSession(dashboard)).toHaveAccessibleName('Session sessB'); + await expect(activeSession(dashboard)).toHaveAccessibleName('Session sessB', { timeout: 30000 }); }); function isAlive(pid: number): boolean { diff --git a/tests/mcp/dialogs.spec.ts b/tests/mcp/dialogs.spec.ts index 57d3d39a18dda..6b13ba0803fbe 100644 --- a/tests/mcp/dialogs.spec.ts +++ b/tests/mcp/dialogs.spec.ts @@ -215,6 +215,51 @@ test('prompt dialog', async ({ client, server }) => { }); }); +test('dialog closed out of band', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41837' }, +}, async ({ cdpServer, startClient, server }) => { + server.setContent('/', `Title`, 'text/html'); + + const browserContext = await cdpServer.start(); + const [page] = browserContext.pages(); + // Subscribe to the dialog event to prevent this connection from auto-dismissing dialogs. + page.on('dialog', () => {}); + // Establish the CDP session up front: creating one while a dialog is blocking the page hangs. + const cdpSession = await browserContext.newCDPSession(page); + await cdpSession.send('Page.enable'); + + const { client } = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] }); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + snapshot: expect.stringContaining(`- button "Button" [ref=e2]`), + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Button', + target: 'e2', + }, + })).toHaveResponse({ + modalState: `- ["alert" dialog with message "Alert"]: can be handled by browser_handle_dialog`, + }); + + // Close the dialog through CDP as a side-channel, similar to the user closing it in the headed browser. + const closedPromise = page.waitForEvent('dialogclosed'); + await cdpSession.send('Page.handleJavaScriptDialog', { accept: true }); + await closedPromise; + + await expect.poll(() => client.callTool({ + name: 'browser_snapshot', + })).toHaveResponse({ + modalState: undefined, + inlineSnapshot: expect.stringContaining(`- button "Button"`), + }); +}); + test('alert dialog w/ race', async ({ client, server }) => { server.setContent('/', `Title`, 'text/html'); expect(await client.callTool({ diff --git a/tests/mcp/files.spec.ts b/tests/mcp/files.spec.ts index 3ed6693140ff1..4456529cac0ec 100644 --- a/tests/mcp/files.spec.ts +++ b/tests/mcp/files.spec.ts @@ -290,6 +290,46 @@ test('file upload is restricted to cwd if no roots are configured', async ({ sta }); }); +test('file upload resolves relative paths against the root', async ({ startClient, server }, testInfo) => { + const rootDir = testInfo.outputPath('workspace'); + await fs.mkdir(rootDir, { recursive: true }); + await fs.writeFile(path.join(rootDir, 'inside.txt'), 'Inside root'); + + const { client } = await startClient({ + roots: [ + { + name: 'workspace', + uri: `file://${rootDir}`, + } + ], + }); + + server.setContent('/', ``, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Textbox', + target: 'e2', + }, + }); + + // The file lives in the root, not in the server's cwd. + expect(await client.callTool({ + name: 'browser_file_upload', + arguments: { + paths: ['inside.txt'], + }, + })).toHaveResponse({ + code: expect.stringContaining(JSON.stringify(path.join(rootDir, 'inside.txt'))), + }); +}); + test('file upload unrestricted when flag is set', async ({ startClient, server }, testInfo) => { const rootDir = testInfo.outputPath('workspace'); await fs.mkdir(rootDir, { recursive: true }); diff --git a/tests/mcp/form.spec.ts b/tests/mcp/form.spec.ts index 46e5c6a8bcb6f..9a6fa3cdd06f7 100644 --- a/tests/mcp/form.spec.ts +++ b/tests/mcp/form.spec.ts @@ -97,7 +97,7 @@ test('browser_fill_form (textbox)', async ({ client, server }) => { await page.getByRole('textbox', { name: 'Email' }).fill('john.doe@example.com'); await page.getByRole('slider', { name: 'Age' }).fill('25'); await page.getByLabel('Choose a country United').selectOption('United States'); -await page.getByRole('checkbox', { name: 'Subscribe to newsletter' }).setChecked(true);`, +await page.getByRole('checkbox', { name: 'Subscribe to newsletter' }).check();`, }); const response = await client.callTool({ diff --git a/tests/mcp/http.spec.ts b/tests/mcp/http.spec.ts index 85dca4f8773e6..f47e3d3b31aa9 100644 --- a/tests/mcp/http.spec.ts +++ b/tests/mcp/http.spec.ts @@ -18,6 +18,7 @@ import fs from 'fs'; import dns from 'dns'; import { ChildProcess, spawn } from 'child_process'; +import { chromium } from 'playwright'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { test as baseTest, expect, mcpServerPath, formatLog } from './fixtures'; @@ -151,13 +152,14 @@ test('http transport browser sigint', async ({ serverEndpoint, server }) => { arguments: { url: server.HELLO_WORLD }, }); - await fetch(new URL('/killkillkill', url).href, { method: 'POST', headers: { 'x-pw-mcp-kill': '1' } }).catch(() => {}); + await fetch(new URL('/killkillkill', url).href).catch(() => {}); await expect.poll(() => formatLog(stderr())).toEqual({ 'create browser (isolated)': 1, 'create context': 1, 'create http session': 1, 'gracefully closing 1': 1, + 'close browser': 1, }); }); @@ -235,6 +237,103 @@ test('http transport browser lifecycle (isolated, concurrent clients)', { annota }); }); +test('http transport isolated multiclient relaunches a crashed shared browser', async ({ serverEndpoint, server }, testInfo) => { + // The CDP port lets the test kill the browser from the outside. + const port = 9400 + testInfo.workerIndex; + const configFile = testInfo.outputPath('config.json'); + await fs.promises.writeFile(configFile, JSON.stringify({ + browser: { launchOptions: { args: [`--remote-debugging-port=${port}`] } }, + })); + const { url, stderr } = await serverEndpoint({ + args: ['--isolated', `--config=${configFile}`], + env: { DEBUG: 'pw:mcp:test,pw:mcp:backend' }, + }); + + const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client1 = new Client({ name: 'test', version: '1.0.0' }); + await client1.connect(transport1); + await client1.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + const transport2 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client2 = new Client({ name: 'test', version: '1.0.0' }); + await client2.connect(transport2); + await client2.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + // Kill the shared browser, as if it crashed, and wait for both backends + // to observe the disconnect. + const cdpBrowser = await chromium.connectOverCDP(`http://localhost:${port}`); + const session = await cdpBrowser.newBrowserCDPSession(); + await session.send('Browser.close').catch(() => {}); + await expect.poll(() => stderr().match(/browser disconnected/g)?.length).toBe(2); + + // Each client transparently migrates to a fresh shared browser on its + // next tool call. + for (const client of [client1, client2]) { + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + snapshot: expect.stringContaining(`Hello, world!`), + }); + } + + await transport1.terminateSession(); + await client1.close(); + await transport2.terminateSession(); + await client2.close(); + + await expect.poll(() => formatLog(stderr())).toEqual(({ + 'create http session': 2, + 'delete http session': 2, + 'create browser (isolated)': 2, + 'create context': 4, + 'close browser': 2, + 'close context': 2, + })); +}); + +test('http transport isolated closes the browser despite an earlier failed backend creation', async ({ serverEndpoint, server }, testInfo) => { + // A failed backend creation must not leak the client count, otherwise the + // browser is never closed once the last client disconnects. + const storageStatePath = testInfo.outputPath('storage-state.json'); + const { url, stderr } = await serverEndpoint({ args: ['--isolated', `--storage-state=${storageStatePath}`] }); + + const transport = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client = new Client({ name: 'test', version: '1.0.0' }); + await client.connect(transport); + + // The browser launches, but context creation fails on the missing file. + expect((await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).isError).toBe(true); + + await fs.promises.writeFile(storageStatePath, JSON.stringify({ origins: [] })); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + snapshot: expect.stringContaining(`Hello, world!`), + }); + + await transport.terminateSession(); + await client.close(); + + await expect.poll(() => formatLog(stderr())).toEqual({ + 'create http session': 1, + 'delete http session': 1, + 'create browser (isolated)': 1, + 'create context': 1, + 'close browser': 1, + }); +}); + test('http transport browser lifecycle (persistent)', async ({ serverEndpoint, server }) => { const { url, stderr } = await serverEndpoint(); @@ -339,6 +438,64 @@ test('http transport shared context', async ({ serverEndpoint, server }) => { }); }); +test('http transport shared context refuses browser_close', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42363' } }, async ({ serverEndpoint, server }) => { + const { url, stderr } = await serverEndpoint({ args: ['--shared-browser-context'] }); + + const transport1 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client1 = new Client({ name: 'test1', version: '1.0.0' }); + await client1.connect(transport1); + await client1.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + const transport2 = new StreamableHTTPClientTransport(new URL('/mcp', url)); + const client2 = new Client({ name: 'test2', version: '1.0.0' }); + await client2.connect(transport2); + await client2.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + // The context is shared with the second client, so closing it is refused. + expect(await client1.callTool({ + name: 'browser_close', + arguments: {}, + })).toHaveResponse({ + error: 'Error: The browser context is shared between clients and cannot be closed.', + isError: true, + }); + + // The first client keeps working. + expect(await client1.callTool({ + name: 'browser_tabs', + arguments: { action: 'new', url: server.HELLO_WORLD }, + })).toHaveResponse({ + snapshot: expect.stringContaining(`Hello, world!`), + }); + + // The second client is unaffected. + expect(await client2.callTool({ + name: 'browser_snapshot', + arguments: {}, + })).toHaveResponse({ + inlineSnapshot: expect.stringContaining(`Hello, world!`), + }); + + await transport1.terminateSession(); + await client1.close(); + await transport2.terminateSession(); + await client2.close(); + + await expect.poll(() => formatLog(stderr())).toEqual({ + 'create browser (persistent)': 1, + 'create http session': 2, + 'delete http session': 2, + 'create context': 2, + 'close browser': 1, + }); +}); + test('http transport (default)', async ({ serverEndpoint }) => { const { url } = await serverEndpoint(); const transport = new StreamableHTTPClientTransport(url); @@ -391,6 +548,44 @@ test('should close session when heartbeat ping is not answered', async ({ server await expect.poll(() => formatLog(stderr())['delete http session']).toBe(1); }); +test('should not reap session of a client without the event stream', async ({ serverEndpoint, server }) => { + const { url, stderr } = await serverEndpoint({ env: { PLAYWRIGHT_MCP_PING_TIMEOUT_MS: '500' } }); + + // A POST-only client that never opens the GET event stream (optional per spec), + // so server-initiated pings cannot be delivered to it. + // https://github.com/microsoft/playwright-mcp/issues/1710 + const endpoint = new URL('/mcp', url); + let lastId = 0; + const post = async (body: object, sessionId?: string) => { + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'accept': 'application/json, text/event-stream', + ...(sessionId ? { 'mcp-session-id': sessionId } : {}), + }, + body: JSON.stringify(body), + }); + return { status: response.status, sessionId: response.headers.get('mcp-session-id'), text: await response.text() }; + }; + + const init = await post({ jsonrpc: '2.0', id: ++lastId, method: 'initialize', params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'post-only', version: '1.0.0' } } }); + expect(init.status).toBe(200); + const sessionId = init.sessionId!; + await post({ jsonrpc: '2.0', method: 'notifications/initialized' }, sessionId); + + const navigate = await post({ jsonrpc: '2.0', id: ++lastId, method: 'tools/call', params: { name: 'browser_navigate', arguments: { url: server.HELLO_WORLD } } }, sessionId); + expect(navigate.status).toBe(200); + + // Wait long past the ping timeout, the heartbeat must not kick in. + await new Promise(f => setTimeout(f, 1000)); + + const snapshot = await post({ jsonrpc: '2.0', id: ++lastId, method: 'tools/call', params: { name: 'browser_snapshot', arguments: {} } }, sessionId); + expect(snapshot.status).toBe(200); + expect(snapshot.text).toContain('Hello, world!'); + expect(formatLog(stderr())['delete http session']).toBeUndefined(); +}); + test('should not run heartbeat when timeout is non-positive', async ({ serverEndpoint, server }) => { const { url, stderr } = await serverEndpoint({ env: { PLAYWRIGHT_MCP_PING_TIMEOUT_MS: '0' } }); diff --git a/tests/mcp/init-script.spec.ts b/tests/mcp/init-script.spec.ts index 9462eda5a2818..4bdb4dda5df99 100644 --- a/tests/mcp/init-script.spec.ts +++ b/tests/mcp/init-script.spec.ts @@ -19,7 +19,7 @@ import fs from 'fs'; for (const context of ['isolated', 'persistent']) { - test(`--init-script option loads and executes script (${context})`, async ({ startClient, server, mcpBrowser }, testInfo) => { + test(`--init-script option loads and executes script (${context})`, async ({ startClient, server }, testInfo) => { // Create a temporary init script const initScriptPath = testInfo.outputPath('init-script1.js'); const initScriptContent1 = `window.testInitScriptExecuted = true;`; @@ -55,9 +55,7 @@ for (const context of ['isolated', 'persistent']) { expect(await client.callTool({ name: 'browser_console_messages', - // FIXME: in firefox commit event comes after console messages from the init script. - // See https://github.com/microsoft/playwright/issues/39624. - arguments: { all: mcpBrowser === 'firefox' } + arguments: {} })).toHaveResponse({ result: expect.stringMatching(/Init script executed successfully.*Custom log/ms), }); diff --git a/tests/mcp/launch.spec.ts b/tests/mcp/launch.spec.ts index 031581f6fab53..9e410e3c796e8 100644 --- a/tests/mcp/launch.spec.ts +++ b/tests/mcp/launch.spec.ts @@ -16,6 +16,8 @@ import fs from 'fs'; +import { chromium } from 'playwright'; + import { test, expect, formatLog } from './fixtures'; test('test reopen browser', async ({ startClient, server }) => { @@ -61,6 +63,36 @@ test('executable path', async ({ startClient, server }) => { }); }); +test('surfaces the missing browser executable path so a version mismatch is diagnosable', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41871' }, +}, async ({ startClient, server, mcpBrowser }, testInfo) => { + test.skip(mcpBrowser === 'chrome' || mcpBrowser === 'msedge', 'Channel browsers use system-installed binaries, which are unaffected by PLAYWRIGHT_BROWSERS_PATH'); + + const emptyBrowsersPath = testInfo.outputPath('empty-browsers'); + await fs.promises.mkdir(emptyBrowsersPath, { recursive: true }); + + const { client } = await startClient({ + env: { PLAYWRIGHT_BROWSERS_PATH: emptyBrowsersPath }, + }); + + const response = await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + // The surfaced path must include the version-specific browser directory + // (e.g. chromium-1234) — that's the detail that reveals a version mismatch, + // as opposed to a generic "not installed". + const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + expect.soft(response).toHaveResponse({ + isError: true, + error: expect.stringContaining('is not installed'), + }); + expect.soft(response).toHaveResponse({ + isError: true, + error: expect.stringMatching(new RegExp(escapeRegExp(emptyBrowsersPath) + String.raw`[\\/][\w.]+-\d+[\\/]`)), + }); +}); + test('persistent context', async ({ startClient, server }, testInfo) => { server.setContent('/', ` @@ -129,6 +161,40 @@ test('isolated context', async ({ startClient, server }) => { }); }); +test('isolated context relaunches the browser after it dies', async ({ startClient, server, mcpBrowser }, testInfo) => { + test.skip(!['chrome', 'msedge', 'chromium'].includes(mcpBrowser!), 'The test kills the browser over CDP'); + + // The CDP port lets the test kill the browser from the outside. + const port = 9300 + testInfo.workerIndex; + const { client, stderr } = await startClient({ + args: [`--isolated`], + config: { browser: { launchOptions: { args: [`--remote-debugging-port=${port}`] } } }, + env: { DEBUG: 'pw:mcp:backend' }, + }); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + snapshot: expect.stringContaining(`Hello, world!`), + }); + + // Kill the browser, as if it crashed. + const cdpBrowser = await chromium.connectOverCDP(`http://localhost:${port}`); + const session = await cdpBrowser.newBrowserCDPSession(); + await session.send('Browser.close').catch(() => {}); + await expect.poll(() => stderr()).toContain('browser disconnected'); + + // The very next tool call must relaunch the browser, with no failed call + // in between. + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + snapshot: expect.stringContaining(`Hello, world!`), + }); +}); + test('isolated context with storage state', async ({ startClient, server }, testInfo) => { const storageStatePath = testInfo.outputPath('storage-state.json'); await fs.promises.writeFile(storageStatePath, JSON.stringify({ diff --git a/tests/mcp/recorder.spec.ts b/tests/mcp/recorder.spec.ts new file mode 100644 index 0000000000000..b3867c5ea47c9 --- /dev/null +++ b/tests/mcp/recorder.spec.ts @@ -0,0 +1,198 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +import type { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import type { TestServer } from '../config/testserver'; + +test.use({ mcpCaps: ['devtools'] }); + +async function navigateToForm(client: Client, server: TestServer) { + server.setContent('/', ` + Title + + + `, 'text/html'); + return await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); +} + +test('record actions between start and stop', async ({ client, server }) => { + await navigateToForm(client, server); + + expect(await client.callTool({ + name: 'browser_start_recording', + })).toHaveResponse({ + result: expect.stringContaining('Recording started'), + }); + + await client.callTool({ + name: 'browser_click', + arguments: { element: 'Submit button', target: 'e2' }, + }); + await client.callTool({ + name: 'browser_type', + arguments: { element: 'textbox', target: 'e3', text: 'Hello world' }, + }); + + expect(await client.callTool({ + name: 'browser_stop_recording', + })).toHaveResponse({ + result: expect.stringContaining([ + 'Recording stopped. Recorded actions:', + '', + '```js', + `await page.getByRole('button', { name: 'Submit' }).click();`, + `await page.getByRole('textbox').fill('Hello world');`, + '```', + ].join('\n')), + }); +}); + +test('record navigation', async ({ client, server }) => { + await navigateToForm(client, server); + server.setContent('/page2', `Page 2`, 'text/html'); + + await client.callTool({ name: 'browser_start_recording' }); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX + '/page2' }, + }); + + expect(await client.callTool({ + name: 'browser_stop_recording', + })).toHaveResponse({ + result: expect.stringContaining(`await page.goto('${server.PREFIX}/page2');`), + }); +}); + +test('stop with no actions recorded', async ({ client, server }) => { + await navigateToForm(client, server); + await client.callTool({ name: 'browser_start_recording' }); + expect(await client.callTool({ + name: 'browser_stop_recording', + })).toHaveResponse({ + result: expect.stringContaining('No actions were recorded.'), + }); +}); + +test('restarted recording only contains new actions', async ({ client, server }) => { + server.setContent('/', ` + Title + + + `, 'text/html'); + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + await client.callTool({ name: 'browser_start_recording' }); + await client.callTool({ + name: 'browser_click', + arguments: { element: 'Alpha button', target: 'e2' }, + }); + expect(await client.callTool({ + name: 'browser_stop_recording', + })).toHaveResponse({ + result: expect.stringContaining(`await page.getByRole('button', { name: 'Alpha' }).click();`), + }); + + await client.callTool({ name: 'browser_start_recording' }); + await client.callTool({ + name: 'browser_click', + arguments: { element: 'Beta button', target: 'e3' }, + }); + // Matching the entire block asserts no duplicate or stale actions. + expect(await client.callTool({ + name: 'browser_stop_recording', + })).toHaveResponse({ + result: expect.stringContaining([ + '```js', + `await page.getByRole('button', { name: 'Beta' }).click();`, + '```', + ].join('\n')), + }); +}); + +test('actions performed while not recording are not included', async ({ client, server }) => { + await navigateToForm(client, server); + + await client.callTool({ + name: 'browser_click', + arguments: { element: 'Submit button', target: 'e2' }, + }); + + await client.callTool({ name: 'browser_start_recording' }); + await client.callTool({ + name: 'browser_type', + arguments: { element: 'textbox', target: 'e3', text: 'Hello world' }, + }); + + expect(await client.callTool({ + name: 'browser_stop_recording', + })).toHaveResponse({ + result: expect.stringContaining([ + '```js', + `await page.getByRole('textbox').fill('Hello world');`, + '```', + ].join('\n')), + }); +}); + +test('record actions in python', async ({ startClient, server }) => { + const { client } = await startClient({ args: ['--codegen=python'] }); + await navigateToForm(client, server); + + await client.callTool({ name: 'browser_start_recording' }); + await client.callTool({ + name: 'browser_click', + arguments: { element: 'Submit button', target: 'e2' }, + }); + + expect(await client.callTool({ + name: 'browser_stop_recording', + })).toHaveResponse({ + result: expect.stringContaining([ + '```python', + `page.get_by_role("button", name="Submit").click()`, + ].join('\n')), + }); +}); + +test('start twice is an error', async ({ client, server }) => { + await navigateToForm(client, server); + await client.callTool({ name: 'browser_start_recording' }); + expect(await client.callTool({ + name: 'browser_start_recording', + })).toHaveResponse({ + isError: true, + error: expect.stringContaining('Recording is already in progress'), + }); +}); + +test('stop without start is an error', async ({ client }) => { + expect(await client.callTool({ + name: 'browser_stop_recording', + })).toHaveResponse({ + isError: true, + error: expect.stringContaining('No recording in progress'), + }); +}); diff --git a/tests/mcp/remote-endpoint.spec.ts b/tests/mcp/remote-endpoint.spec.ts index 42521074bbfe8..4b0ce13b78b89 100644 --- a/tests/mcp/remote-endpoint.spec.ts +++ b/tests/mcp/remote-endpoint.spec.ts @@ -16,6 +16,10 @@ import { test, expect } from './fixtures'; +import { tools } from '../../packages/playwright-core/lib/coreBundle'; + +const { resolveCLIConfigForMCP, createBrowserWithInfo } = tools; + test.skip(({ mcpBrowser }) => mcpBrowser !== 'chromium', 'Run only on the chromium project; the remote server connection is browser-agnostic.'); test('connect without headers fails on run-server endpoint', async ({ startClient, server, runServerEndpoint }) => { @@ -60,6 +64,18 @@ test('remoteEndpoint accepts ConnectOptions object with headers', async ({ start }); }); +test('browserInfo reports the browser running on the remote endpoint, not the configured one', async ({ wsEndpoint }, testInfo) => { + // Empty env to isolate the test from the host environment. + const config = await resolveCLIConfigForMCP({ browser: 'firefox', endpoint: wsEndpoint }, {}); + const { browser, browserInfo } = await createBrowserWithInfo(config, { clientName: 'test-client', cwd: testInfo.outputPath() }, {}); + try { + expect(config.browser.browserName).toBe('firefox'); + expect(browserInfo.browserName).toBe('chromium'); + } finally { + await browser.close(); + } +}); + test('back-compat: remoteHeaders config still selects the browser on run-server endpoint', async ({ startClient, server, runServerEndpoint }) => { const { client } = await startClient({ config: { diff --git a/tests/mcp/screenshot.spec.ts b/tests/mcp/screenshot.spec.ts index d61f6f116d05d..b9f6ad7d2e69f 100644 --- a/tests/mcp/screenshot.spec.ts +++ b/tests/mcp/screenshot.spec.ts @@ -246,6 +246,31 @@ test('browser_take_screenshot (filename: "output.png")', async ({ client, server expect(files[0]).toMatch(/^output\.png$/); }); +test('browser_take_screenshot (filename: "sub/dir/output.png")', async ({ client, server }, testInfo) => { + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + code: expect.stringContaining(`page.goto('http://localhost`), + }); + + expect(await client.callTool({ + name: 'browser_take_screenshot', + arguments: { + filename: 'sub/dir/output.png', + }, + })).toEqual({ + content: [ + { + text: expect.stringContaining(`output.png`), + type: 'text', + }, + ], + }); + + expect(fs.existsSync(testInfo.outputPath('sub', 'dir', 'output.png'))).toBeTruthy(); +}); + test('browser_take_screenshot (imageResponses=omit)', async ({ startClient, server }, testInfo) => { const outputDir = testInfo.outputPath('output'); const { client } = await startClient({ @@ -307,7 +332,7 @@ test('browser_take_screenshot (fullPage: true)', async ({ startClient, server }, }); }); -test('browser_take_screenshot size cap', async ({ startClient, server, mcpBrowser }, testInfo) => { +test('browser_take_screenshot preserves image dimensions', async ({ startClient, server, mcpBrowser }, testInfo) => { test.skip(!['chrome', 'msedge', 'chromium'].includes(mcpBrowser ?? ''), 'Non-chrome has unusual full page size'); const { client } = await startClient({ @@ -315,8 +340,8 @@ test('browser_take_screenshot size cap', async ({ startClient, server, mcpBrowse }); const expectations = [ - { title: '2000x500', pageWidth: 2000, pageHeight: 500, expectedWidth: 1568, expectedHeight: 720 * 1568 / 2000 | 0 }, - { title: '2000x2000', pageWidth: 2000, pageHeight: 2000, expectedWidth: 1098, expectedHeight: 1098 }, + { title: '2000x500', pageWidth: 2000, pageHeight: 500, expectedWidth: 2000, expectedHeight: 720 }, + { title: '2000x2000', pageWidth: 2000, pageHeight: 2000, expectedWidth: 2000, expectedHeight: 2000 }, { title: '1280x800', pageWidth: 1280, pageHeight: 800, expectedWidth: 1280, expectedHeight: 800 }, ]; diff --git a/tests/mcp/snapshot-mode.spec.ts b/tests/mcp/snapshot-mode.spec.ts index e96689ddc5e0b..2b6bafa1e5155 100644 --- a/tests/mcp/snapshot-mode.spec.ts +++ b/tests/mcp/snapshot-mode.spec.ts @@ -96,6 +96,32 @@ test('should not inline console messages with --snapshot-mode=none', async ({ st }); }); +test('should respect --snapshot-boxes', async ({ startClient, server }) => { + server.setContent('/', ` + + + `, 'text/html'); + + const { client } = await startClient({ + args: ['--snapshot-boxes'], + }); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { + url: server.PREFIX, + }, + })).toHaveResponse({ + snapshot: expect.stringContaining(`- button "click" [ref=e1] [box=100,50,80,40]`), + }); + + expect(await client.callTool({ + name: 'browser_snapshot', + })).toHaveResponse({ + inlineSnapshot: expect.stringContaining(`- button "click" [ref=e1] [box=100,50,80,40]`), + }); +}); + test('should respect snapshot[filename]', async ({ client, server }, testInfo) => { server.setContent('/', ``, 'text/html'); diff --git a/tests/mcp/test-debug.spec.ts b/tests/mcp/test-debug.spec.ts index 880d73f3f5ae9..4389d0443482c 100644 --- a/tests/mcp/test-debug.spec.ts +++ b/tests/mcp/test-debug.spec.ts @@ -64,7 +64,7 @@ Timeout: 1000ms Error: element(s) not found Call log: - - Expect "toBeVisible" with timeout 1000ms + - Expect "toBeVisible" getByRole('button', { name: 'Missing' }) with timeout 1000ms - waiting for getByRole('button', { name: 'Missing' }) @@ -183,7 +183,7 @@ Timeout: 1000ms Error: element(s) not found Call log: - - Expect "toBeVisible" with timeout 1000ms + - Expect "toBeVisible" getByRole('button', { name: 'Missing' }) with timeout 1000ms - waiting for getByRole('button', { name: 'Missing' }) diff --git a/tests/mcp/trace-cli-fixtures.ts b/tests/mcp/trace-cli-fixtures.ts index 1c39e81887dac..0f05b8f492fd5 100644 --- a/tests/mcp/trace-cli-fixtures.ts +++ b/tests/mcp/trace-cli-fixtures.ts @@ -108,6 +108,10 @@ export const test = baseTest // Fetch await page.evaluate(() => fetch('/feedback', { method: 'POST', body: 'What a great product!' }).then(res => res.text())); + // Aborted fetch + await page.route('**/blocked', route => route.abort()); + await page.evaluate(() => fetch('/blocked').catch(() => {})); + // Navigate to another page await page.locator('a').click(); diff --git a/tests/mcp/trace-cli.spec.ts b/tests/mcp/trace-cli.spec.ts index 7b886ad1f6a71..f1192a7d3111e 100644 --- a/tests/mcp/trace-cli.spec.ts +++ b/tests/mcp/trace-cli.spec.ts @@ -93,6 +93,18 @@ test('trace requests shows requests with ordinals', async ({ runTraceCli }) => { expect(stdout).toMatch(/\d+\./); }); +test('trace requests shows start times and aborted request durations', async ({ runTraceCli }) => { + const { stdout, exitCode } = await runTraceCli(['requests']); + expect(exitCode).toBe(0); + expect(stdout).toContain('Start'); + // Every request row carries a start timestamp in the `trace actions` Time format. + expect(stdout).toMatch(/\d+\.\s+\d+:\d{2}\.\d{3}\s/); + // The aborted request has a recorded duration rather than '-'. + const abortedRow = stdout.split('\n').find(line => line.includes('aborted'))!; + expect(abortedRow).toContain('blocked'); + expect(abortedRow).toMatch(/\s\d+(\.\d+)?m?s\s/); +}); + test('trace requests --method filters', async ({ runTraceCli }) => { const { stdout, exitCode } = await runTraceCli(['requests', '--method', 'GET']); expect(exitCode).toBe(0); @@ -105,6 +117,7 @@ test('trace request shows details', async ({ runTraceCli }) => { expect(exitCode).toBe(0); expect(stdout).toContain('General'); expect(stdout).toContain('status:'); + expect(stdout).toMatch(/start:\s+\d+:\d{2}\.\d{3}/); expect(stdout).toContain('Request headers'); expect(stdout).toContain('Response headers'); }); @@ -148,6 +161,14 @@ test('trace console --errors-only', async ({ runTraceCli }) => { expect(stdout).not.toContain('info message'); }); +test('trace console --grep filters by message text', async ({ runTraceCli }) => { + const { stdout, exitCode } = await runTraceCli(['console', '--grep', 'warning']); + expect(exitCode).toBe(0); + expect(stdout).toContain('warning message'); + expect(stdout).not.toContain('info message'); + expect(stdout).not.toContain('error message'); +}); + test('trace errors', async ({ runTraceCli }) => { const { stdout, exitCode } = await runTraceCli(['errors']); expect(exitCode).toBe(0); @@ -165,12 +186,12 @@ test('trace snapshot runs command on snapshot', async ({ runTraceCli }) => { expect(stdout).toBeTruthy(); }); -test('trace snapshot --name before', async ({ runTraceCli }) => { +test('trace snapshot --phase before', async ({ runTraceCli }) => { const { stdout: listOutput } = await runTraceCli(['actions', '--grep', 'Click']); const match = listOutput.match(/^\s+(\d+)\.\s/m); expect(match).toBeTruthy(); - const { stdout, exitCode } = await runTraceCli(['snapshot', '--name', 'before', match![1]]); + const { stdout, exitCode } = await runTraceCli(['snapshot', '--phase', 'before', match![1]]); expect(exitCode).toBe(0); expect(stdout).toBeTruthy(); }); @@ -181,7 +202,7 @@ test('trace snapshot resolves inner frames', async ({ runTraceCli }) => { expect(ordinals.length).toBeGreaterThanOrEqual(2); const anchorClickOrdinal = ordinals[ordinals.length - 1]; - const { stdout } = await runTraceCli(['snapshot', '--name', 'after', anchorClickOrdinal]); + const { stdout } = await runTraceCli(['snapshot', '--phase', 'after', anchorClickOrdinal]); expect(stdout).toContain('Innermost'); }); @@ -192,7 +213,7 @@ test('trace snapshot replays sub-resource stylesheets from the archive', async ( const ordinal = match![1]; const { stdout, exitCode } = await runTraceCli([ - 'snapshot', '--name', 'before', ordinal, + 'snapshot', '--phase', 'before', ordinal, '--', 'eval', 'el => getComputedStyle(el).color', '#styled', ]); expect(exitCode).toBe(0); diff --git a/tests/mcp/tracing.spec.ts b/tests/mcp/tracing.spec.ts index 9f9e0edbdefb6..8aa4f23e1aaa9 100644 --- a/tests/mcp/tracing.spec.ts +++ b/tests/mcp/tracing.spec.ts @@ -45,6 +45,7 @@ test('check that trace is saved with browser_start_tracing', async ({ startClien const files = await fs.promises.readdir(path.join(outputDir, 'traces')); expect(files).toEqual([ 'resources', + 'screencast', expect.stringMatching(/trace-\d+\.network/), expect.stringMatching(/trace-\d+\.stacks/), expect.stringMatching(/trace-\d+\.trace/), @@ -78,6 +79,7 @@ test('check that trace is saved with browser_start_tracing (no output dir)', asy const files = await fs.promises.readdir(testInfo.outputPath('.playwright-mcp', 'traces')); expect(files).toEqual([ 'resources', + 'screencast', expect.stringMatching(/trace-\d+\.network/), expect.stringMatching(/trace-\d+\.stacks/), expect.stringMatching(/trace-\d+\.trace/), diff --git a/tests/mcp/verify.spec.ts b/tests/mcp/verify.spec.ts index a98a00fa6f080..ebacc7525b9e6 100644 --- a/tests/mcp/verify.spec.ts +++ b/tests/mcp/verify.spec.ts @@ -261,11 +261,11 @@ test('browser_verify_list_visible', async ({ client, server }) => { })).toHaveResponse({ result: 'Done', code: expect.stringContaining(`await expect(page.locator('body')).toMatchAriaSnapshot(\` -- list: - - listitem: "Apple" - - listitem: "Banana" - - listitem: "Cherry" -\`);`), + - list: + - listitem: "Apple" + - listitem: "Banana" + - listitem: "Cherry" + \`);`), }); }); @@ -295,10 +295,10 @@ test('browser_verify_list_visible (partial items)', async ({ client, server }) = })).toHaveResponse({ result: 'Done', code: expect.stringContaining(`await expect(page.locator('body')).toMatchAriaSnapshot(\` -- list: - - listitem: "Apple" - - listitem: "Cherry" -\`);`), + - list: + - listitem: "Apple" + - listitem: "Cherry" + \`);`), }); }); diff --git a/tests/page/expect-boolean.spec.ts b/tests/page/expect-boolean.spec.ts index f2eddbc3f1b6e..043c55f695d9f 100644 --- a/tests/page/expect-boolean.spec.ts +++ b/tests/page/expect-boolean.spec.ts @@ -61,7 +61,7 @@ Locator: locator('input') Expected: checked Received: unchecked Timeout: 1000ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" locator('input') with timeout 1000ms`); }); test('with not', async ({ page }) => { @@ -86,7 +86,7 @@ Locator: locator('input') Expected: not checked Received: checked Timeout: 1000ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "not toBeChecked" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "not toBeChecked" locator('input') with timeout 1000ms`); expect(stripAnsi(error.message)).toContain(`locator resolved to `); }); @@ -100,7 +100,7 @@ Locator: locator('input') Expected: unchecked Received: checked Timeout: 1000ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" locator('input') with timeout 1000ms`); expect(stripAnsi(error.message)).toContain(`locator resolved to `); }); @@ -114,7 +114,7 @@ Locator: locator('input') Expected: indeterminate Received: unchecked Timeout: 1000ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" locator('input') with timeout 1000ms`); }); test('fail missing', async ({ page }) => { @@ -129,7 +129,7 @@ Timeout: 1000ms Error: element(s) not found Call log: - - Expect "not toBeChecked" with timeout 1000ms + - Expect "not toBeChecked" locator('input2') with timeout 1000ms - waiting for locator('input2') `); }); @@ -475,7 +475,7 @@ Timeout: 1000ms Error: element(s) not found Call log: - - Expect "not toBeHidden" with timeout 1000ms + - Expect "not toBeHidden" locator('button') with timeout 1000ms `); }); diff --git a/tests/page/expect-misc.spec.ts b/tests/page/expect-misc.spec.ts index a118d402717b2..76d66d5287feb 100644 --- a/tests/page/expect-misc.spec.ts +++ b/tests/page/expect-misc.spec.ts @@ -73,7 +73,7 @@ Locator: locator('span') Expected: 0 Received: 1 Timeout: 1000ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "toHaveCount" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toHaveCount" locator('span') with timeout 1000ms`); }); test('fail zero 2', async ({ page }) => { @@ -86,7 +86,7 @@ Locator: locator('span') Expected: not 1 Received: 1 Timeout: 1000ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveCount" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveCount" locator('span') with timeout 1000ms`); }); }); @@ -124,7 +124,7 @@ Locator: locator('div') Expected: "error" Received: "string" Timeout: 200ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" with timeout 200ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" locator('div') with timeout 200ms`); }); test('pass number', async ({ page }) => { @@ -145,7 +145,7 @@ Locator: locator('div') Expected: 1 Received: 2021 Timeout: 200ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" with timeout 200ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" locator('div') with timeout 200ms`); }); test('pass boolean', async ({ page }) => { @@ -166,7 +166,7 @@ Locator: locator('div') Expected: true Received: false Timeout: 200ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" with timeout 200ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" locator('div') with timeout 200ms`); }); test('pass boolean 2', async ({ page }) => { @@ -187,7 +187,7 @@ Locator: locator('div') Expected: true Received: false Timeout: 200ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" with timeout 200ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toHaveJSProperty" locator('div') with timeout 200ms`); }); test('pass undefined', async ({ page }) => { @@ -249,7 +249,7 @@ Timeout: 1000ms Call log: `); - expect(stripAnsi(error.message)).toContain(`- Expect "toHaveClass" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toHaveClass" locator('div') with timeout 1000ms`); }); test('pass with array', async ({ page }) => { @@ -264,7 +264,7 @@ Call log: const error = await expect(locator).toHaveClass(['foo', 'bar', /[a-z]az/], { timeout: 1000 }).catch(e => e); expect(stripAnsi(error.message)).toContain(`expect(locator).toHaveClass(expected) failed`); expect(stripAnsi(error.message)).toContain(`Timeout: 1000ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "toHaveClass" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toHaveClass" locator('div') with timeout 1000ms`); }); }); @@ -298,7 +298,7 @@ Timeout: 1000ms Call log: `); - expect(stripAnsi(error.message)).toContain(`- Expect "toContainClass" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toContainClass" locator('div') with timeout 1000ms`); }); test('pass with array', async ({ page }) => { @@ -316,7 +316,7 @@ Call log: const error = await expect(locator).toContainClass(['foo', 'bar', 'baz'], { timeout: 1000 }).catch(e => e); expect(stripAnsi(error.message)).toContain(`expect(locator).toContainClass(expected) failed`); expect(stripAnsi(error.message)).toContain(`Timeout: 1000ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "toContainClass" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toContainClass" locator('div') with timeout 1000ms`); }); }); @@ -442,7 +442,7 @@ Timeout: 1000ms Call log: `); - expect(stripAnsi(error.message)).toContain(`- Expect "toHaveAttribute" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toHaveAttribute" locator('#node') with timeout 1000ms`); } { const error = await expect(locator).toHaveAttribute('disabled', /.*/, { timeout: 1000 }).catch(e => e); @@ -455,7 +455,7 @@ Timeout: 1000ms Call log: `); - expect(stripAnsi(error.message)).toContain(`- Expect "toHaveAttribute" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "toHaveAttribute" locator('#node') with timeout 1000ms`); } await expect(locator).not.toHaveAttribute('disabled', ''); await expect(locator).not.toHaveAttribute('disabled', /.*/); @@ -477,7 +477,7 @@ Timeout: 1000ms Call log: `); - expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveAttribute" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveAttribute" locator('#node') with timeout 1000ms`); } { const error = await expect(locator).not.toHaveAttribute('checked', /.*/, { timeout: 1000 }).catch(e => e); @@ -487,7 +487,7 @@ Locator: locator('#node') Expected pattern: not /.*/ Received string: "" Timeout: 1000ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveAttribute" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveAttribute" locator('#node') with timeout 1000ms`); } }); diff --git a/tests/page/expect-timeout.spec.ts b/tests/page/expect-timeout.spec.ts index 6753716a1c25b..04f8b6544d61c 100644 --- a/tests/page/expect-timeout.spec.ts +++ b/tests/page/expect-timeout.spec.ts @@ -136,7 +136,7 @@ Expected: visible Error: element(s) not found Call log: - - Expect "toBeVisible" with timeout 5000ms + - Expect "toBeVisible" locator('span') with timeout 5000ms - waiting for locator('span') - operation was aborted: stop it`); }); diff --git a/tests/page/expect-to-have-text.spec.ts b/tests/page/expect-to-have-text.spec.ts index 081e9a2ecafb7..e11e7d813c739 100644 --- a/tests/page/expect-to-have-text.spec.ts +++ b/tests/page/expect-to-have-text.spec.ts @@ -259,7 +259,7 @@ test.describe('toHaveText with array', () => { const error = await expect(locator).not.toHaveText([], { timeout: 1000 }).catch(e => e); expect(stripAnsi(error.message)).toContain(`expect(locator).not.toHaveText(expected)`); expect(stripAnsi(error.message)).toContain(`Timeout: 1000ms`); - expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveText" with timeout 1000ms`); + expect(stripAnsi(error.message)).toContain(`- Expect "not toHaveText" locator('p') with timeout 1000ms`); }); test('pass eventually empty', async ({ page }) => { @@ -289,7 +289,7 @@ Timeout: 1000ms ] Call log: - - Expect \"toHaveText\" with timeout 1000ms + - Expect \"toHaveText\" locator('div') with timeout 1000ms - waiting for locator('div') `); expect(stripAnsi(error.message)).toContain('locator resolved to 2 elements'); diff --git a/tests/page/locator-any-frame.spec.ts b/tests/page/locator-any-frame.spec.ts new file mode 100644 index 0000000000000..9059c437fac3f --- /dev/null +++ b/tests/page/locator-any-frame.spec.ts @@ -0,0 +1,528 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Page } from 'playwright-core'; +import { test as it, expect } from './pageTest'; + +function routePage(page: Page, url: string, body: string) { + return page.route('**/' + url, route => { + route.fulfill({ body, contentType: 'text/html' }).catch(() => {}); + }); +} + +async function waitForAllFrames(page: Page, frameCount: number, selector: string) { + // Wait for all child frames to load their content, so that the search + // deterministically sees elements in all of them. + await expect.poll(() => page.frames().length).toBe(frameCount); + for (const frame of page.frames()) { + if (frame !== page.mainFrame()) + await frame.waitForSelector(selector, { state: 'attached' }); + } +} + +it('should click a button inside an iframe', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await page.goto(server.EMPTY_PAGE); + await page.frameLocator().getByRole('button', { name: 'Click me' }).click(); + expect(await page.frames()[1].evaluate(() => (window as any).__clicked)).toBe(true); +}); + +it('should click a button in the main frame', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `
No buttons here
`); + await page.goto(server.EMPTY_PAGE); + await page.frameLocator().locator('button').click(); + expect(await page.evaluate(() => (window as any).__clicked)).toBe(true); +}); + +it('should fail click when elements match in multiple frames', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await routePage(page, 'b.html', ``); + await page.goto(server.EMPTY_PAGE); + await waitForAllFrames(page, 3, 'button'); + const error = await page.frameLocator().locator('button').click({ timeout: 3000 }).catch(e => e); + expect(error.message).toContain('frameLocator() matched elements in multiple frames'); + expect(error.message).toContain(`waiting for frameLocator().locator('button')`); +}); + +it('should fail click upon strict mode violation inside a single frame', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await page.goto(server.EMPTY_PAGE); + await waitForAllFrames(page, 2, 'button'); + const error = await page.frameLocator().locator('button').click({ timeout: 3000 }).catch(e => e); + expect(error.message).toContain('strict mode violation'); + expect(error.message).toContain(`waiting for frameLocator().locator('button')`); +}); + +it('should time out on click when there are no matches', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `
Nothing here
`); + await page.goto(server.EMPTY_PAGE); + const error = await page.frameLocator().locator('button').click({ timeout: 1000 }).catch(e => e); + expect(error.message).toContain('Timeout 1000ms exceeded'); + expect(error.message).toContain(`waiting for frameLocator().locator('button')`); +}); + +it('should count elements in a single frame', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `
1
2
3
`); + await page.goto(server.EMPTY_PAGE); + await waitForAllFrames(page, 2, 'div'); + expect(await page.frameLocator().locator('div').count()).toBe(3); + expect(await page.frameLocator().locator('button').count()).toBe(0); +}); + +it('should fail count when elements match in multiple frames', async ({ page, server }) => { + await routePage(page, 'empty.html', `
main
`); + await routePage(page, 'a.html', `
child
`); + await page.goto(server.EMPTY_PAGE); + await waitForAllFrames(page, 2, 'div'); + const error = await page.frameLocator().locator('div').count().catch(e => e); + expect(error.message).toContain('frameLocator() matched elements in multiple frames'); +}); + +it('should support toHaveCount', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `onetwo`); + await page.goto(server.EMPTY_PAGE); + await expect(page.frameLocator().locator('span')).toHaveCount(2); + await expect(page.frameLocator().locator('button')).toHaveCount(0); +}); + +it('should wait for a frame to appear with toHaveCount', async ({ page, server }) => { + await routePage(page, 'empty.html', `
No frames yet
`); + await routePage(page, 'a.html', `onetwo`); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + window.builtins.setTimeout(() => { + const iframe = document.createElement('iframe'); + iframe.src = 'a.html'; + document.body.appendChild(iframe); + }, 500); + }); + await expect(page.frameLocator().locator('span')).toHaveCount(2); +}); + +it('should fail toHaveCount when elements match in multiple frames', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `one`); + await routePage(page, 'b.html', `two`); + await page.goto(server.EMPTY_PAGE); + await waitForAllFrames(page, 3, 'span'); + const error = await expect(page.frameLocator().locator('span')).toHaveCount(2, { timeout: 3000 }).catch(e => e); + expect(error.message).toContain('frameLocator() matched elements in multiple frames'); + expect(error.message).toContain(`Locator: frameLocator().locator('span')`); +}); + +it('should support toHaveText', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `
Hello iframe
`); + await page.goto(server.EMPTY_PAGE); + await expect(page.frameLocator().locator('div')).toHaveText('Hello iframe'); +}); + +it('should support toHaveText with an array', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `onetwo`); + await page.goto(server.EMPTY_PAGE); + await expect(page.frameLocator().locator('span')).toHaveText(['one', 'two']); +}); + +it('should fail toHaveText when elements match in multiple frames', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `
one
`); + await routePage(page, 'b.html', `
two
`); + await page.goto(server.EMPTY_PAGE); + await waitForAllFrames(page, 3, 'div'); + const error = await expect(page.frameLocator().locator('div')).toHaveText('one', { timeout: 3000 }).catch(e => e); + expect(error.message).toContain('frameLocator() matched elements in multiple frames'); + expect(error.message).toContain(`Locator: frameLocator().locator('div')`); +}); + +it('should fail toHaveText with an array when elements match in multiple frames', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `one`); + await routePage(page, 'b.html', `two`); + await page.goto(server.EMPTY_PAGE); + await waitForAllFrames(page, 3, 'span'); + const error = await expect(page.frameLocator().locator('span')).toHaveText(['one', 'two'], { timeout: 3000 }).catch(e => e); + expect(error.message).toContain('frameLocator() matched elements in multiple frames'); +}); + +it('should fail toHaveText upon strict mode violation inside a single frame', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `
one
two
`); + await page.goto(server.EMPTY_PAGE); + await waitForAllFrames(page, 2, 'div'); + const error = await expect(page.frameLocator().locator('div')).toHaveText('one', { timeout: 3000 }).catch(e => e); + expect(error.message).toContain('strict mode violation'); + expect(error.message).toContain(`Locator: frameLocator().locator('div')`); +}); + +it('should support evaluate', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `
Hello
`); + await page.goto(server.EMPTY_PAGE); + expect(await page.frameLocator().locator('div').evaluate(e => e.getAttribute('data-foo'))).toBe('bar'); +}); + +it('should fail evaluate when elements match in multiple frames', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `
one
`); + await routePage(page, 'b.html', `
two
`); + await page.goto(server.EMPTY_PAGE); + await waitForAllFrames(page, 3, 'div'); + const error = await page.frameLocator().locator('div').evaluate(e => e.textContent, undefined, { timeout: 3000 }).catch(e => e); + expect(error.message).toContain('frameLocator() matched elements in multiple frames'); +}); + +it('should time out on evaluate when there are no matches', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `
Nothing here
`); + await page.goto(server.EMPTY_PAGE); + const error = await page.frameLocator().locator('button').evaluate(e => e.textContent, undefined, { timeout: 1000 }).catch(e => e); + expect(error.message).toContain('Timeout 1000ms exceeded'); + expect(error.message).toContain(`waiting for frameLocator().locator('button')`); +}); + +it('should support evaluateAll', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `onetwo`); + await page.goto(server.EMPTY_PAGE); + await waitForAllFrames(page, 2, 'span'); + expect(await page.frameLocator().locator('span').evaluateAll(els => els.map(e => e.textContent))).toEqual(['one', 'two']); + expect(await page.frameLocator().locator('button').evaluateAll(els => els.length)).toBe(0); +}); + +it('should fail evaluateAll when elements match in multiple frames', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `one`); + await routePage(page, 'b.html', `two`); + await page.goto(server.EMPTY_PAGE); + await waitForAllFrames(page, 3, 'span'); + const error = await page.frameLocator().locator('span').evaluateAll(els => els.length).catch(e => e); + expect(error.message).toContain('frameLocator() matched elements in multiple frames'); +}); + +it('should support hasText filter', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `
foo
bar
`); + await page.goto(server.EMPTY_PAGE); + await expect(page.frameLocator().locator('div', { hasText: 'bar' })).toHaveText('bar'); +}); + +it('should support first/last/nth', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `onetwothree`); + await page.goto(server.EMPTY_PAGE); + await waitForAllFrames(page, 2, 'span'); + await expect(page.frameLocator().locator('span').first()).toHaveText('one'); + await expect(page.frameLocator().locator('span').last()).toHaveText('three'); + await expect(page.frameLocator().locator('span').nth(1)).toHaveText('two'); +}); + +it('should support nth in the middle of the chain', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `
one
two
`); + await page.goto(server.EMPTY_PAGE); + await expect(page.frameLocator().locator('div').nth(1).locator('span')).toHaveText('two'); +}); + +it('should support composite locators', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `
foo
bar
`); + await page.goto(server.EMPTY_PAGE); + await expect(page.frameLocator().locator('div', { has: page.locator('span') })).toHaveText('foo'); +}); + +it('should support capture', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `
hello
`); + await page.goto(server.EMPTY_PAGE); + await expect(page.frameLocator().locator('*css=div >> span')).toHaveAttribute('id', 'target'); +}); + +it('should find a frame inside the scope', async ({ page, server }) => { + await routePage(page, 'empty.html', `
`); + await routePage(page, 'a.html', ``); + await page.goto(server.EMPTY_PAGE); + await waitForAllFrames(page, 2, 'button'); + const scope = (await page.$('section'))!; + const buttons = await scope.$$('internal:control=any-frame >> button'); + expect(buttons.length).toBe(1); + expect(await buttons[0].textContent()).toBe('inside'); +}); + +it('should find a nested frame inside the scope', async ({ page, server }) => { + await routePage(page, 'empty.html', `
`); + await routePage(page, 'a.html', ``); + await routePage(page, 'b.html', ``); + await routePage(page, 'c.html', ``); + await page.goto(server.EMPTY_PAGE); + await expect.poll(() => page.frames().length).toBe(4); + for (const frame of page.frames()) { + if (frame.url().includes('b.html') || frame.url().includes('c.html')) + await frame.waitForSelector('button', { state: 'attached' }); + } + const scope = (await page.$('section'))!; + const buttons = await scope.$$('internal:control=any-frame >> button'); + expect(buttons.length).toBe(1); + expect(await buttons[0].textContent()).toBe('deep'); +}); + +it('should find a frame inside the scope while another iframe is stalled', async ({ page, server }) => { + await routePage(page, 'empty.html', `
`); + await routePage(page, 'a.html', ``); + await page.route('**/stall.html', () => {}); + await page.goto(server.EMPTY_PAGE, { waitUntil: 'domcontentloaded' }); + await expect.poll(() => page.frames().length).toBe(3); + const scope = (await page.$('section'))!; + const buttons = await scope.$$('internal:control=any-frame >> button'); + expect(buttons.length).toBe(1); + expect(await buttons[0].textContent()).toBe('inside'); +}); + +it('should respect the scope without a frame inside the scope', async ({ page, server }) => { + await routePage(page, 'empty.html', `
`); + await routePage(page, 'a.html', ``); + await page.goto(server.EMPTY_PAGE); + await waitForAllFrames(page, 2, 'button'); + const scope = (await page.$('section'))!; + const buttons = await scope.$$('internal:control=any-frame >> button'); + expect(buttons.length).toBe(1); + expect(await buttons[0].textContent()).toBe('target'); +}); + +it('should not match a chain across a frame boundary', async ({ page, server }) => { + await routePage(page, 'empty.html', `
`); + await routePage(page, 'a.html', ``); + await routePage(page, 'b.html', ``); + await page.goto(server.EMPTY_PAGE); + await expect.poll(() => page.frames().length).toBe(3); + const deepFrame = page.frames().find(f => f.url().includes('b.html'))!; + await deepFrame.waitForSelector('button', { state: 'attached' }); + // "section" lives in the main frame, while the button is two frames below it. + await expect(page.frameLocator().locator('section').locator('button')).toHaveCount(0); + await expect(page.frameLocator().locator('button')).toHaveText('deep'); +}); + +it('should only search frames inside the starting frame', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await routePage(page, 'b.html', ``); + await page.goto(server.EMPTY_PAGE); + await expect.poll(() => page.frames().length).toBe(3); + const deepFrame = page.frames().find(f => f.url().includes('b.html'))!; + await deepFrame.waitForSelector('button', { state: 'attached' }); + const middleFrame = page.frames().find(f => f.url().includes('a.html'))!; + await expect(middleFrame.frameLocator().locator('button')).toHaveText('deep'); +}); + +it('should enter a frame found in a nested frame', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await routePage(page, 'b.html', ``); + await page.goto(server.EMPTY_PAGE); + await expect(page.frameLocator().frameLocator('#target').locator('button')).toHaveText('inside'); +}); + +it('should click inside an entered frame', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await routePage(page, 'b.html', ``); + await page.goto(server.EMPTY_PAGE); + await page.frameLocator().frameLocator('#target').getByRole('button', { name: 'Click me' }).click(); + const frame = page.frames().find(f => f.url().includes('b.html'))!; + expect(await frame.evaluate(() => (window as any).__clicked)).toBe(true); +}); + +it('should not search nested frames after entering a frame', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await routePage(page, 'b.html', ``); + await page.goto(server.EMPTY_PAGE); + await expect.poll(() => page.frames().length).toBe(3); + // The entered frame itself has no button, and we do not look inside its nested frames. + await expect(page.frameLocator().frameLocator('#target').locator('button')).toHaveCount(0); +}); + +it('should support two frameLocators', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await routePage(page, 'b.html', ``); + await routePage(page, 'c.html', ``); + await page.goto(server.EMPTY_PAGE); + await expect(page.frameLocator().frameLocator('#x').frameLocator('#y').locator('button')).toHaveText('bottom'); +}); + +it('should support locator before frameLocator', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `
`); + await routePage(page, 'b.html', ``); + await routePage(page, 'c.html', ``); + await page.goto(server.EMPTY_PAGE); + await expect(page.frameLocator().locator('section').frameLocator('iframe').locator('button')).toHaveText('in-section'); +}); + +it('should support owner of a frameLocator', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await routePage(page, 'b.html', ``); + await page.goto(server.EMPTY_PAGE); + expect(await page.frameLocator().frameLocator('#target').owner().getAttribute('id')).toBe('target'); +}); + +it('should wait for the frame to enter to appear', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `
Nothing yet
`); + await routePage(page, 'b.html', ``); + await page.goto(server.EMPTY_PAGE); + await expect.poll(() => page.frames().length).toBe(2); + await page.frames()[1].evaluate(() => { + window.builtins.setTimeout(() => { + const iframe = document.createElement('iframe'); + iframe.id = 'late'; + iframe.src = 'b.html'; + document.body.appendChild(iframe); + }, 3000); + }); + await expect(page.frameLocator().frameLocator('#late').locator('button')).toHaveText('late'); +}); + +it('should fail when the frame to enter matches in multiple frames', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await routePage(page, 'b.html', ``); + await routePage(page, 'c.html', ``); + await page.goto(server.EMPTY_PAGE); + await expect.poll(() => page.frames().length).toBe(5); + for (const frame of page.frames()) { + if (frame.url().includes('c.html')) + await frame.waitForSelector('button', { state: 'attached' }); + } + const error = await page.frameLocator().frameLocator('.inner').locator('button').click({ timeout: 3000 }).catch(e => e); + expect(error.message).toContain('frameLocator() matched elements in multiple frames'); +}); + +it('should support contentFrame', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await routePage(page, 'b.html', ``); + await page.goto(server.EMPTY_PAGE); + await expect(page.frameLocator().locator('#target').contentFrame().locator('button')).toHaveText('inside'); +}); + +it('should render frameLocator() in the locator description', async ({ page }) => { + expect(String(page.frameLocator().frameLocator('#x').locator('button'))).toBe(`frameLocator().locator('#x').contentFrame().locator('button')`); + expect(String(page.frameLocator().locator('section').frameLocator('iframe').getByText('foo'))).toBe(`frameLocator().locator('section').locator('iframe').contentFrame().getByText('foo')`); +}); + +it('should not allow frameLocator() inside a composite locator', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `link`); + await page.goto(server.EMPTY_PAGE); + await waitForAllFrames(page, 2, 'a'); + + const error = await page.locator('button').or(page.frameLocator().locator('a')).count().catch(e => e); + expect(error.message).toContain(`frameLocator() is not allowed inside composite locators, while querying "locator('button').or(frameLocator().locator('a'))"`); + + const error2 = await page.locator('button').filter({ has: page.frameLocator().locator('a') }).count().catch(e => e); + expect(error2.message).toContain(`frameLocator() is not allowed inside composite locators`); + + // Repeating frameLocator() in the operand is not allowed either, even though the outer locator has it. + const error3 = await page.frameLocator().locator('button').or(page.frameLocator().locator('a')).count().catch(e => e); + expect(error3.message).toContain(`frameLocator() is not allowed inside composite locators, while querying "frameLocator().locator('button').or(frameLocator().locator('a'))"`); + + // Repeating frameLocator() is not allowed even when the rest of the frame chain matches. + const error4 = await page.frameLocator().frameLocator('#f').locator('a').or(page.frameLocator().frameLocator('#f').locator('button')).count().catch(e => e); + expect(error4.message).toContain(`frameLocator() is not allowed inside composite locators`); + + // With frameLocator() first, the token applies to the whole locator, so both operands are searched in every frame. + const error5 = await page.frameLocator().locator('a').or(page.locator('button')).count().catch(e => e); + expect(error5.message).toContain(`frameLocator() matched elements in multiple frames`); +}); + +it('should support a composite locator under frameLocator()', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', `
first
`); + await page.goto(server.EMPTY_PAGE); + await waitForAllFrames(page, 2, 'button'); + await expect(page.frameLocator().locator('.classname').or(page.getByRole('button'))).toHaveText(['first', 'second']); +}); + +it('should support a composite locator under frameLocator() and a frame locator', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await routePage(page, 'b.html', `
first
`); + await page.goto(server.EMPTY_PAGE); + await expect.poll(() => page.frames().length).toBe(3); + await expect(page.frameLocator().frameLocator('#f').locator('.classname').or(page.frameLocator('#f').getByRole('button'))).toHaveText(['first', 'second']); +}); + +it('should not allow first/last/nth on frameLocator()', async ({ page }) => { + expect(() => page.frameLocator().first()).toThrow('Selecting the nth frame is not allowed on frameLocator()'); + expect(() => page.frameLocator().last()).toThrow('Selecting the nth frame is not allowed on frameLocator()'); + expect(() => page.frameLocator().nth(1)).toThrow('Selecting the nth frame is not allowed on frameLocator()'); +}); + +it('should not allow owner on frameLocator()', async ({ page }) => { + const error = await page.frameLocator().owner().count().catch(e => e); + expect(error.message).toContain('Selector cannot be empty after frameLocator()'); +}); + +it('should resolve aria-ref selectors', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await page.goto(server.EMPTY_PAGE); + await waitForAllFrames(page, 2, 'button'); + const snapshot = await page.ariaSnapshot({ mode: 'ai' }); + // Refs are unique across frames, so every starting frame resolves them to the same element. + const insideMatch = snapshot.match(/button "inside" \[ref=(.*?)\]/); + expect(insideMatch![1]).toMatch(/^f\d+e\d+$/); + await expect(page.frameLocator().locator(`aria-ref=${insideMatch![1]}`)).toHaveText('inside'); +}); + +it('should click while another iframe is stalled', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await page.route('**/stall.html', () => {}); + await page.goto(server.EMPTY_PAGE, { waitUntil: 'domcontentloaded' }); + await expect.poll(() => page.frames().length).toBe(3); + await page.frameLocator().locator('button').click(); + const frame = page.frames().find(f => f.url().includes('a.html'))!; + expect(await frame.evaluate(() => (window as any).__clicked)).toBe(true); +}); + +it('should support toBeVisible while another iframe is stalled', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await page.route('**/stall.html', () => {}); + await page.goto(server.EMPTY_PAGE, { waitUntil: 'domcontentloaded' }); + await expect.poll(() => page.frames().length).toBe(3); + await expect(page.frameLocator().locator('button')).toBeVisible(); +}); + +it('should support toHaveCount while another iframe is stalled', async ({ page, server }) => { + await routePage(page, 'empty.html', ``); + await routePage(page, 'a.html', ``); + await page.route('**/stall.html', () => {}); + await page.goto(server.EMPTY_PAGE, { waitUntil: 'domcontentloaded' }); + await expect.poll(() => page.frames().length).toBe(3); + await expect(page.frameLocator().locator('button')).toHaveCount(1); +}); diff --git a/tests/page/locator-misc-2.spec.ts b/tests/page/locator-misc-2.spec.ts index d28463a730bf2..0b921928bfdfa 100644 --- a/tests/page/locator-misc-2.spec.ts +++ b/tests/page/locator-misc-2.spec.ts @@ -161,6 +161,22 @@ it('should support filter(visible)', async ({ page }) => { await expect(page.locator('.item').filter({ visible: false }).getByText('data1')).toHaveText('Hidden data1'); }); +it('should support visible()', async ({ page }) => { + await page.setContent(`
+ +
visible data1
+ +
visible data2
+ +
visible data3
+
+ `); + const locator = page.locator('.item').visible().nth(1); + await expect(locator).toHaveText('visible data2'); + await expect(page.locator('.item').visible().getByText('data3')).toHaveText('visible data3'); + await expect(page.locator('.item').visible()).toHaveCount(3); +}); + it('locator.count should work with deleted Map in main world', async ({ page }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/11254' }); await page.evaluate('Map = 1'); diff --git a/tests/page/locator-query.spec.ts b/tests/page/locator-query.spec.ts index ff48d2cf8fb1c..9fc227adcddaa 100644 --- a/tests/page/locator-query.spec.ts +++ b/tests/page/locator-query.spec.ts @@ -251,6 +251,16 @@ it('should allow some, but not all nested frameLocators', async ({ page }) => { expect(error2.message).toContain(`Frame locators are not allowed inside composite locators, while querying "locator('iframe').contentFrame().locator('div').and(locator('#iframe').contentFrame().locator('span'))`); }); +it('should keep the capture when removing a common frame prefix', async ({ page }) => { + await page.setContent(``); + const inner = page.frameLocator('#f').locator('*css=section >> span'); + await expect(page.frameLocator('#f').locator('body').locator(inner)).toHaveAttribute('id', 'target'); + + const captureFrame = page.locator('*css=#f').contentFrame().locator('span'); + const error = await page.frameLocator('#f').locator('body').locator(captureFrame).count().catch(e => e); + expect(error.message).toContain('Can not capture the selector before diving into the frame'); +}); + it('should enforce same frame for has/leftOf/rightOf/above/below/near', async ({ page, server }) => { await page.goto(server.PREFIX + '/frames/two-frames.html'); const child = page.frames()[1]; diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index 56d314979896c..27f0eea5ca411 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -395,6 +395,30 @@ it('should omit redundant name when a contributor is a skipped leaf generic', as `); }); +it('should keep the name when the contributing wrapper collapses into repeating text', async ({ page }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41985' }); + // The outer span is marked as represented while it still wraps the icon. Once the nameless + // image is dropped, the span collapses into a lone text repeating the button's name and removes + // itself, so the button must keep the name derived from it. + await page.setContent(` + + `); + + const snapshot = await snapshotForAI(page); + expect(snapshot).toContainYaml(` + - button "Add New Item" [ref=e2] + `); +}); + it('should keep names not derived from printed nodes', async ({ page }) => { await page.setContent(`

Clipboard API

@@ -416,13 +440,13 @@ it('should omit images without an accessible name', async ({ page }) => { `); const snapshot = await snapshotForAI(page); - // A nameless image carries no information and is omitted, whether or not it is clickable. Only - // the named image is kept - and the body wrapper, left with a single child, is unwrapped. + // A nameless image that cannot be clicked carries no information and is omitted. expect(snapshot).toContainYaml(` - - img "A cat" [ref=e3] + - generic [active] [ref=e1]: + - img "A cat" [ref=e3] + - img [ref=e4] [cursor=pointer] `); expect(snapshot).not.toContain('[ref=e2]'); - expect(snapshot).not.toContain('[ref=e4]'); }); it('should omit a nameless image nested inside a link', async ({ page }) => { @@ -439,6 +463,27 @@ it('should omit a nameless image nested inside a link', async ({ page }) => { expect(snapshot).not.toContain('img'); }); +it('should keep icon-only clickable elements', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42013' }, +}, async ({ page }) => { + const icon = ``; + await page.setContent(` +
${icon}
+ + ${icon} +
${icon}
+ `); + + const snapshot = await snapshotForAI(page); + expect(snapshot).toContainYaml(` + - generic [active] [ref=e1]: + - generic [ref=e2] [cursor=pointer] + - img [ref=e5] [cursor=pointer] + - link [ref=e6] [cursor=pointer]: + - /url: /target + `); +}); + it('should omit leaf generic whose text is already in an ancestor name', async ({ page }) => { // The inner element is block so it survives as its own generic node (an inline single-text span // would be collapsed into the link instead). It inherits the link's pointer cursor. @@ -886,3 +931,43 @@ it('should limit depth', async ({ page }) => { - listitem [ref=e8] `); }); + +it('should annotate aria-hidden elements', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42223' } }, async ({ page }) => { + await page.setContent(` +

Visible heading

+ +

After hidden

+ `); + + expect(await snapshotForAI(page)).toContainYaml(` + - generic [active] [ref=e1]: + - heading "Visible heading" [level=2] [ref=e2] + - generic [aria-hidden] [ref=e3]: + - heading [level=1] [ref=e4]: Hidden heading + - paragraph [ref=e5]: Hidden content + - heading "After hidden" [level=2] [ref=e6] + `); + + // Default snapshot excludes aria-hidden elements entirely. + const defaultSnapshot = await page.locator('body').ariaSnapshot(); + expect(defaultSnapshot).not.toContain('Hidden content'); +}); + +it('should only annotate the top element in a hidden subtree', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42223' } }, async ({ page }) => { + await page.setContent(` + + `); + + // Only the element with aria-hidden="true" gets the annotation, not its descendants. + expect(await snapshotForAI(page)).toContainYaml(` + - generic [aria-hidden] [ref=e2]: + - heading [level=1] [ref=e3]: Heading + - paragraph [ref=e4]: Paragraph + `); +}); diff --git a/tests/page/page-aria-snapshot-json.spec.ts b/tests/page/page-aria-snapshot-json.spec.ts new file mode 100644 index 0000000000000..946fc81fa1468 --- /dev/null +++ b/tests/page/page-aria-snapshot-json.spec.ts @@ -0,0 +1,182 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test as it, expect } from './pageTest'; + +type NodeJSON = { role: string, name?: string, ref?: string, children?: (NodeJSON | string)[] } & Record; + +function findNode(nodes: (NodeJSON | string)[], predicate: (node: NodeJSON) => boolean): NodeJSON | undefined { + for (const node of nodes) { + if (typeof node === 'string') + continue; + if (predicate(node)) + return node; + const result = findNode(node.children || [], predicate); + if (result) + return result; + } + return undefined; +} + +it('should snapshot roles, names and text', async ({ page }) => { + await page.setContent(` +

title

+
    +
  • one
  • +
  • two
  • +
+ `); + expect(await page.ariaSnapshotJSON()).toEqual([ + { role: 'heading', name: 'title', level: 1 }, + { + role: 'list', + name: 'my list', + children: [ + { role: 'listitem', text: 'one' }, + { role: 'listitem', text: 'two' }, + ], + }, + ]); +}); + +it('should snapshot flags as properties', async ({ page }) => { + await page.setContent(` + + + `); + expect(await page.ariaSnapshotJSON()).toEqual([ + { role: 'checkbox', name: 'Check', checked: true }, + { role: 'button', name: 'Click', disabled: true }, + ]); +}); + +it('should snapshot link url and textbox value', async ({ page }) => { + await page.setContent(` + Link + + `); + expect(await page.ariaSnapshotJSON()).toEqual([ + { role: 'link', name: 'Link', url: 'https://example.com/' }, + { role: 'textbox', name: 'Input', text: 'hello' }, + ]); +}); + +it('should snapshot text fragments in children', async ({ page }) => { + await page.setContent(`

Hello world again

`); + expect(await page.ariaSnapshotJSON()).toEqual([ + { + role: 'paragraph', + children: [ + 'Hello', + { role: 'link', name: 'world', url: '/link' }, + 'again', + ], + }, + ]); +}); + +it('should snapshot top-level text fragments as text nodes', async ({ page }) => { + await page.setContent(`Hello again`); + expect(await page.ariaSnapshotJSON()).toEqual([ + { role: 'text', text: 'Hello' }, + { role: 'button', name: 'One' }, + { role: 'text', text: 'again' }, + ]); +}); + +it('should generate refs in ai mode', async ({ page }) => { + await page.setContent(` + + + `); + expect(await page.ariaSnapshotJSON({ mode: 'ai' })).toEqual([ + { + role: 'generic', + active: true, + ref: 'e1', + children: [ + { role: 'button', name: 'One', ref: 'e2' }, + { role: 'button', name: 'Two', ref: 'e3' }, + ], + }, + ]); + await expect(page.locator('aria-ref=e2')).toHaveText('One'); +}); + +it('should mark clickable elements with cursor in ai mode', async ({ page }) => { + await page.setContent(``); + const json = await page.ariaSnapshotJSON({ mode: 'ai' }) as NodeJSON[]; + const button = findNode(json, node => node.role === 'button'); + expect(button?.cursor).toBe('pointer'); +}); + +it('should snapshot iframes in ai mode', async ({ page }) => { + await page.setContent(` +

Hello

+ + `); + const json = await page.ariaSnapshotJSON({ mode: 'ai' }) as NodeJSON[]; + const iframe = findNode(json, node => node.role === 'iframe'); + expect(iframe?.ref).toBeTruthy(); + const button = findNode(iframe!.children!, node => node.role === 'button'); + expect(button?.name).toBe('In frame'); + expect(button?.ref).toMatch(/^f\d+e\d+$/); +}); + +it('should limit depth', async ({ page }) => { + await page.setContent(`
`); + expect(await page.ariaSnapshotJSON({ depth: 1 })).toEqual([ + { + role: 'list', + children: [ + { role: 'listitem' }, + ], + }, + ]); +}); + +it('should include boxes when requested', async ({ page }) => { + await page.setContent(``); + const json = await page.ariaSnapshotJSON({ boxes: true }) as NodeJSON[]; + const button = findNode(json, node => node.role === 'button'); + expect(button?.box).toEqual({ + x: expect.any(Number), + y: expect.any(Number), + width: expect.any(Number), + height: expect.any(Number), + }); + expect(button!.box.width).toBeGreaterThan(0); + expect(button!.box.height).toBeGreaterThan(0); +}); + +it('should snapshot a locator', async ({ page }) => { + await page.setContent(` +

title

+
    +
  • one
  • +
  • two
  • +
+ `); + expect(await page.locator('ul').ariaSnapshotJSON()).toEqual([ + { + role: 'list', + children: [ + { role: 'listitem', text: 'one' }, + { role: 'listitem', text: 'two' }, + ], + }, + ]); +}); diff --git a/tests/page/page-dialog.spec.ts b/tests/page/page-dialog.spec.ts index 657653a577b74..441611ab20115 100644 --- a/tests/page/page-dialog.spec.ts +++ b/tests/page/page-dialog.spec.ts @@ -17,6 +17,8 @@ import { test as it, expect } from './pageTest'; +import type { Dialog } from 'playwright-core'; + it('should fire', async ({ page, server }) => { page.on('dialog', dialog => { expect(dialog.type()).toBe('alert'); @@ -27,6 +29,39 @@ it('should fire', async ({ page, server }) => { await page.evaluate(() => alert('yo')); }); +it('should fire dialogclosed when dialog is accepted', async ({ page }) => { + const closed: Dialog[] = []; + page.on('dialogclosed', dialog => closed.push(dialog)); + let opened: Dialog | undefined; + page.on('dialog', dialog => { + opened = dialog; + void dialog.accept(); + }); + await page.evaluate(() => alert('yo')); + await expect.poll(() => closed.length).toBe(1); + expect(closed[0]).toBe(opened); + // Perform some roundtrips to ensure the event does not fire twice. + await page.evaluate(() => 1); + await page.evaluate(() => 1); + expect(closed.length).toBe(1); +}); + +it('should fire dialogclosed when dialog is dismissed', async ({ page }) => { + const closedPromise = page.waitForEvent('dialogclosed'); + page.on('dialog', dialog => void dialog.dismiss()); + await page.evaluate(() => confirm('boolean?')); + const dialog = await closedPromise; + expect(dialog.type()).toBe('confirm'); + expect(dialog.message()).toBe('boolean?'); +}); + +it('should fire dialogclosed for auto-dismissed dialogs', async ({ page }) => { + const closedPromise = page.waitForEvent('dialogclosed'); + await page.evaluate(() => alert('yo')); + const dialog = await closedPromise; + expect(dialog.message()).toBe('yo'); +}); + it('should allow accepting prompts @smoke', async ({ page, isElectron }) => { it.skip(isElectron, 'prompt() is not a thing in electron'); diff --git a/tests/page/page-evaluate-callback.spec.ts b/tests/page/page-evaluate-callback.spec.ts index d7157f62cd1e9..1c4edf7100f22 100644 --- a/tests/page/page-evaluate-callback.spec.ts +++ b/tests/page/page-evaluate-callback.spec.ts @@ -132,6 +132,18 @@ it('should work in a child frame', async ({ page, server }) => { expect(received).toEqual([42]); }); +it('should route callbacks back to the calling frame', async ({ page, server }) => { + await page.goto(server.EMPTY_PAGE); + const frame = await attachFrame(page, 'frame1', server.EMPTY_PAGE); + const greet = async (where: string) => `hello ${where}`; + const [fromMain, fromChild] = await Promise.all([ + page.evaluate(async ({ cb }) => await cb('main'), { cb: greet }, { exposeFunctions: true }), + frame.evaluate(async ({ cb }) => await cb('child'), { cb: greet }, { exposeFunctions: true }), + ]); + expect(fromMain).toBe('hello main'); + expect(fromChild).toBe('hello child'); +}); + it('should work with jsHandle.evaluate', async ({ page }) => { const handle = await page.evaluateHandle(() => window); const received: number[] = []; diff --git a/tests/page/page-evaluate.spec.ts b/tests/page/page-evaluate.spec.ts index fa45692fc6138..3877fc256ffe2 100644 --- a/tests/page/page-evaluate.spec.ts +++ b/tests/page/page-evaluate.spec.ts @@ -890,13 +890,3 @@ it('should ignore dangerous object keys', async ({ page }) => { const result = await page.evaluate(arg => arg, input); expect(result).toEqual({ safeKey: 'safeValue' }); }); - -it('promise collected', async ({ page, browserName }) => { - it.skip(browserName !== 'chromium', 'this is a chromium-only behavior'); - - const resultPromise = page.evaluate(() => new Promise(() => {})).catch(e => e); - for (let i = 0; i < 20; i++) - await page.requestGC(); - const error = await resultPromise; - expect(error.message).toContain('Resulting promise was garbage collected'); -}); diff --git a/tests/page/page-goto.spec.ts b/tests/page/page-goto.spec.ts index 90c0ad8930d08..c096d1529915f 100644 --- a/tests/page/page-goto.spec.ts +++ b/tests/page/page-goto.spec.ts @@ -34,6 +34,15 @@ it('should work with file URL', async ({ page, asset, isAndroid, mode, channel } expect(page.frames().length).toBe(1); }); +it('should navigate from file URL to about:blank', async ({ page, asset, isAndroid, channel }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42050' }); + it.skip(isAndroid, 'No files on Android'); + it.skip(channel === 'webkit-wsl', 'separate filesystem on wsl'); + + await page.goto(url.pathToFileURL(asset('empty.html')).href); + await page.goto('about:blank'); +}); + it('should work with file URL with subframes', async ({ page, asset, isAndroid, mode, channel }) => { it.skip(isAndroid, 'No files on Android'); it.skip(channel === 'webkit-wsl', 'separate filesystem on wsl'); diff --git a/tests/page/page-network-response.spec.ts b/tests/page/page-network-response.spec.ts index 2a8b86c5f8a79..9ec3e8eb6121d 100644 --- a/tests/page/page-network-response.spec.ts +++ b/tests/page/page-network-response.spec.ts @@ -83,6 +83,22 @@ it('should return uncompressed text for brotli encoding', { expect(await response.text()).toBe(text); }); +it('should return text for identity encoding', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42501' }, +}, async ({ page, server }) => { + const text = '
hello
'; + server.setRoute('/identity.html', (req, res) => { + res.writeHead(200, { + 'Content-Type': 'text/html; charset=utf-8', + 'Content-Encoding': 'Identity', + }); + res.end(text); + }); + const response = await page.goto(server.PREFIX + '/identity.html'); + expect(response.headers()['content-encoding']).toBe('Identity'); + expect(await response.text()).toBe(text); +}); + it('should throw when requesting body of redirected response', async ({ page, server }) => { server.setRedirect('/foo.html', '/empty.html'); const response = await page.goto(server.PREFIX + '/foo.html'); @@ -344,8 +360,9 @@ it('should report if request was fromServiceWorker', async ({ page, server, isAn } }); -it('should return body for prefetch script', async ({ page, server, browserName }) => { +it('should return body for prefetch script', async ({ page, server, browserName, browserMajorVersion }) => { it.skip(browserName === 'webkit', 'No prefetch in WebKit: https://caniuse.com/link-rel-prefetch'); + it.skip(browserName === 'chromium' && browserMajorVersion < 138, 'Requires Sec-Purpose header, shipped in Chrome 138'); const [response] = await Promise.all([ page.waitForResponse('**/prefetch.js'), page.goto(server.PREFIX + '/prefetch.html') @@ -354,6 +371,27 @@ it('should return body for prefetch script', async ({ page, server, browserName expect(body.toString()).toBe('// Scripts will be pre-fetched'); }); +it('should return body for image with evicted body', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42002' }, +}, async ({ page, server, isMac, browserName }) => { + it.fixme(isMac && browserName === 'webkit', 'WebKit on Mac evicts the body and returns empty buffer'); + const imageBase64 = 'R0lGODlhAQABAAAAACw='; // truncated 1x1 gif, Chromium evicts its body + server.setRoute('/pixel.gif', (req, res) => { + res.setHeader('content-type', 'image/gif'); + res.end(Buffer.from(imageBase64, 'base64')); + }); + server.setRoute('/page.html', (req, res) => { + res.setHeader('content-type', 'text/html'); + res.end(''); + }); + const [response] = await Promise.all([ + page.waitForResponse('**/pixel.gif'), + page.goto(server.PREFIX + '/page.html'), + ]); + const body = await response.body(); + expect(body.toString('base64')).toBe(imageBase64); +}); + it('should bypass disk cache when page interception is enabled', async ({ page, server }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30000' }); await page.goto(server.PREFIX + '/frames/one-frame.html'); diff --git a/tests/page/page-network-sizes.spec.ts b/tests/page/page-network-sizes.spec.ts index 3157b4dffb03e..bbbb3508ea453 100644 --- a/tests/page/page-network-sizes.spec.ts +++ b/tests/page/page-network-sizes.spec.ts @@ -93,8 +93,9 @@ it('should have the correct responseBodySize for chunked request', async ({ page const sizes = await response.request().sizes(); // The actual file size is 5100 bytes. The extra 75 bytes are coming from the chunked encoding headers and end bytes. if (browserName === 'webkit') - // It should be 5175 there. On the actual network response, the body has a size of 5175. - expect(sizes.responseBodySize).toBe(5173); + // WebKit on macOS reports 5173 with the legacy CFNetwork loader (builds <= 2346) and the + // correct 5175 with NWLoader. TODO: expect 5175 once the NWLoader-based build ships. + expect([5173, 5175]).toContain(sizes.responseBodySize); else expect(sizes.responseBodySize).toBe(5175); }); diff --git a/tests/page/page-request-fulfill.spec.ts b/tests/page/page-request-fulfill.spec.ts index 7e5f0f9caf8c4..d1772c9c0f767 100644 --- a/tests/page/page-request-fulfill.spec.ts +++ b/tests/page/page-request-fulfill.spec.ts @@ -17,7 +17,7 @@ import { test as base, expect } from './pageTest'; import fs from 'fs'; -import type * as har from '../../packages/trace/src/har'; +import type * as har from '../../packages/isomorphic/trace/versions/har'; import type { Route } from 'playwright-core'; const it = base.extend<{ diff --git a/tests/page/page-route.spec.ts b/tests/page/page-route.spec.ts index 9f426e588c976..7da7d1e60b93a 100644 --- a/tests/page/page-route.spec.ts +++ b/tests/page/page-route.spec.ts @@ -16,6 +16,7 @@ */ import type { Route } from 'playwright-core'; +import { chromiumVersionLessThan } from '../config/utils'; import { test as it, expect } from './pageTest'; it('should intercept @smoke', async ({ page, server }) => { @@ -273,11 +274,11 @@ it('should pause intercepted fetch request until continue', async ({ page, serve expect(status).toBe(200); }); -it('should work with custom referer headers', async ({ page, server, browserName }) => { +it('should work with custom referer headers', async ({ page, server, browserName, browserVersion }) => { await page.setExtraHTTPHeaders({ 'referer': server.EMPTY_PAGE }); await page.route('**/*', route => { // See https://github.com/microsoft/playwright/issues/8999 - if (browserName === 'chromium') + if (browserName === 'chromium' && chromiumVersionLessThan(browserVersion, '154.0.8014.0')) expect(route.request().headers()['referer']).toBe(server.EMPTY_PAGE + ', ' + server.EMPTY_PAGE); else expect(route.request().headers()['referer']).toBe(server.EMPTY_PAGE); @@ -985,6 +986,32 @@ it('should support async handler w/ times', async ({ page, server }) => { await expect(page.locator('body')).not.toHaveText('intercepted'); }); +it('route abort with times: 1 should not affect second sequential fetch', async ({ page, server, browserName }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41802' }); + it.fixme(browserName === 'chromium', 'Chromium drops a request that is intercepted while Fetch.disable is being processed; fix is not rolled yet'); + + server.setRoute('/data', (req, res) => { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('ok'); + }); + + await page.goto(server.EMPTY_PAGE); + await page.route('**/data', route => route.abort('timedout'), { times: 1 }); + + const results = await page.evaluate(async () => { + async function fetchOrHung(url: string) { + const timeout = new Promise(resolve => setTimeout(() => resolve('hung'), 3000)); + const request = fetch(url).then(r => String(r.status)).catch(() => 'aborted'); + return Promise.race([request, timeout]); + } + const first = await fetchOrHung('/data'); + const second = await fetchOrHung('/data'); + return [first, second]; + }); + + expect(results).toEqual(['aborted', '200']); +}); + it('should contain raw request header', async ({ page, server }) => { let headers: any; await page.route('**/*', async route => { diff --git a/tests/page/page-set-extra-http-headers.spec.ts b/tests/page/page-set-extra-http-headers.spec.ts index 284621edd61ac..f9b9b4984dc5e 100644 --- a/tests/page/page-set-extra-http-headers.spec.ts +++ b/tests/page/page-set-extra-http-headers.spec.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { chromiumVersionLessThan } from '../config/utils'; import { test as it, expect } from './pageTest'; it('should work @smoke', async ({ page, server }) => { @@ -61,8 +62,8 @@ it('should throw for non-string header values', async ({ page }) => { expect(error2.message).toContain('Expected value of header "foo" to be String, but "boolean" is found.'); }); -it('should not duplicate referer header', async ({ page, server, browserName }) => { - it.fail(browserName === 'chromium', 'Request has referer and Referer'); +it('should not duplicate referer header', async ({ page, server, browserName, browserVersion }) => { + it.fail(browserName === 'chromium' && chromiumVersionLessThan(browserVersion, '154.0.8014.0'), 'Request has referer and Referer'); await page.setExtraHTTPHeaders({ 'referer': server.EMPTY_PAGE }); const response = await page.goto(server.EMPTY_PAGE); expect(response.ok()).toBe(true); diff --git a/tests/page/page-set-input-files.spec.ts b/tests/page/page-set-input-files.spec.ts index b5085c712e148..74fb5aa46beec 100644 --- a/tests/page/page-set-input-files.spec.ts +++ b/tests/page/page-set-input-files.spec.ts @@ -436,5 +436,5 @@ test('should preserve lastModified timestamp', async ({ page, asset }) => { // On Linux browser sometimes reduces the timestamp by 1ms: 1696272058110.0715 -> 1696272058109 or even // rounds it to seconds in WebKit: 1696272058110 -> 1696272058000. for (let i = 0; i < timestamps.length; i++) - expect(Math.abs(timestamps[i] - expectedTimestamps[i]), `expected: ${expectedTimestamps}; actual: ${timestamps}`).toBeLessThan(1000); + expect(Math.abs(timestamps[i] - expectedTimestamps[i]), `expected: ${expectedTimestamps}; actual: ${timestamps}`).toBeLessThanOrEqual(1000); }); diff --git a/tests/page/selectors-frame.spec.ts b/tests/page/selectors-frame.spec.ts index 3ccfb6acd14af..62d5899a9fa6c 100644 --- a/tests/page/selectors-frame.spec.ts +++ b/tests/page/selectors-frame.spec.ts @@ -40,7 +40,7 @@ async function routeIframe(page: Page) { }); await page.route('**/iframe-2.html', route => { route.fulfill({ - body: '', + body: '', contentType: 'text/html' }).catch(() => {}); }); @@ -310,3 +310,96 @@ it('should non work for non-frame', async ({ page, server }) => { expect(error.message).toContain('
'); expect(error.message).toContain('', contentType: 'text/html' }).catch(() => {}); + }); + await page.route('**/a.html', route => { + route.fulfill({ body: '
one
', contentType: 'text/html' }).catch(() => {}); + }); + await page.route('**/b.html', route => { + route.fulfill({ body: 'twothree', contentType: 'text/html' }).catch(() => {}); + }); + await page.goto(server.EMPTY_PAGE); + + const texts = await page.$$eval('internal:control=any-frame >> span', els => els.map(e => e.textContent)); + expect(texts).toEqual(['two', 'three']); +}); + +it('should throw when matching elements in multiple frames', async ({ page, server }) => { + await page.route('**/empty.html', route => { + route.fulfill({ body: '', contentType: 'text/html' }).catch(() => {}); + }); + await page.route('**/a.html', route => { + route.fulfill({ body: '
one
', contentType: 'text/html' }).catch(() => {}); + }); + await page.route('**/b.html', route => { + route.fulfill({ body: '
two
', contentType: 'text/html' }).catch(() => {}); + }); + await page.goto(server.EMPTY_PAGE); + + // Make sure both child frames have their
before matching, otherwise resolution + // may transiently collapse to a single frame. + await expect.poll(() => page.frames().length).toBe(3); + for (const frame of page.frames()) { + if (frame !== page.mainFrame()) + await frame.waitForSelector('div'); + } + + const error = await page.locator('internal:control=any-frame >> div').innerHTML().catch(e => e); + expect(error.message).toContain('frameLocator() matched elements in multiple frames'); +}); + +it('should not allow any-frame in the middle of a selector', async ({ page, server }) => { + await routeIframe(page); + await page.goto(server.EMPTY_PAGE); + const error = await page.locator('iframe >> internal:control=any-frame >> div').waitFor().catch(e => e); + expect(error.message).toContain('"any-frame" is only allowed as the first selector token'); +}); + +it('should allow entering frames from any frame', async ({ page, server }) => { + await routeIframe(page); + await page.goto(server.EMPTY_PAGE); + const button = page.locator('internal:control=any-frame >> iframe[src="iframe-2.html"] >> internal:control=enter-frame >> button'); + await button.waitFor(); + expect(await button.innerText()).toBe('Hello nested iframe'); +}); + +it('should not allow any-frame after entering a frame', async ({ page }) => { + const error = await page.locator('iframe >> internal:control=enter-frame >> internal:control=any-frame >> button').count().catch(e => e); + expect(error.message).toContain('"any-frame" is only allowed as the first selector token'); +}); + +it('should not allow dangling enter-frame after any-frame', async ({ page }) => { + const error = await page.locator('internal:control=any-frame >> iframe >> internal:control=enter-frame').count().catch(e => e); + expect(error.message).toContain('Selector cannot end with entering frame'); +}); diff --git a/tests/playwright-test/config.spec.ts b/tests/playwright-test/config.spec.ts index 9c4acda6e445a..58e69363e2ed1 100644 --- a/tests/playwright-test/config.spec.ts +++ b/tests/playwright-test/config.spec.ts @@ -740,44 +740,6 @@ test('should merge projects in the config', async ({ runInlineTest }) => { expect(result.exitCode).toBe(0); }); -test('should merge ct configs', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': ` - import { defineConfig, expect } from '@playwright/experimental-ct-react'; - const baseConfig = defineConfig({ - timeout: 10, - use: { - foo: 1, - }, - }); - const derivedConfig = defineConfig(baseConfig, { - grep: 'hi', - use: { - bar: 2, - }, - }); - - // Make sure ct-specific properties are preserved - // and config properties are merged. - expect(derivedConfig).toEqual(expect.objectContaining({ - use: { foo: 1, bar: 2 }, - grep: 'hi', - '@playwright/test': expect.objectContaining({ - babelPlugins: [[expect.stringContaining('tsxTransform.js')]] - }), - '@playwright/experimental-ct-core': expect.objectContaining({ - registerSourceFile: expect.stringContaining('registerSource'), - }), - })); - `, - 'a.test.ts': ` - import { test } from '@playwright/experimental-ct-react'; - test('pass', async ({}) => {}); - ` - }); - expect(result.exitCode).toBe(0); -}); - test('should throw on invalid config.tsconfig option', async ({ runInlineTest }) => { const result = await runInlineTest({ 'playwright.config.ts': ` diff --git a/tests/playwright-test/esm.spec.ts b/tests/playwright-test/esm.spec.ts index 0e1deb9585917..5f4022b466410 100644 --- a/tests/playwright-test/esm.spec.ts +++ b/tests/playwright-test/esm.spec.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { test, expect, playwrightCtConfigText } from './playwright-test-fixtures'; +import { test, expect } from './playwright-test-fixtures'; test('should load nested as esm when package.json has type module', async ({ runInlineTest }) => { const result = await runInlineTest({ @@ -542,29 +542,6 @@ test('should resolve no-extension import to .jsx file in ESM mode', async ({ run expect(result.exitCode).toBe(0); }); -test('should resolve .js import to .tsx file in ESM mode for components', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'package.json': `{ "type": "module" }`, - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - - 'src/button.tsx': ` - export const Button = () => ; - `, - - 'src/test.spec.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button.js'; - test('pass', async ({ mount }) => { - await mount(); - }); - `, - }, { workers: 1 }); - expect(result.passed).toBe(1); - expect(result.exitCode).toBe(0); -}); - test('should load cjs config and test in non-ESM mode', async ({ runInlineTest }) => { const result = await runInlineTest({ 'package.json': `{ "type": "module" }`, diff --git a/tests/playwright-test/exit-code.spec.ts b/tests/playwright-test/exit-code.spec.ts index 82b4f0d7db60b..44d6238930aec 100644 --- a/tests/playwright-test/exit-code.spec.ts +++ b/tests/playwright-test/exit-code.spec.ts @@ -230,3 +230,24 @@ test('should force-kill a worker that does not exit on stop', async ({ runInline // Should complete well within a minute thanks to the watchdog. expect(monotonicTime() - now).toBeLessThan(60000); }); + +test('should not force-kill a worker that is running a slow fixture teardown', async ({ runInlineTest }) => { + test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42007' }); + const result = await runInlineTest({ + 'a.spec.ts': ` + import { test as base, expect } from '@playwright/test'; + const test = base.extend<{}, { slowTeardown: void }>({ + slowTeardown: [async ({}, use) => { + await use(); + await new Promise(f => setTimeout(f, 4000)); + console.log('slow teardown finished'); + }, { scope: 'worker', timeout: 0 }], + }); + test('passes', async ({ slowTeardown }) => {}); + `, + }, undefined, { PWTEST_CHILD_PROCESS_TIMEOUT: '2000' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + expect(result.output).toContain('slow teardown finished'); + expect(result.output).not.toContain('force-killed'); +}); diff --git a/tests/playwright-test/loader.spec.ts b/tests/playwright-test/loader.spec.ts index 75e7d85267409..cca5bbf037d20 100644 --- a/tests/playwright-test/loader.spec.ts +++ b/tests/playwright-test/loader.spec.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { test, expect, playwrightCtConfigText } from './playwright-test-fixtures'; +import { test, expect } from './playwright-test-fixtures'; import fs from 'fs'; import path from 'path'; import url from 'url'; @@ -756,28 +756,6 @@ test('should resolve .js import to .tsx file in non-ESM mode', async ({ runInlin expect(result.exitCode).toBe(0); }); -test('should resolve .js import to .tsx file in non-ESM mode for components', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - - 'src/button.tsx': ` - export const Button = () => ; - `, - - 'src/test.spec.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button.js'; - test('pass', async ({ mount }) => { - await mount(); - }); - `, - }, { workers: 1 }); - expect(result.passed).toBe(1); - expect(result.exitCode).toBe(0); -}); - test('should import export assignment from ts', async ({ runInlineTest }) => { const result = await runInlineTest({ 'a.test.ts': ` diff --git a/tests/playwright-test/only-changed.spec.ts b/tests/playwright-test/only-changed.spec.ts index 13bb434670d6c..7bc7fc645c0f9 100644 --- a/tests/playwright-test/only-changed.spec.ts +++ b/tests/playwright-test/only-changed.spec.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { test, expect, playwrightCtConfigText } from './playwright-test-fixtures'; +import { test, expect } from './playwright-test-fixtures'; test.slow(); @@ -166,91 +166,6 @@ test('should throw nice error message if git doesnt work', async ({ runInlineTes expect(result.output, 'contains git command output').toContain('unknown revision or path not in the working tree'); }); -test('should support component tests', async ({ runInlineTest, git, writeFiles }) => { - await writeFiles({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ` - `, - 'src/contents.ts': ` - export const content = "Button"; - `, - 'src/button.tsx': ` - import {content} from './contents'; - export const Button = () => ; - `, - 'src/helper.ts': ` - export { Button } from "./button"; - `, - 'src/button.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './helper'; - - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button', { timeout: 1000 }); - }); - `, - 'src/button2.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './helper'; - - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button', { timeout: 1000 }); - }); - `, - 'src/button3.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - - test('pass', async ({ mount }) => { - const component = await mount(

Hello World

); - await expect(component).toHaveText('Hello World'); - }); - `, - }); - - git(`add .`); - git(`commit -m "init"`); - - const result = await runInlineTest({}, { 'only-changed': true }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(0); - expect(result.failed).toBe(0); - - const result2 = await runInlineTest({ - 'src/button2.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './helper'; - - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Different Button', { timeout: 1000 }); - }); - ` - }, { 'only-changed': true }); - - expect(result2.exitCode).toBe(1); - expect(result2.failed).toBe(1); - expect(result2.passed).toBe(0); - expect(result2.output).toContain('button2.test.tsx'); - expect(result2.output).not.toContain('button.test.tsx'); - expect(result2.output).not.toContain('button3.test.tsx'); - - git(`commit -am "update button2 test"`); - - const result3 = await runInlineTest({ - 'src/contents.ts': ` - export const content = 'Changed Content'; - ` - }, { 'only-changed': true }); - - expect(result3.exitCode).toBe(1); - expect(result3.failed).toBe(2); - expect(result3.passed).toBe(0); -}); - test.describe('should work the same if being called in subdirectory', () => { test('tracked file', async ({ runInlineTest, git, writeFiles }) => { await writeFiles({ diff --git a/tests/playwright-test/playwright-test-fixtures.ts b/tests/playwright-test/playwright-test-fixtures.ts index 28ef919e29f1f..93ab2ecd79691 100644 --- a/tests/playwright-test/playwright-test-fixtures.ts +++ b/tests/playwright-test/playwright-test-fixtures.ts @@ -464,16 +464,6 @@ export function parseTestRunnerOutput(output: string) { }; } -export const playwrightCtConfigText = ` -import { defineConfig } from '@playwright/experimental-ct-react'; -export default defineConfig({ - use: { - ctPort: ${3200 + (+process.env.TEST_PARALLEL_INDEX)} - }, - projects: [{name: 'default'}], -}); -`; - export async function removeFolders(dirs: string[]): Promise { return await Promise.all(dirs.map((dir: string) => fs.promises.rm(dir, { recursive: true, force: true, maxRetries: 10 }).catch(e => e) diff --git a/tests/playwright-test/playwright.config.spec.ts b/tests/playwright-test/playwright.config.spec.ts index a168f8983b7a7..025a160e34c02 100644 --- a/tests/playwright-test/playwright.config.spec.ts +++ b/tests/playwright-test/playwright.config.spec.ts @@ -127,8 +127,10 @@ test('should override contextOptions', async ({ runInlineTest }) => { acceptDownloads: false, bypassCSP: true, colorScheme: 'dark', + contrast: 'more', deviceScaleFactor: 2, extraHTTPHeaders: {'foo': 'bar'}, + forcedColors: 'active', hasTouch: true, ignoreHTTPSErrors: true, isMobile: true, @@ -136,6 +138,7 @@ test('should override contextOptions', async ({ runInlineTest }) => { locale: 'fr-FR', offline: true, permissions: ['geolocation'], + reducedMotion: 'reduce', timezoneId: 'TIMEZONE', userAgent: 'UA', viewport: null, @@ -143,8 +146,10 @@ test('should override contextOptions', async ({ runInlineTest }) => { acceptDownloads: true, bypassCSP: false, colorScheme: 'light', + contrast: 'no-preference', deviceScaleFactor: 1, extraHTTPHeaders: {'foo': 'bar2'}, + forcedColors: 'none', hasTouch: false, ignoreHTTPSErrors: false, isMobile: false, @@ -152,6 +157,7 @@ test('should override contextOptions', async ({ runInlineTest }) => { locale: 'en-US', offline: false, permissions: [], + reducedMotion: 'no-preference', timezoneId: 'TIMEZONE 2', userAgent: 'UA 2', viewport: { width: 500, height: 500 } @@ -161,12 +167,14 @@ test('should override contextOptions', async ({ runInlineTest }) => { `, 'a.test.ts': ` import { test, expect } from '@playwright/test'; - test('pass', async ({ acceptDownloads, bypassCSP, colorScheme, deviceScaleFactor, extraHTTPHeaders, hasTouch, ignoreHTTPSErrors, isMobile, javaScriptEnabled, locale, offline, permissions, timezoneId, userAgent, viewport }) => { + test('pass', async ({ acceptDownloads, bypassCSP, colorScheme, contrast, deviceScaleFactor, extraHTTPHeaders, forcedColors, hasTouch, ignoreHTTPSErrors, isMobile, javaScriptEnabled, locale, offline, permissions, reducedMotion, timezoneId, userAgent, viewport }) => { expect.soft(acceptDownloads).toBe(false); expect.soft(bypassCSP).toBe(true); expect.soft(colorScheme).toBe('dark'); + expect.soft(contrast).toBe('more'); expect.soft(deviceScaleFactor).toBe(2); expect.soft(extraHTTPHeaders).toEqual({'foo': 'bar'}); + expect.soft(forcedColors).toBe('active'); expect.soft(hasTouch).toBe(true); expect.soft(ignoreHTTPSErrors).toBe(true); expect.soft(isMobile).toBe(true); @@ -174,6 +182,7 @@ test('should override contextOptions', async ({ runInlineTest }) => { expect.soft(locale).toBe('fr-FR'); expect.soft(offline).toBe(true); expect.soft(permissions).toEqual(['geolocation']); + expect.soft(reducedMotion).toBe('reduce'); expect.soft(timezoneId).toBe('TIMEZONE'); expect.soft(userAgent).toBe('UA'); expect.soft(viewport).toBe(null); diff --git a/tests/playwright-test/playwright.connect.spec.ts b/tests/playwright-test/playwright.connect.spec.ts index 71aa32b0cc83a..b31e15edfd595 100644 --- a/tests/playwright-test/playwright.connect.spec.ts +++ b/tests/playwright-test/playwright.connect.spec.ts @@ -210,9 +210,9 @@ test('should record trace', async ({ runInlineTest }) => { expect(result.passed).toBe(1); expect(result.failed).toBe(1); - // A single tracing artifact should be created. We see it in the logs twice: + // One tracing artifact should be created for each tracing stream. We see each in the logs twice: // as a regular message and wrapped inside a jsonPipe. - expect(countTimes(result.output, `"type":"Artifact","initializer"`)).toBe(2); + expect(countTimes(result.output, `"type":"Artifact","initializer"`)).toBe(4); expect(fs.existsSync(test.info().outputPath('test-results', 'a-pass', 'trace.zip'))).toBe(false); diff --git a/tests/playwright-test/playwright.ct-build.spec.ts b/tests/playwright-test/playwright.ct-build.spec.ts deleted file mode 100644 index 0b2229b54f593..0000000000000 --- a/tests/playwright-test/playwright.ct-build.spec.ts +++ /dev/null @@ -1,797 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs'; -import path from 'path'; -import { expect, playwrightCtConfigText, test } from './playwright-test-fixtures'; - -test.describe.configure({ mode: 'parallel' }); - -test('should work with the empty component list', async ({ runInlineTest }, testInfo) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.js': ``, - - 'a.test.ts': ` - import { test, expect } from '@playwright/experimental-ct-react'; - test('pass', async ({ mount }) => {}); - `, - }, { workers: 1 }); - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); - const output = result.output; - expect(output).toContain('transforming...'); - expect(output.replace(/\\+/g, '/')).toContain('.cache/index.html'); - - const metainfo = JSON.parse(fs.readFileSync(testInfo.outputPath('playwright/.cache/metainfo.json'), 'utf-8')); - expect(metainfo.version).toEqual(require('playwright-core/package.json').version); - expect(metainfo.viteVersion).toEqual(require('vite/package.json').version); - expect(Object.entries(metainfo.deps)).toHaveLength(0); - expect(Object.entries(metainfo.sources)).toHaveLength(14); -}); - -test('should extract component list', async ({ runInlineTest }, testInfo) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - - 'src/button.tsx': ` - export const Button = () => ; - `, - - 'src/components.tsx': ` - export const Component1 = () =>
Component 1
; - export const Component2 = () =>
Component 2
; - `, - - 'src/defaultExport.tsx': ` - export default () =>
Default export
; - `, - - 'src/clashingNames1.tsx': ` - export const ClashingName = () =>
Clashing name 1
; - `, - - 'src/clashingNames2.tsx': ` - export const ClashingName = () =>
Clashing name 2
; - `, - - 'src/one-import.spec.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button'; - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button'); - }); - `, - - 'src/named-imports.spec.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Component1, Component2 } from './components'; - - test('pass 1', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Component 1'); - }); - - test('pass 2', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Component 2'); - }); - `, - - 'src/default-import.spec.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import DefaultComponent from './defaultExport'; - - test('named', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Default export'); - }); - `, - - 'src/clashing-imports.spec.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - - import DefaultComponent from './defaultExport.tsx'; - import { ClashingName as CN1 } from './clashingNames1'; - import { ClashingName as CN2 } from './clashingNames2'; - - test('named', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Clashing name 1'); - }); - - test('pass 2', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Clashing name 2'); - }); - `, - 'src/relative-import-different-folders/one/index.tsx': ` - export default () => ; - `, - 'src/relative-import-different-folders/one/one.spec.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import Button from '.'; - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button'); - }); - `, - 'src/relative-import-different-folders/two/index.tsx': ` - export default () => ; - `, - 'src/relative-import-different-folders/two/two.spec.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import Button from '.'; - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button'); - }); - `, - }, { workers: 1 }); - expect(result.exitCode).toBe(0); - - const metainfo = JSON.parse(fs.readFileSync(testInfo.outputPath('playwright/.cache/metainfo.json'), 'utf-8')); - metainfo.components.sort((a, b) => { - return (a.importSource + '/' + a.importedName).localeCompare(b.importSource + '/' + b.importedName); - }); - - expect(metainfo.components).toEqual([{ - id: expect.stringContaining('button_Button'), - remoteName: 'Button', - importSource: expect.stringContaining('./button'), - filename: expect.stringContaining('one-import.spec.tsx'), - }, { - id: expect.stringContaining('clashingNames1_ClashingName'), - remoteName: 'ClashingName', - importSource: expect.stringContaining('./clashingNames1'), - filename: expect.stringContaining('clashing-imports.spec.tsx'), - }, { - id: expect.stringContaining('clashingNames2_ClashingName'), - remoteName: 'ClashingName', - importSource: expect.stringContaining('./clashingNames2'), - filename: expect.stringContaining('clashing-imports.spec.tsx'), - }, { - id: expect.stringContaining('components_Component1'), - remoteName: 'Component1', - importSource: expect.stringContaining('./components'), - filename: expect.stringContaining('named-imports.spec.tsx'), - }, { - id: expect.stringContaining('components_Component2'), - remoteName: 'Component2', - importSource: expect.stringContaining('./components'), - filename: expect.stringContaining('named-imports.spec.tsx'), - }, { - id: expect.stringContaining('defaultExport'), - importSource: expect.stringContaining('./defaultExport'), - filename: expect.stringContaining('default-import.spec.tsx'), - }, { - id: expect.stringContaining('_one'), - importSource: expect.stringContaining('.'), - filename: expect.stringContaining(`one${path.sep}one.spec.tsx`), - }, { - id: expect.stringContaining('_two'), - importSource: expect.stringContaining('.'), - filename: expect.stringContaining(`two${path.sep}two.spec.tsx`), - }]); - - for (const [, value] of Object.entries(metainfo.deps)) - (value as string[]).sort(); - - expect(Object.entries(metainfo.deps)).toEqual([ - [expect.stringContaining('clashingNames1.tsx'), [ - expect.stringContaining('clashingNames1.tsx'), - ]], - [expect.stringContaining('clashingNames2.tsx'), [ - expect.stringContaining('clashingNames2.tsx'), - ]], - [expect.stringContaining('defaultExport.tsx'), [ - expect.stringContaining('defaultExport.tsx'), - ]], - [expect.stringContaining('components.tsx'), [ - expect.stringContaining('components.tsx'), - ]], - [expect.stringContaining('button.tsx'), [ - expect.stringContaining('button.tsx'), - ]], - [expect.stringContaining(`one${path.sep}index.tsx`), [ - expect.stringContaining(`one${path.sep}index.tsx`), - ]], - [expect.stringContaining(`two${path.sep}index.tsx`), [ - expect.stringContaining(`two${path.sep}index.tsx`), - ]], - ]); -}); - -test('should cache build', async ({ runInlineTest }, testInfo) => { - test.slow(); - - await test.step('original test', async () => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - - 'src/button.tsx': ` - export const Button = () => ; - `, - - 'src/button.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button.tsx'; - - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button'); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); - const output = result.output; - expect(output, 'should rebuild bundle').toContain('modules transformed'); - }); - - await test.step('re-run same test', async () => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - }, { workers: 1 }); - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); - const output = result.output; - expect(output, 'should not rebuild bundle').not.toContain('modules transformed'); - }); - - await test.step('modify test', async () => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'src/button.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button.tsx'; - - test('pass updated', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button 2', { timeout: 200 }); - }); - `, - }, { workers: 1 }); - expect(result.exitCode).toBe(1); - expect(result.passed).toBe(0); - const output = result.output; - expect(output, 'should not rebuild bundle').not.toContain('modules transformed'); - }); - - await test.step('modify source', async () => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'src/button.tsx': ` - export const Button = () => ; - `, - }, { workers: 1 }); - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); - const output = result.output; - expect(output, 'should rebuild bundle').toContain('modules transformed'); - }); -}); - -test('should grow cache', async ({ runInlineTest }, testInfo) => { - test.slow(); - - await test.step('original test', async () => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - 'src/button1.tsx': ` - export const Button1 = () => ; - `, - 'src/button2.tsx': ` - export const Button2 = () => ; - `, - 'src/button1.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button1 } from './button1.tsx'; - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button 1'); - }); - `, - 'src/button2.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button2 } from './button2.tsx'; - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button 2'); - }); - `, - }, { workers: 1 }, undefined, { additionalArgs: ['button1'] }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); - const output = result.output; - expect(output).toContain('modules transformed'); - }); - - await test.step('run second test', async () => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - }, { workers: 1 }, undefined, { additionalArgs: ['button2'] }); - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); - const output = result.output; - expect(output).toContain('modules transformed'); - }); - - await test.step('run first test again', async () => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - }, { workers: 1 }, undefined, { additionalArgs: ['button2'] }); - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); - const output = result.output; - expect(output).not.toContain('modules transformed'); - }); -}); - -test('should not crash when cached component test file is deleted', async ({ runInlineTest }, testInfo) => { - - await test.step('run first test to build the cache', async () => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - 'src/button.tsx': ` - export const Button = () => ; - `, - 'src/button.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button.tsx'; - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button'); - }); - `, - 'src/button2.tsx': ` - export const Button2 = () => ; - `, - 'src/button2.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button2 } from './button2.tsx'; - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button 2'); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(2); - - }); - - await test.step('remove the second test and component and run the tests again', async () => { - - fs.unlinkSync(testInfo.outputPath('src/button2.tsx')); - fs.unlinkSync(testInfo.outputPath('src/button2.test.tsx')); - - const result2 = await runInlineTest({}, { workers: 1 }); - - expect(result2.exitCode).toBe(0); - expect(result2.passed).toBe(1); - }); - -}); - -test('should not use global config for preview', async ({ runInlineTest }) => { - const result1 = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.js': ``, - 'vite.config.js': ` - export default { - plugins: [{ - configurePreviewServer: () => { - throw new Error('Original preview throws'); - } - }] - }; - `, - 'a.test.ts': ` - import { test, expect } from '@playwright/experimental-ct-react'; - test('pass', async ({ mount }) => {}); - `, - }, { workers: 1 }); - expect(result1.exitCode).toBe(0); - expect(result1.passed).toBe(1); - - const result2 = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - }, { workers: 1 }); - expect(result2.exitCode).toBe(0); - expect(result2.passed).toBe(1); -}); - -test('should work with https enabled', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright/index.html': ``, - 'playwright/index.js': ``, - 'playwright.config.js': ` - import { defineConfig } from '@playwright/experimental-ct-react'; - import basicSsl from '@vitejs/plugin-basic-ssl'; - export default defineConfig({ - use: { - ignoreHTTPSErrors: true, - ctViteConfig: { - plugins: [basicSsl()], - preview: { - https: true - } - } - }, - }); - `, - 'http.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - - test('pass', async ({ page }) => { - await expect(page).toHaveURL(/https:.*/); - }); - `, - }, { workers: 1 }); - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); -}); - -test('list compilation cache should not clash with the run one', async ({ runInlineTest }) => { - const listResult = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - 'src/button.tsx': ` - export const Button = () => ; - `, - 'src/button.spec.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button'; - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button'); - }); - `, - }, { workers: 1 }, {}, { additionalArgs: ['--list'] }); - expect(listResult.exitCode).toBe(0); - expect(listResult.passed).toBe(0); - - const runResult = await runInlineTest({}, { workers: 1 }); - expect(runResult.exitCode).toBe(0); - expect(runResult.passed).toBe(1); -}); - -test('should retain deps when test changes', async ({ runInlineTest }, testInfo) => { - test.slow(); - - await test.step('original test', async () => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - 'src/button.tsx': ` - export const Button = () => ; - `, - 'src/button.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button.tsx'; - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button'); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); - const output = result.output; - expect(output).toContain('modules transformed'); - }); - - await test.step('modify test and run it again', async () => { - const result = await runInlineTest({ - 'src/button.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button.tsx'; - test('pass', async ({ mount }) => { - const component1 = await mount(); - await expect(component1).toHaveText('Button'); - }); - `, - }, { workers: 1 }); - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); - const output = result.output; - expect(output).not.toContain('modules transformed'); - }); - - const metainfo = JSON.parse(fs.readFileSync(testInfo.outputPath('playwright/.cache/metainfo.json'), 'utf-8')); - - expect(metainfo.components).toEqual([{ - id: expect.stringContaining('button_tsx_Button'), - remoteName: 'Button', - importSource: expect.stringContaining('button.tsx'), - filename: expect.stringContaining('button.test.tsx'), - }]); - - for (const [, value] of Object.entries(metainfo.deps)) - (value as string[]).sort(); - - expect(Object.entries(metainfo.deps)).toEqual([ - [ - expect.stringContaining('button.tsx'), - [ - expect.stringContaining('button.tsx'), - ], - ] - ]); -}); - -test('should render component via re-export', async ({ runInlineTest }, testInfo) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - 'src/button.tsx': ` - export const Button = () => ; - `, - 'src/buttonHelper.ts': ` - import { Button } from './button.tsx'; - export { Button }; - `, - 'src/button.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './buttonHelper'; - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button'); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); -}); - -test('should import json', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - 'src/some.json': `{ "some": "value" }`, - 'src/button.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import json from './some.json'; - test('pass', async ({}) => { - expect(json.some).toBe('value'); - }); - `, - }, { workers: 1 }); - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); -}); - -test('should render component exported via fixture', async ({ runInlineTest }, testInfo) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - 'src/button.tsx': ` - export const Button = () => ; - `, - 'src/buttonFixture.tsx': ` - import { Button } from './button'; - import { test as baseTest } from '@playwright/experimental-ct-react'; - export { expect } from '@playwright/experimental-ct-react'; - export const test = baseTest.extend({ - button: async ({ mount }, use) => { - await use(await mount()); - } - }); - `, - 'src/button.test.tsx': ` - import { test, expect } from './buttonFixture'; - test('pass', async ({ button }) => { - await expect(button).toHaveText('Button'); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); -}); - -test('should pass imported images from test to component', async ({ runInlineTest }, testInfo) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - 'src/image.png': Buffer.from('iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAQAAAD9CzEMAAACMElEQVRYw+1XT0tCQRD/9Qci0Cw7mp1C6BMYnt5niMhPEEFCh07evNk54XnuGkhFehA/QxHkqYMEFWXpscMTipri7fqeu+vbfY+EoBkQ3Zn5zTo7MzsL/NNfoClkUUQNN3jCJ/ETfavRSpYkkSmFQzz8wMr4gaSp8OBJ2HCU4Iwd0kqGgd9GPxCccZ+0jWgWVW1wxlWy0qR51I3hv7lOllq7b4SC/+aGzr+QBadjEKgAykvzJGXwr/Lj4JfRk5hUSLKIa00HPUJRki0xeMWSWxVXmi5sddXKymqTyxdwquXAUVV3WREeLx3gTcNFWQY/jXtB8QIzgt4qTvAR4OCe0ATKCmrnmFMEM0Pp2BvrIisaFUdUjgKKZgYWSjjDLR5J+x13lATHuHSti6JBzQP+gq2QHXjfRaiJojbPgYqbmGFow0VpiyIW0/VIF9QKLzeBWA2MHmwCu8QJQV++Ps/joHQQH4HpuO0uobUeVztgIcr4Vnf4we9orWfUIWKHbEVyYKkPmaVpIVKICuo0ZYXWjHTITXWhsVYxkIDpUoKsla1i2Oz2QjvYG9fshu36GbFQ8DGyHNOuvRdOKZSDUtCFM7wyHeSM4XN8e7bOpd9F2gg+TRYal753bGkbuEjzMg0YW/yDV1czUDm+e43Byz86OnRwsYDMKXlmkYbeAOwffrtU/nGpXpwkXfPhVza+D9AiMAtrtOMYfVr0q8Wr1nh8n8ADZCJPqAk8AifyjP2n36cvkA6/Wln9MokAAAAASUVORK5CYII=', 'base64'), - 'src/image.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import imageSrc from './image.png'; - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveJSProperty('naturalWidth', 48); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); -}); - -test('should pass dates, regex, urls and bigints', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - 'src/button.tsx': ` - export const Button = ({ props }: any) => { - const { date, url, bigint, regex } = props; - const types = [ - date instanceof Date, - url instanceof URL, - typeof bigint === 'bigint', - regex instanceof RegExp, - ]; - return
{types.join(' ')}
; - }; - `, - 'src/component.spec.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button'; - - test('renders props with builtin types', async ({ mount, page }) => { - const component = await mount(; - `, - 'button2.tsx': ` - export const Button2 = () => ; - `, - 'button3.tsx': ` - export const Button3 = () => ; - `, - 'button.story.tsx': ` - import { Button as Button1 } from './button'; - import { Button } from './button'; - import { Button as ButtonA } from './button?a'; - import { Button as ButtonB } from './button#b'; - import { Button2 } from './button2?c'; - import { Button3 } from './button3#d'; - - export const ButtonStory = () => ( -
-
- ) - `, - 'a.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { ButtonStory } from './button.story.tsx'; - - test('Button', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('ButtonButton2ButtonButton3Button'); - }); - `, - }, { workers: 1 }); - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); - const metainfo = JSON.parse(fs.readFileSync(testInfo.outputPath('playwright/.cache/metainfo.json'), 'utf-8')); - - for (const [, value] of Object.entries(metainfo.deps)) - (value as string[]).sort(); - - expect(Object.entries(metainfo.deps)).toEqual([ - [expect.stringContaining('button.story.tsx'), [ - expect.stringContaining('button.story.tsx'), - expect.stringContaining('button.tsx'), - expect.stringContaining('button2.tsx'), - expect.stringContaining('button3.tsx'), - ]], - ]); -}); diff --git a/tests/playwright-test/playwright.ct-react.spec.ts b/tests/playwright-test/playwright.ct-react.spec.ts deleted file mode 100644 index dec391284b946..0000000000000 --- a/tests/playwright-test/playwright.ct-react.spec.ts +++ /dev/null @@ -1,655 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect, playwrightCtConfigText } from './playwright-test-fixtures'; - -test('should work with TSX', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ` - `, - 'src/button.tsx': ` - export const Button = () => ; - `, - 'src/button.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button'; - - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button'); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); -}); - -test('should work with JSX', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.js': ` - `, - - 'src/button.jsx': ` - export const Button = () => ; - `, - - 'src/button.test.jsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button'; - - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button'); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); -}); - -test('should work with JSX in JS', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.js': ` - `, - - 'src/button.js': ` - export const Button = () => ; - `, - - 'src/button.test.jsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button'; - - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button'); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); -}); - -test('should work with JSX in JS and in JSX', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.js': ` - `, - - 'src/button.js': ` - export const Button = () => ; - `, - - 'src/list.jsx': ` - export const List = () =>
  • List
; - `, - - 'src/button.test.jsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button'; - import { List } from './list'; - - test('pass button', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button'); - }); - - test('pass list', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('List'); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(2); -}); - - -test('should work with stray TSX import', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ` - `, - - 'src/button.tsx': ` - export const Button = () => ; - `, - - 'src/list.tsx': ` - export const List = () =>
  • List
; - `, - - 'src/button.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button'; - import { List } from './list'; - - test('pass button', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button'); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); -}); - -test('should work with stray JSX import', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.js': ` - `, - - 'src/button.jsx': ` - export const Button = () => ; - `, - - 'src/list.jsx': ` - export const List = () =>
  • List
; - `, - - 'src/button.test.jsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button'; - import { List } from './list'; - - test('pass button', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button'); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); -}); - -test('should work with stray JS import', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.js': ` - `, - - 'src/button.js': ` - export const Button = () => ; - `, - - 'src/list.js': ` - export const List = () =>
  • List
; - `, - - 'src/button.test.jsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button'; - import { List } from './list'; - - test('pass button', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button'); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); -}); - -test('should work with JSX in variable', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.js': ` - `, - - 'src/button.jsx': ` - export const Button = () => ; - `, - - 'src/button.test.jsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button'; - - const button = ; - - test('pass button', async ({ mount }) => { - const component = await mount(button); - await expect(component).toHaveText('Button'); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); -}); - -test('should pass "key" attribute from JSX in variable', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.js': ` - `, - - 'src/container.jsx': ` - import { useState } from 'react'; - export function Container({ children }) { - const [index, setIndex] = useState(0); - return ( -
setIndex((index + 1) % children.length)}> - {children[index]} -
- ); - } - `, - - 'src/button.jsx': ` - import { useState } from 'react'; - export function Button({ value }) { - const [state, setState] = useState(value); - return ; - } - `, - - 'src/index.test.jsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button'; - import { Container } from './container'; - - test('key should tear down and recreate component', async ({ mount }) => { - const component = await mount( - - ; - `, - - 'src/button.test.jsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button'; - - test('pass button', async ({ mount }) => { - const component = await mount(); - await expect(component).toContainText('Header'); - await expect(component).toContainText('Button'); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); -}); - -test('should respect default property values', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - 'src/label.tsx': ` - export const Label = ({ checked }) =>
type:{typeof checked} value:{String(checked)}
; - `, - - 'src/label.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Label } from './label'; - - test('boolean shorthand', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('type:boolean value:true'); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); -}); - -test('should bundle public folder', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ` - `, - 'public/logo.svg': ` - - - `, - 'src/image.tsx': ` - export const Image = () => logo; - `, - 'src/image.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Image } from './image'; - - test('pass', async ({ mount, page }) => { - const urls = []; - const [response] = await Promise.all([ - page.waitForResponse('**/*.svg'), - mount() - ]); - const data = await response.body(); - await expect(data.toString()).toContain(''); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); -}); - -test('should work with property expressions in JSX', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ` - `, - 'src/button1.tsx': ` - const Button = () => ; - export const components1 = { Button }; - `, - 'src/button2.tsx': ` - const Button = () => ; - export default { Button }; - `, - 'src/button.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { components1 } from './button1'; - import components2 from './button2'; - - test('pass 1', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button 1'); - }); - - test('pass 2', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button 2'); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(2); -}); - -test('should handle the baseUrl config', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': ` - import { defineConfig } from '@playwright/experimental-ct-react'; - export default defineConfig({ use: { baseURL: 'http://127.0.0.1:8080' } }); - `, - 'playwright/index.html': ``, - 'playwright/index.js': ``, - - 'src/component.jsx': ` - export const Component = () => <>; - `, - - 'src/component.test.jsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Component } from './component'; - - test('pass component', async ({ page, mount }) => { - const component = await mount(); - await expect(page).toHaveURL('http://127.0.0.1:8080/'); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); -}); - -test('should handle the vite host config', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': ` - import { defineConfig } from '@playwright/experimental-ct-react'; - export default defineConfig({ use: { ctViteConfig: { preview: { host: '127.0.0.1' } } } }); - `, - 'playwright/index.html': ``, - 'playwright/index.js': ``, - - 'src/component.jsx': ` - export const Component = () => <>; - `, - - 'src/component.test.jsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Component } from './component'; - - test('pass component', async ({ page, mount }) => { - const component = await mount(); - const host = await page.evaluate(() => window.location.hostname); - await expect(host).toBe('127.0.0.1'); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); -}); - -test('should prioritize the vite host config over the baseUrl config', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': ` - import { defineConfig } from '@playwright/experimental-ct-react'; - export default defineConfig({ - use: { - baseURL: 'http://localhost:8080', - ctViteConfig: { preview: { host: '127.0.0.1' } } - }, - }); - `, - 'playwright/index.html': ``, - 'playwright/index.js': ``, - - 'src/component.jsx': ` - export const Component = () => <>; - `, - - 'src/component.test.jsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Component } from './component'; - - test('pass component', async ({ page, mount }) => { - const component = await mount(); - await expect(page).toHaveURL('http://127.0.0.1:8080/'); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); -}); - -test('should normalize children', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - 'src/component.tsx': ` - import React from 'react'; - export const OneChild: React.FC> = ({ children }) => { - React.Children.only(children); - return <>{children}; - }; - export const OtherComponent: React.FC = () =>

othercomponent

; - `, - - 'src/component.spec.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { OneChild, OtherComponent } from './component'; - - test("can pass an HTML element to OneChild", async ({ mount }) => { - const component = await mount(

child

); - await expect(component).toHaveText("child"); - }); - - test("can pass another component to OneChild", async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText("othercomponent"); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(2); -}); - -test('should allow props children', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - 'src/component.spec.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - - test("renders children from props object", async ({ mount, page }) => { - const props = { children: 'test' }; - await mount(); - await expect(component).toHaveText('Minified'); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); -}); - -test('should throw test error when template index.html is not provided', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': ` - import { defineConfig } from '@playwright/experimental-ct-react'; - export default defineConfig({}); - `, - 'src/component.jsx': ` - export const Component = () => <>; - `, - - 'src/component.test.jsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Component } from './component'; - - test('pass component', async ({ page, mount }) => { - const component = await mount(); - await expect(page).toHaveURL('http://127.0.0.1:8080/'); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(1); - expect(result.passed).toBe(0); - expect(result.output).toContain('Component testing template file playwright/index.html is missing and there is no existing Vite server. Component tests will fail.'); - expect(result.results[0].error.message).toBe('Error: Component testing could not determine the base URL of your component under test. Ensure you have supplied a template playwright/index.html or have set the PLAYWRIGHT_TEST_BASE_URL environment variable.'); -}); diff --git a/tests/playwright-test/playwright.reuse.browser.spec.ts b/tests/playwright-test/playwright.reuse.browser.spec.ts index 3f8ad95789ddc..5a3af6921abbb 100644 --- a/tests/playwright-test/playwright.reuse.browser.spec.ts +++ b/tests/playwright-test/playwright.reuse.browser.spec.ts @@ -104,11 +104,14 @@ test('should produce correct test steps', async ({ runInlineTest, runServer }) = const result = await runInlineTest({ 'reporter.ts': ` class Reporter { + stepTitle(step) { + return step.subtitle ? step.title + ' ' + step.subtitle : step.title; + } onStepBegin(test, result, step) { - console.log('%% onStepBegin [' + step.category + '] ' + step.title); + console.log('%% onStepBegin [' + step.category + '] ' + this.stepTitle(step)); } onStepEnd(test, result, step) { - console.log('%% onStepEnd [' + step.category + '] ' + step.title); + console.log('%% onStepEnd [' + step.category + '] ' + this.stepTitle(step)); } } module.exports = Reporter; @@ -137,8 +140,8 @@ test('should produce correct test steps', async ({ runInlineTest, runServer }) = 'onStepEnd [pw:api] Create page', 'onStepEnd [fixture] Fixture "page"', 'onStepEnd [hook] Before Hooks', - 'onStepBegin [pw:api] Navigate to "about:blank"', - 'onStepEnd [pw:api] Navigate to "about:blank"', + 'onStepBegin [pw:api] Navigate about:blank', + 'onStepEnd [pw:api] Navigate about:blank', 'onStepBegin [pw:api] Evaluate', 'onStepEnd [pw:api] Evaluate', 'onStepBegin [hook] After Hooks', diff --git a/tests/playwright-test/playwright.reuse.spec.ts b/tests/playwright-test/playwright.reuse.spec.ts index 047ec35d2952c..b6bae8cab2b45 100644 --- a/tests/playwright-test/playwright.reuse.spec.ts +++ b/tests/playwright-test/playwright.reuse.spec.ts @@ -139,7 +139,7 @@ test('should reuse context with trace if mode=when-possible', async ({ runInline ' Fixture "page"', ' Create page', 'Set content', - 'Click', + `Click locator('button')`, 'After Hooks', ' Fixture "page"', ' Fixture "context"', @@ -154,8 +154,8 @@ test('should reuse context with trace if mode=when-possible', async ({ runInline ' Fixture "page"', 'Expect "toBe"', 'Set content', - 'Fill "value"', - 'Click', + `Fill "value" locator('input')`, + `Click locator('input')`, 'After Hooks', ' Fixture "page"', ' Fixture "context"', @@ -485,15 +485,15 @@ test('should reset tracing', async ({ runInlineTest }, testInfo) => { const trace1 = await parseTrace(traceFile1); expect(trace1.model.renderActionTree()).toEqual([ 'Set content', - 'Click', + `Click locator('button')`, ]); expect(trace1.snapshots.snapshotsForTest().length).toBeGreaterThan(0); const trace2 = await parseTrace(traceFile2); expect(trace2.model.renderActionTree()).toEqual([ 'Set content', - 'Fill "value"', - 'Click', + `Fill "value" locator('input')`, + `Click locator('input')`, ]); expect(trace1.snapshots.snapshotsForTest().length).toBeGreaterThan(0); }); diff --git a/tests/playwright-test/playwright.trace.spec.ts b/tests/playwright-test/playwright.trace.spec.ts index 78ebd785be167..093f61597ab9b 100644 --- a/tests/playwright-test/playwright.trace.spec.ts +++ b/tests/playwright-test/playwright.trace.spec.ts @@ -97,8 +97,8 @@ test('should record api trace', async ({ runInlineTest, server }, testInfo) => { ' Create context', ' Fixture "page"', ' Create page', - 'Navigate to "about:blank"', - 'GET "/empty.html"', + 'Navigate about:blank', + `GET ${server.HOST}/empty.html`, 'After Hooks', ' Fixture "page"', ' Fixture "context"', @@ -109,7 +109,7 @@ test('should record api trace', async ({ runInlineTest, server }, testInfo) => { expect(trace2.model.renderActionTree()).toEqual([ 'Before Hooks', 'Create request context', - 'GET "/empty.html"', + `GET ${server.HOST}/empty.html`, 'After Hooks', ]); const trace3 = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.zip')); @@ -121,8 +121,8 @@ test('should record api trace', async ({ runInlineTest, server }, testInfo) => { ' Create context', ' Fixture "page"', ' Create page', - 'Navigate to "about:blank"', - 'GET "/empty.html"', + 'Navigate about:blank', + `GET ${server.HOST}/empty.html`, 'Expect "toBe"', 'After Hooks', ' Fixture "page"', @@ -188,9 +188,11 @@ test('should not mixup network files between contexts', async ({ runInlineTest, test.beforeAll(async ({ browser }) => { page1 = await browser.newPage(); await page1.goto("${server.EMPTY_PAGE}"); + await page1.request.get("${server.PREFIX}/simple.json?context=1"); page2 = await browser.newPage(); await page2.goto("${server.EMPTY_PAGE}"); + await page2.request.get("${server.PREFIX}/simple.json?context=2"); }); test.afterAll(async () => { @@ -200,12 +202,43 @@ test('should not mixup network files between contexts', async ({ runInlineTest, test('example', async ({ page }) => { await page.goto("${server.EMPTY_PAGE}"); + await page.request.get("${server.PREFIX}/simple.json?context=3"); }); `, }, { workers: 1, timeout: 15000 }); expect(result.exitCode).toEqual(0); expect(result.passed).toBe(1); - expect(fs.existsSync(testInfo.outputPath('test-results', 'a-example', 'trace.zip'))).toBe(true); + const tracePath = testInfo.outputPath('test-results', 'a-example', 'trace.zip'); + const { resources } = await parseTraceRaw(tracePath); + const traceEntries = [...resources].filter(([name]) => name.endsWith('.trace')).map(([name, content]) => ({ + prefix: name.slice(0, -'.trace'.length), + contextOptions: JSON.parse(content.toString().split('\n')[0]), + })).filter(entry => entry.contextOptions.origin === 'library'); + // Each of the 3 browser contexts and 3 api request contexts produces + // a trace chunk per test phase it was recording during. + const browserTraces = traceEntries.filter(entry => entry.contextOptions.browserName); + const apiTraces = traceEntries.filter(entry => !entry.contextOptions.browserName); + expect(browserTraces.length).toBeGreaterThanOrEqual(3); + expect(apiTraces.length).toBeGreaterThanOrEqual(3); + for (const entry of browserTraces) { + const network = resources.get(entry.prefix + '.network')!.toString(); + expect(network).not.toContain('?context='); + } + const apiURLs = [ + server.PREFIX + '/simple.json?context=1', + server.PREFIX + '/simple.json?context=2', + server.PREFIX + '/simple.json?context=3', + ]; + // Api request context network files are chunk-specific, so each of the requests + // must show up in exactly one network file. + const apiURLsByTrace = apiTraces.map(entry => { + const network = resources.get(entry.prefix + '.network')!.toString(); + return apiURLs.filter(url => network.includes(url)); + }); + expect(apiURLsByTrace.every(urls => urls.length <= 1)).toBe(true); + expect(apiURLsByTrace.flat().sort()).toEqual(apiURLs); + const trace = await parseTrace(tracePath); + expect(trace.model.resources.filter(resource => resource._apiRequestRef).map(resource => resource.request.url).sort()).toEqual(apiURLs); }); test('should save sources when requested', async ({ runInlineTest }, testInfo) => { @@ -226,7 +259,62 @@ test('should save sources when requested', async ({ runInlineTest }, testInfo) = }, { workers: 1 }); expect(result.exitCode).toEqual(0); const { resources } = await parseTraceRaw(testInfo.outputPath('test-results', 'a-pass', 'trace.zip')); - expect([...resources.keys()].filter(name => name.startsWith('resources/src@'))).toHaveLength(1); + expect([...resources.keys()].filter(name => name.startsWith('src/'))).toHaveLength(1); +}); + +test('should expand snapshots object in trace option', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + use: { + trace: { mode: 'on', snapshots: { dom: true, aria: true, screen: true } }, + } + }; + `, + 'a.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('pass', async ({ page }) => { + await page.setContent(''); + await page.locator('button').click(); + }); + `, + }, { workers: 1 }); + expect(result.exitCode).toEqual(0); + expect(result.passed).toBe(1); + + const { model } = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip')); + const click = model.actions.find(a => a.method === 'click')!; + expect(model.hasDomSnapshotForCall(click.callId, 'after')).toBeTruthy(); + expect(model.screenshotForCall(click.callId, 'after')).toBeTruthy(); + expect(model.ariaSnapshotForCall(click.callId, 'after')).toBeTruthy(); +}); + +test('should keep trace config options when forcing mode with --trace', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + use: { + trace: { mode: 'on-first-retry', snapshots: { dom: true, aria: true, screen: true } }, + } + }; + `, + 'a.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('pass', async ({ page }) => { + await page.setContent(''); + await page.locator('button').click(); + }); + `, + }, { workers: 1, trace: 'on' }); + expect(result.exitCode).toEqual(0); + expect(result.passed).toBe(1); + + // The 'on' mode is forced, while the snapshots configuration is preserved. + const { model } = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip')); + const click = model.actions.find(a => a.method === 'click')!; + expect(model.hasDomSnapshotForCall(click.callId, 'after')).toBeTruthy(); + expect(model.screenshotForCall(click.callId, 'after')).toBeTruthy(); + expect(model.ariaSnapshotForCall(click.callId, 'after')).toBeTruthy(); }); test('should not save sources when not requested', async ({ runInlineTest }, testInfo) => { @@ -250,7 +338,7 @@ test('should not save sources when not requested', async ({ runInlineTest }, tes }, { workers: 1 }); expect(result.exitCode).toEqual(0); const { resources } = await parseTraceRaw(testInfo.outputPath('test-results', 'a-pass', 'trace.zip')); - expect([...resources.keys()].filter(name => name.startsWith('resources/src@'))).toHaveLength(0); + expect([...resources.keys()].filter(name => name.startsWith('src/'))).toHaveLength(0); }); test('should work in serial mode', async ({ runInlineTest }, testInfo) => { @@ -324,7 +412,7 @@ test('should not override trace file in afterAll', async ({ runInlineTest, serve ' Create context', ' Fixture "page"', ' Create page', - 'Navigate to "about:blank"', + 'Navigate about:blank', 'After Hooks', ' Fixture "page"', ' Fixture "context"', @@ -332,7 +420,7 @@ test('should not override trace file in afterAll', async ({ runInlineTest, serve ' afterAll hook', ' Fixture "request"', ' Create request context', - ' GET "/empty.html"', + ` GET ${server.HOST}/empty.html`, ' Fixture "request"', 'Worker Cleanup', ' Fixture "browser"', @@ -450,7 +538,7 @@ test(`trace:retain-on-failure should create trace if context is closed before fa }, { trace: 'retain-on-failure' }); const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.zip'); const trace = await parseTrace(tracePath); - expect(trace.model.renderActionTree()).toContain('Navigate to "about:blank"'); + expect(trace.model.renderActionTree()).toContain('Navigate about:blank'); expect(result.failed).toBe(1); }); @@ -472,7 +560,7 @@ test(`trace:retain-on-failure should create trace if context is closed before fa }, { trace: 'retain-on-failure' }); const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.zip'); const trace = await parseTrace(tracePath); - expect(trace.model.renderActionTree()).toContain(' Navigate to "about:blank"'); + expect(trace.model.renderActionTree()).toContain(' Navigate about:blank'); expect(result.failed).toBe(1); }); @@ -492,7 +580,7 @@ test(`trace:retain-on-failure should create trace if request context is disposed }, { trace: 'retain-on-failure' }); const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.zip'); const trace = await parseTrace(tracePath); - expect(trace.model.renderActionTree()).toContain('GET "/empty.html"'); + expect(trace.model.renderActionTree()).toContain(`GET ${server.HOST}/empty.html`); expect(result.failed).toBe(1); }); @@ -522,10 +610,10 @@ test('should include attachments by default', async ({ runInlineTest, server }, expect(trace.model.actions[1].attachments).toEqual([{ name: 'foo', contentType: 'text/plain', - sha1: expect.any(String), + file: expect.any(String), }]); const { resources } = await parseTraceRaw(tracePath); - expect([...resources.keys()]).toContain(`resources/${trace.model.actions[1].attachments[0].sha1}`); + expect([...resources.keys()]).toContain(trace.model.actions[1].attachments[0].file); }); test('should opt out of attachments', async ({ runInlineTest, server }, testInfo) => { @@ -553,7 +641,7 @@ test('should opt out of attachments', async ({ runInlineTest, server }, testInfo ]); expect(trace.model.actions[1].attachments).toEqual(undefined); const { resources } = await parseTraceRaw(tracePath); - expect([...resources.keys()].filter(f => f.startsWith('resources/') && !f.startsWith('resources/src@'))).toHaveLength(0); + expect([...resources.keys()].filter(f => f.startsWith('attachments/') || f.startsWith('resources/'))).toHaveLength(0); }); test('should record with custom page fixture', async ({ runInlineTest }, testInfo) => { @@ -614,11 +702,11 @@ test('should expand expect.toPass', async ({ runInlineTest }, testInfo) => { ' Fixture "page"', ' Create page', 'Expect "toPass"', - ' Navigate to "data:"', + ' Navigate data:', ' Expect "toBe"', - ' Navigate to "data:"', + ' Navigate data:', ' Expect "toBe"', - ' Navigate to "data:"', + ' Navigate data:', ' Expect "toBe"', 'After Hooks', ' Fixture "page"', @@ -783,7 +871,7 @@ test('should use custom expect message in trace', async ({ runInlineTest }, test ' Create context', ' Fixture "page"', ' Create page', - 'expect to have text: find a hotel', + `expect to have text: find a hotel getByRole('button', { name: 'Find a hotel' })`, 'After Hooks', ' Fixture "page"', ' Fixture "context"', @@ -1099,7 +1187,7 @@ test('trace:retain-on-first-failure should create trace but only on first failur const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip'); const trace = await parseTrace(tracePath); - expect(trace.model.renderActionTree()).toContain('Navigate to "about:blank"'); + expect(trace.model.renderActionTree()).toContain('Navigate about:blank'); expect(result.failed).toBe(1); }); @@ -1116,7 +1204,7 @@ test('trace:retain-on-first-failure should create trace if context is closed bef }, { trace: 'retain-on-first-failure' }); const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip'); const trace = await parseTrace(tracePath); - expect(trace.model.renderActionTree()).toContain('Navigate to "about:blank"'); + expect(trace.model.renderActionTree()).toContain('Navigate about:blank'); expect(result.failed).toBe(1); }); @@ -1135,7 +1223,7 @@ test('trace:retain-on-first-failure should create trace if context is closed bef }, { trace: 'retain-on-first-failure' }); const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip'); const trace = await parseTrace(tracePath); - expect(trace.model.renderActionTree()).toContain(' Navigate to "about:blank"'); + expect(trace.model.renderActionTree()).toContain(' Navigate about:blank'); expect(result.failed).toBe(1); }); @@ -1152,7 +1240,7 @@ test('trace:retain-on-first-failure should create trace if request context is di }, { trace: 'retain-on-first-failure' }); const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip'); const trace = await parseTrace(tracePath); - expect(trace.model.renderActionTree()).toContain('GET "/empty.html"'); + expect(trace.model.renderActionTree()).toContain(`GET ${server.HOST}/empty.html`); expect(result.failed).toBe(1); }); @@ -1299,9 +1387,9 @@ test('should not nest top level expect into unfinished api calls ', { ' Create context', ' Fixture "page"', ' Create page', - 'Navigate to "/index"', - 'GET "/hang"', - 'Expect "toBeVisible"', + `Navigate ${server.HOST}/index`, + `GET ${server.HOST}/hang`, + `Expect "toBeVisible" getByText('Hello!')`, 'After Hooks', ' Fixture "page"', ' Fixture "context"', @@ -1376,25 +1464,19 @@ test('should record trace snapshot for more obscure commands', async ({ runInlin ' Launch browser', 'Create page', 'Set content', - 'Query count', + `Query count locator('div')`, 'Expect "toBe"', - 'Bounding box', + `Bounding box locator('div')`, 'After Hooks', ]); - const snapshotFrameOrPageId = trace.snapshots.snapshotsForTest()[0]; - const countAction = trace.model.actions.find(a => a.method === 'queryCount'); - expect(countAction.beforeSnapshot).toBeTruthy(); - expect(countAction.afterSnapshot).toBeTruthy(); - expect(trace.snapshots.snapshotByName(snapshotFrameOrPageId, countAction.beforeSnapshot)).toBeTruthy(); - expect(trace.snapshots.snapshotByName(snapshotFrameOrPageId, countAction.afterSnapshot)).toBeTruthy(); + expect(trace.snapshots.snapshotForCall(countAction.callId, 'before')).toBeTruthy(); + expect(trace.snapshots.snapshotForCall(countAction.callId, 'after')).toBeTruthy(); const boundingBoxAction = trace.model.actions.find(a => a.title === 'Bounding box'); - expect(boundingBoxAction.beforeSnapshot).toBeTruthy(); - expect(boundingBoxAction.afterSnapshot).toBeTruthy(); - expect(trace.snapshots.snapshotByName(snapshotFrameOrPageId, boundingBoxAction.beforeSnapshot)).toBeTruthy(); - expect(trace.snapshots.snapshotByName(snapshotFrameOrPageId, boundingBoxAction.afterSnapshot)).toBeTruthy(); + expect(trace.snapshots.snapshotForCall(boundingBoxAction.callId, 'before')).toBeTruthy(); + expect(trace.snapshots.snapshotForCall(boundingBoxAction.callId, 'after')).toBeTruthy(); }); test('should record default test timeout in trace', async ({ runInlineTest }, testInfo) => { @@ -1427,3 +1509,63 @@ test('should record custom test timeout in trace', async ({ runInlineTest }, tes const trace = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip')); expect(trace.model.testTimeout).toBe(120_000); }); + +test('should record test annotations in trace', async ({ runInlineTest }, testInfo) => { + test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42035' }); + + const result = await runInlineTest({ + 'a.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('pass', { + annotation: { type: 'note1', description: 'static annotation' }, + }, async ({}) => { + test.info().annotations.push({ type: 'note2', description: 'dynamic annotation' }); + test.info().annotations.push({ type: 'note3' }); + }); + `, + }, { trace: 'on' }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip')); + expect(trace.model.annotations).toEqual([ + { type: 'note1', description: 'static annotation' }, + { type: 'note2', description: 'dynamic annotation' }, + { type: 'note3' }, + ]); +}); + +test('should record step params in trace', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('pass', async ({}) => { + expect(1).toBe(1); + await test.step('my step', async () => {}, { params: { foo: 'bar' } }); + }); + `, + }, { trace: 'on' }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip')); + const actionByTitle = (title: string) => trace.model.actions.find(a => a.title === title)!; + expect(actionByTitle('my step').params).toEqual({ foo: 'bar' }); + expect(actionByTitle('Expect "toBe"').params).toEqual({ expected: '1' }); +}); + +test('should record step subtitle in trace', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('pass', async ({}) => { + await test.step('my step', async () => {}, { subtitle: 'my subtitle' }); + }); + `, + }, { trace: 'on' }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + const trace = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip')); + expect(trace.model.actions.find(a => a.title === 'my step')!.subtitle).toBe('my subtitle'); +}); diff --git a/tests/playwright-test/reporter-base.spec.ts b/tests/playwright-test/reporter-base.spec.ts index afd5aa0543a9a..fb8b8176b7133 100644 --- a/tests/playwright-test/reporter-base.spec.ts +++ b/tests/playwright-test/reporter-base.spec.ts @@ -486,5 +486,106 @@ for (const useIntermediateMergeReport of [false, true] as const) { expect(text).toContain('› passes @bar1 @bar2 ('); expect(text).toContain('› passes @baz1 @baz2 ('); }); + + test('should omit tags when omitTags is set', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + reporter: [['list', { omitTags: true }]], + }; + `, + 'a.test.ts': ` + const { test, expect } = require('@playwright/test'); + test('passes', { tag: ['@foo1', '@foo2'] }, async ({}) => { + expect(0).toBe(0); + }); + test('passes @bar1 @bar2', async ({}) => { + expect(0).toBe(0); + }); + test('passes @baz1', { tag: ['@baz2'] }, async ({}) => { + expect(0).toBe(0); + }); + `, + }); + const text = stripAnsi(result.output); + // Tags appended from the `tag` annotation are omitted. + expect(text).toContain('› passes ('); + expect(text).not.toContain('@foo1'); + expect(text).not.toContain('@foo2'); + expect(text).not.toContain('@baz2'); + // Tags authored directly in the title are preserved. + expect(text).toContain('› passes @bar1 @bar2 ('); + expect(text).toContain('› passes @baz1 ('); + expect(result.exitCode).toBe(0); + }); + + test('should omit tags from failures when omitTags is set', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + reporter: [['list', { omitTags: true }]], + }; + `, + 'a.test.ts': ` + const { test, expect } = require('@playwright/test'); + test('fails', { tag: ['@qux1'] }, async ({}) => { + expect(1).toBe(2); + }); + `, + }); + const text = stripAnsi(result.output); + const titleLines = text.split('\n').filter(line => line.includes('a.test.ts') && line.includes('› fails')); + expect(titleLines.length).toBeGreaterThan(0); + for (const line of titleLines) + expect(line).not.toContain('@qux1'); + expect(result.exitCode).toBe(1); + }); + + test('should omit tags via the PLAYWRIGHT_LIST_OMIT_TAGS environment variable', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + reporter: [['list']], + }; + `, + 'a.test.ts': ` + const { test, expect } = require('@playwright/test'); + test('passes', { tag: ['@foo1', '@foo2'] }, async ({}) => { + expect(0).toBe(0); + }); + test('passes @bar1 @bar2', async ({}) => { + expect(0).toBe(0); + }); + `, + }, undefined, { PLAYWRIGHT_LIST_OMIT_TAGS: '1' }); + const text = stripAnsi(result.output); + // Tags appended from the `tag` annotation are omitted. + expect(text).toContain('› passes ('); + expect(text).not.toContain('@foo1'); + expect(text).not.toContain('@foo2'); + // Tags authored directly in the title are preserved. + expect(text).toContain('› passes @bar1 @bar2 ('); + expect(result.exitCode).toBe(0); + }); + + test('should let PLAYWRIGHT_LIST_OMIT_TAGS override the omitTags option', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + reporter: [['list', { omitTags: true }]], + }; + `, + 'a.test.ts': ` + const { test, expect } = require('@playwright/test'); + test('passes', { tag: ['@foo1', '@foo2'] }, async ({}) => { + expect(0).toBe(0); + }); + `, + }, undefined, { PLAYWRIGHT_LIST_OMIT_TAGS: '0' }); + const text = stripAnsi(result.output); + // The environment variable disables omitTags, so the appended tags are shown. + expect(text).toContain('› passes @foo1 @foo2 ('); + expect(result.exitCode).toBe(0); + }); }); } diff --git a/tests/playwright-test/reporter-blob.spec.ts b/tests/playwright-test/reporter-blob.spec.ts index 0f3590b0e01f8..d6899c520409b 100644 --- a/tests/playwright-test/reporter-blob.spec.ts +++ b/tests/playwright-test/reporter-blob.spec.ts @@ -1256,6 +1256,40 @@ test('preserve steps in html report', async ({ runInlineTest, mergeReports, show await expect(page.getByText('Expect "toBe"')).toBeVisible(); }); +test('preserve step params', async ({ runInlineTest, mergeReports }) => { + const reportDir = test.info().outputPath('blob-report'); + const files = { + 'params-reporter.js': ` + class ParamsReporter { + onStepEnd(test, result, step) { + if (step.category === 'test.step' || step.title.startsWith('Navigate')) + console.log('%%' + (step.subtitle ? step.title + ' ' + step.subtitle : step.title) + ' | ' + JSON.stringify(step.params)); + } + } + module.exports = ParamsReporter; + `, + 'playwright.config.ts': ` + module.exports = { + reporter: [['blob']] + }; + `, + 'a.test.js': ` + import { test, expect } from '@playwright/test'; + test('test 1', async ({ page }) => { + await page.goto('about:blank'); + await test.step('my step', async () => {}, { subtitle: 'my subtitle', params: { foo: 'bar', count: 7 } }); + }); + `, + }; + await runInlineTest(files); + const { exitCode, outputLines } = await mergeReports(reportDir, undefined, { additionalArgs: ['--reporter', './params-reporter.js'] }); + expect(exitCode).toBe(0); + expect(outputLines).toEqual([ + `Navigate about:blank | {"url":"about:blank"}`, + `my step my subtitle | {"foo":"bar","count":7}`, + ]); +}); + test('support fileName option', async ({ runInlineTest, mergeReports }) => { const files = (fileSuffix: string) => ({ 'playwright.config.ts': ` diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 707c4c04b25e1..5a0b7f59c80fd 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -182,7 +182,7 @@ for (const useIntermediateMergeReport of [true, false] as const) { await expect(page.locator('text=Image mismatch')).toBeVisible(); await expect(page.locator('text=Snapshot mismatch')).toHaveCount(0); - await expect(page.getByTestId('test-screenshot-error-view').getByTestId('test-result-image-mismatch-tabs').locator('div')).toHaveText([ + await expect(page.getByTestId('test-screenshot-error-view').getByTestId('test-result-image-mismatch-tabs').getByRole('tab')).toHaveText([ 'Diff', 'Actual', 'Expected', @@ -714,6 +714,45 @@ for (const useIntermediateMergeReport of [true, false] as const) { await expect(page.locator('.source-line-running')).toContainText('request.get'); }); + test('should show a thumbnail for every trace attachment', async ({ runInlineTest, page, server, showReport }) => { + const result = await runInlineTest({ + 'a.test.js': ` + import { test, expect } from '@playwright/test'; + test('passes', async ({ browser }, testInfo) => { + for (const index of [1, 2]) { + const context = await browser.newContext(); + await context.tracing.start({ screenshots: true, snapshots: true }); + const page = await context.newPage(); + await page.goto('${server.EMPTY_PAGE}'); + const tracePath = testInfo.outputPath('trace' + index + '.zip'); + await context.tracing.stop({ path: tracePath }); + await testInfo.attach('trace', { path: tracePath, contentType: 'application/zip' }); + await context.close(); + } + }); + `, + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + + await showReport(); + await page.getByRole('link', { name: 'passes' }).click(); + + const traces = page.locator('.chip').filter({ hasText: 'Traces' }); + await expect(traces.locator('img')).toHaveCount(2); + await expect(traces.getByRole('link', { name: 'trace-1', exact: true })).toBeVisible(); + await expect(traces.getByRole('link', { name: 'trace-2', exact: true })).toBeVisible(); + + const hrefs = await traces.locator('a').filter({ has: page.locator('img') }).evaluateAll(links => links.map(link => link.getAttribute('href'))); + expect(hrefs).toHaveLength(2); + for (const href of hrefs) + expect(href!.match(/trace=/g)).toHaveLength(1); + expect(hrefs[0]).not.toBe(hrefs[1]); + + await traces.locator('img').first().click(); + await expect(page.locator('.action-title').first()).toBeVisible(); + }); + test('trace should not hang when showing parallel api requests', async ({ runInlineTest, page, server, showReport }) => { const result = await runInlineTest({ 'playwright.config.js': ` @@ -740,10 +779,12 @@ for (const useIntermediateMergeReport of [true, false] as const) { await page.getByRole('link', { name: 'View Trace' }).click(); // Trace viewer should not hang here when displaying parallal requests. - await expect(page.getByTestId('actions-tree')).toContainText('GET'); - await page.getByText('GET "/empty.html"').nth(2).click(); - await page.getByText('GET "/empty.html"').nth(1).click(); - await page.getByText('GET "/empty.html"').nth(0).click(); + const getActions = page.getByTestId('actions-tree').getByRole('treeitem', { name: /GET/ }); + await expect(getActions).toHaveCount(4); + await expect(getActions.first()).toContainText('/empty.html'); + await getActions.nth(2).click(); + await getActions.nth(1).click(); + await getActions.nth(0).click(); }); test('should warn user when viewing via file:// protocol', async ({ runInlineTest, page, showReport }, testInfo) => { @@ -895,6 +936,48 @@ for (const useIntermediateMergeReport of [true, false] as const) { await expect(page.locator('.tree-item-title', { hasText: 'fill password' })).toBeHidden(); }); + test('should highlight filter matches in step title and subtitle', async ({ runInlineTest, page, showReport }) => { + const result = await runInlineTest({ + 'a.test.js': ` + import { test, expect } from '@playwright/test'; + test('has steps', async ({ page }) => { + await page.setContent(''); + await page.click('#target'); + }); + `, + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + + await showReport(); + await page.getByRole('link', { name: 'has steps' }).click(); + + const filterInput = page.getByLabel('Filter steps'); + await filterInput.fill('click'); + await expect(page.locator('.step-title-highlight')).toHaveText(['Click']); + + await filterInput.fill('#target'); + await expect(page.locator('.step-subtitle .step-title-highlight')).toHaveText(['#target']); + }); + + test('should render test.step subtitle', async ({ runInlineTest, page, showReport }) => { + const result = await runInlineTest({ + 'a.test.js': ` + import { test, expect } from '@playwright/test'; + test('has steps', async ({}) => { + await test.step('Add to cart', async () => {}, { subtitle: 'SKU 42' }); + }); + `, + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + + await showReport(); + await page.getByRole('link', { name: 'has steps' }).click(); + await expect(page.locator('.step-title-container', { hasText: 'Add to cart' })).toHaveAttribute('aria-label', 'Add to cart SKU 42'); + await expect(page.locator('.step-subtitle')).toHaveText('SKU 42'); + }); + test('should show step snippets from non-root', async ({ runInlineTest, page, showReport }) => { const result = await runInlineTest({ 'playwright.config.js': ` @@ -1556,6 +1639,41 @@ for (const useIntermediateMergeReport of [true, false] as const) { `); }); + test('should toggle metadata with keyboard', async ({ runInlineTest, writeFiles, showReport, page }) => { + test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42323' }); + const files = { + 'playwright.config.ts': ` + export default { + captureGitInfo: { commit: true }, + }; + `, + 'example.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('sample', async ({}) => { expect(2).toBe(2); }); + `, + }; + const baseDir = await writeFiles(files); + await initGitRepo(baseDir); + + const result = await runInlineTest(files, { reporter: 'dot,html' }, { + PLAYWRIGHT_HTML_OPEN: 'never', + }); + + await showReport(); + + expect(result.exitCode).toBe(0); + const toggle = page.getByRole('button', { name: 'Metadata' }); + await toggle.focus(); + await expect(toggle).toBeFocused(); + await expect(toggle).toHaveAttribute('aria-expanded', 'false'); + await toggle.press('Enter'); + await expect(toggle).toHaveAttribute('aria-expanded', 'true'); + await expect(page.locator('.metadata-view')).toBeVisible(); + await toggle.press(' '); + await expect(toggle).toHaveAttribute('aria-expanded', 'false'); + await expect(page.locator('.metadata-view')).toBeHidden(); + }); + test('should not include git metadata w/o CI', async ({ runInlineTest, showReport, page }) => { const result = await runInlineTest({ 'playwright.config.ts': ` @@ -3192,6 +3310,68 @@ for (const useIntermediateMergeReport of [true, false] as const) { expect(prompt, 'contains diff').toContain(`+ expect(2).toBe(3);`); }); + test('should not turn complete clone shallow when capturing diff', async ({ runInlineTest, writeFiles }, testInfo) => { + test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42203' }); + const files = { + 'playwright.config.ts': `export default {}`, + 'example.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('sample', async ({}) => { expect(2).toBe(2); }); + `, + }; + const baseDir = await writeFiles(files); + await initGitRepo(baseDir); + const originDir = testInfo.outputPath('origin.git'); + await execGit(baseDir, ['clone', '--bare', baseDir, originDir]); + await execGit(originDir, ['config', 'uploadpack.allowAnySHA1InWant', 'true']); + await execGit(baseDir, ['remote', 'add', 'origin', originDir]); + const { stdout: baseSha } = await spawnAsync('git', ['rev-parse', 'HEAD~1'], { stdio: 'pipe', cwd: baseDir }); + + const result = await runInlineTest({}, { reporter: 'dot' }, { + PLAYWRIGHT_HTML_OPEN: 'never', + ...(await ghaPullRequestEnv(baseDir, baseSha.trim())), + }); + + expect(result.exitCode).toBe(0); + expect(result.report.config.metadata.gitDiff).toContain('example.spec.ts'); + const { stdout: isShallow } = await spawnAsync('git', ['rev-parse', '--is-shallow-repository'], { stdio: 'pipe', cwd: baseDir }); + expect(isShallow.trim()).toBe('false'); + }); + + test('should fetch missing pull request base commit when capturing diff', async ({ runInlineTest, writeFiles }, testInfo) => { + const files = { + 'playwright.config.ts': `export default {}`, + 'example.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('sample', async ({}) => { expect(2).toBe(2); }); + `, + }; + const baseDir = await writeFiles(files); + await initGitRepo(baseDir); + const originDir = testInfo.outputPath('origin.git'); + await execGit(baseDir, ['clone', '--bare', baseDir, originDir]); + await execGit(originDir, ['config', 'uploadpack.allowAnySHA1InWant', 'true']); + await execGit(baseDir, ['remote', 'add', 'origin', originDir]); + + const otherDir = testInfo.outputPath('other'); + await execGit(baseDir, ['clone', originDir, otherDir]); + await fs.promises.writeFile(path.join(otherDir, 'baseline.txt'), 'baseline'); + await execGit(otherDir, ['add', 'baseline.txt']); + await execGit(otherDir, ['-c', 'user.email=shakespeare@example.local', '-c', 'user.name=William', 'commit', '-m', 'baseline']); + const { stdout: baseSha } = await spawnAsync('git', ['rev-parse', 'HEAD'], { stdio: 'pipe', cwd: otherDir }); + await execGit(otherDir, ['push', 'origin', 'HEAD:refs/heads/baseline']); + + const result = await runInlineTest({}, { reporter: 'dot' }, { + PLAYWRIGHT_HTML_OPEN: 'never', + ...(await ghaPullRequestEnv(baseDir, baseSha.trim())), + }); + + expect(result.exitCode).toBe(0); + expect(result.report.config.metadata.gitDiff).toContain('baseline.txt'); + const { stdout: isShallow } = await spawnAsync('git', ['rev-parse', '--is-shallow-repository'], { stdio: 'pipe', cwd: baseDir }); + expect(isShallow.trim()).toBe('false'); + }); + test('should include snapshot when page wasnt closed', async ({ runInlineTest, showReport, page }) => { const result = await runInlineTest({ 'example.spec.ts': ` @@ -3431,7 +3611,7 @@ for (const useIntermediateMergeReport of [true, false] as const) { await expect(page.getByRole('link', { name: 'Speedboard' })).toHaveAttribute('aria-selected', 'true'); await expect(page).toMatchAriaSnapshot(` - - button "Slowest Tests" + - heading "Slowest Tests" [level=2] - region: - list: - listitem: @@ -3455,7 +3635,7 @@ for (const useIntermediateMergeReport of [true, false] as const) { `); await page.getByText('foo').first().click(); await expect(page).toMatchAriaSnapshot(` - - button "Slowest Tests" + - heading "Slowest Tests" [level=2] `); await page.getByRole('link', { name: 'Failed' }).click(); @@ -3660,13 +3840,13 @@ function ghaCommitEnv() { }; } -async function ghaPullRequestEnv(baseDir: string) { +async function ghaPullRequestEnv(baseDir: string, baseSha: string = 'main') { const eventPath = path.join(baseDir, 'event.json'); await fs.promises.writeFile(eventPath, JSON.stringify({ pull_request: { title: 'My PR', number: 42, - base: { sha: 'main' }, + base: { sha: baseSha }, }, })); return { diff --git a/tests/playwright-test/reporter-perfetto.spec.ts b/tests/playwright-test/reporter-perfetto.spec.ts new file mode 100644 index 0000000000000..15d230405e996 --- /dev/null +++ b/tests/playwright-test/reporter-perfetto.spec.ts @@ -0,0 +1,292 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as zlib from 'zlib'; +import { test, expect } from './playwright-test-fixtures'; + +type TraceEvent = { + name: string; + cat: string; + ph: 'X' | 'M' | 'i'; + ts: number; + dur?: number; + pid: number; + tid: number; + cname?: string; + args?: any; +}; + +function readTrace(baseDir: string, fileName: string = 'test-results/perfetto.json') { + const file = path.join(baseDir, fileName); + const content = fileName.endsWith('.gz') ? zlib.gunzipSync(fs.readFileSync(file)).toString('utf8') : fs.readFileSync(file, 'utf8'); + return JSON.parse(content) as { + traceEvents: TraceEvent[], + displayTimeUnit: string, + metadata: any, + }; +} + +function slices(events: TraceEvent[]) { + return events.filter(e => e.ph === 'X'); +} + +function threadNames(events: TraceEvent[]) { + return events.filter(e => e.ph === 'M' && e.name === 'thread_name').map(e => e.args.name); +} + +function findSlice(events: TraceEvent[], name: string) { + return slices(events).find(e => e.name === name); +} + +const testFiles = { + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test.beforeAll(async () => {}); + test.beforeEach(async () => {}); + test.describe('suite', () => { + test('passing @smoke', { annotation: { type: 'issue', description: 'flaky' } }, async ({}) => { + console.log('hello from the test'); + await test.step('outer', async () => { + await test.step('inner', async () => { + expect(1).toBe(1); + }); + }); + }); + }); + test('failing', async ({}) => { + expect(1).toBe(2); + }); + `, +}; + +test('should write a perfetto report', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest(testFiles, { reporter: 'perfetto' }); + expect(result.exitCode).toBe(1); + + const report = readTrace(testInfo.outputPath()); + expect(report.displayTimeUnit).toBe('ms'); + expect(report.metadata.status).toBe('failed'); + + const events = report.traceEvents; + expect(events.filter(e => e.ph === 'M' && e.name === 'process_name')[0].args.name).toBe('Playwright Test'); + expect(threadNames(events)).toEqual(['Worker 0']); + + const passing = findSlice(events, 'passing @smoke')!; + expect(passing.cat).toBe('test'); + expect(passing.cname).toBe('good'); + expect(passing.dur).toBeGreaterThan(0); + expect(passing.args).toEqual(expect.objectContaining({ + status: 'passed', + expectedStatus: 'passed', + title: 'suite › passing @smoke', + workerIndex: 0, + parallelIndex: 0, + timeout: 30000, + tags: '@smoke', + annotations: ['issue: flaky'], + stdout: 'hello from the test\n', + })); + expect(passing.args.location).toContain('a.test.ts:'); + + const failing = findSlice(events, 'failing')!; + expect(failing.cname).toBe('bad'); + expect(failing.args.status).toBe('failed'); + expect(failing.args.errors[0]).toContain('expect(received).toBe(expected)'); + + // Hooks, fixtures and steps are all rendered as slices. + expect(findSlice(events, 'Before Hooks')!.cat).toBe('hook'); + expect(findSlice(events, 'beforeAll hook')!.cat).toBe('hook'); + expect(findSlice(events, 'beforeEach hook')!.cat).toBe('hook'); + expect(findSlice(events, 'After Hooks')!.cat).toBe('hook'); + expect(findSlice(events, 'Expect "toBe"')!.cat).toBe('expect'); + + const outer = findSlice(events, 'outer')!; + expect(outer.cat).toBe('test.step'); + expect(outer.args.location).toContain('a.test.ts:'); +}); + +test('should nest steps within the test slice', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest(testFiles, { reporter: 'perfetto' }); + expect(result.exitCode).toBe(1); + + const events = slices(readTrace(testInfo.outputPath()).traceEvents); + // Complete events on the same thread must form a proper stack. + const byThread = new Map(); + for (const event of events) { + let list = byThread.get(event.tid); + if (!list) { + list = []; + byThread.set(event.tid, list); + } + list.push(event); + } + for (const list of byThread.values()) { + list.sort((a, b) => a.ts - b.ts || b.dur! - a.dur!); + const stack: TraceEvent[] = []; + for (const event of list) { + while (stack.length && stack[stack.length - 1].ts + stack[stack.length - 1].dur! <= event.ts) + stack.pop(); + if (stack.length) { + const parent = stack[stack.length - 1]; + expect(event.ts + event.dur!, `${event.name} inside ${parent.name}`).toBeLessThanOrEqual(parent.ts + parent.dur!); + } + stack.push(event); + } + } + + const outer = findSlice(events, 'outer')!; + const inner = findSlice(events, 'inner')!; + expect(inner.ts).toBeGreaterThanOrEqual(outer.ts); + expect(inner.ts + inner.dur!).toBeLessThanOrEqual(outer.ts + outer.dur!); +}); + +test('should use a lane per worker', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('one', async ({}) => { await new Promise(f => setTimeout(f, 500)); }); + `, + 'b.test.ts': ` + import { test, expect } from '@playwright/test'; + test('two', async ({}) => { await new Promise(f => setTimeout(f, 500)); }); + `, + }, { reporter: 'perfetto', workers: 2 }); + expect(result.exitCode).toBe(0); + + const events = readTrace(testInfo.outputPath()).traceEvents; + expect(threadNames(events)).toEqual(['Worker 0', 'Worker 1']); + expect(findSlice(events, 'one')!.tid).not.toBe(findSlice(events, 'two')!.tid); +}); + +test('should report attachment files', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import fs from 'fs'; + import { test, expect } from '@playwright/test'; + test('one', async ({}) => { + const file = test.info().outputPath('note.txt'); + fs.writeFileSync(file, 'hello'); + await test.info().attach('inline', { body: 'body' }); + await test.info().attach('file', { path: file }); + }); + `, + }, { reporter: 'perfetto' }); + expect(result.exitCode).toBe(0); + + const one = findSlice(readTrace(testInfo.outputPath()).traceEvents, 'one')!; + expect(one.args.attachments).toEqual(['inline', expect.stringMatching(/^test-results\/a-one\/attachments\/file-.*\.txt$/)]); +}); + +test('should report step params', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('one', async ({ page }) => { + await page.goto('about:blank'); + await page.setContent(''); + await page.getByRole('button').click(); + await expect(page.getByRole('button')).toBeVisible(); + await test.step('my step', async () => {}, { params: { foo: 'bar', count: 7 } }); + }); + `, + }, { reporter: 'perfetto' }); + expect(result.exitCode).toBe(0); + + const events = slices(readTrace(testInfo.outputPath()).traceEvents); + expect(findSlice(events, 'Navigate about:blank')!.args.params).toEqual({ url: 'about:blank' }); + expect(findSlice(events, 'Set content')!.args.params).toBe(undefined); + expect(findSlice(events, `Click getByRole('button')`)!.args.params).toEqual({ locator: `getByRole('button')` }); + expect(findSlice(events, `Expect "toBeVisible" getByRole('button')`)!.args.params).toEqual({ locator: `getByRole('button')` }); + expect(findSlice(events, 'my step')!.args.params).toEqual({ foo: 'bar', count: 7 }); +}); + +test('should respect outputFile option', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { reporter: [['perfetto', { outputFile: 'reports/my-trace.json' }]] }; + `, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('one', async ({}) => {}); + `, + }); + expect(result.exitCode).toBe(0); + expect(findSlice(readTrace(testInfo.outputPath(), 'reports/my-trace.json').traceEvents, 'one')).toBeTruthy(); +}); + +test('should gzip the report when output file ends with .gz', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { reporter: [['perfetto', { outputFile: 'perfetto.json.gz' }]] }; + `, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('one', async ({}) => {}); + `, + }); + expect(result.exitCode).toBe(0); + + const gzipped = fs.readFileSync(testInfo.outputPath('perfetto.json.gz')); + expect(gzipped.subarray(0, 2)).toEqual(Buffer.from([0x1f, 0x8b])); + expect(findSlice(readTrace(testInfo.outputPath(), 'perfetto.json.gz').traceEvents, 'one')).toBeTruthy(); +}); + +test('should respect PLAYWRIGHT_PERFETTO_OUTPUT_FILE', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('one', async ({}) => {}); + `, + }, { reporter: 'perfetto' }, { PLAYWRIGHT_PERFETTO_OUTPUT_FILE: testInfo.outputPath('env-trace.json') }); + expect(result.exitCode).toBe(0); + expect(findSlice(readTrace(testInfo.outputPath(), 'env-trace.json').traceEvents, 'one')).toBeTruthy(); +}); + +test('should report retries as separate slices', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('flaky', async ({}, testInfo) => { + expect(testInfo.retry).toBe(1); + }); + `, + }, { reporter: 'perfetto', retries: 1 }); + expect(result.exitCode).toBe(0); + + const flaky = slices(readTrace(testInfo.outputPath()).traceEvents).filter(e => e.name === 'flaky'); + expect(flaky).toHaveLength(2); + expect(flaky.map(e => e.args.status)).toEqual(['failed', 'passed']); + expect(flaky[1].args.retry).toBe(1); +}); + +test('should report global errors as instant events', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('one', async ({}) => {}); + `, + 'b.test.ts': ` + throw new Error('Oh my!'); + `, + }, { reporter: 'perfetto' }); + expect(result.exitCode).toBe(1); + + const errors = readTrace(testInfo.outputPath()).traceEvents.filter(e => e.ph === 'i'); + expect(errors).toHaveLength(1); + expect(errors[0].args.error).toContain('Oh my!'); +}); diff --git a/tests/playwright-test/reporter-preprocess.spec.ts b/tests/playwright-test/reporter-preprocess.spec.ts index 12930c32f57d9..66a84ba4fb0fe 100644 --- a/tests/playwright-test/reporter-preprocess.spec.ts +++ b/tests/playwright-test/reporter-preprocess.spec.ts @@ -522,3 +522,41 @@ test('plan.suite temporarily exposes dependencies without changing final project 'ran keep/keep-test', ]); }); + +test('serial suites expose a serial annotation for custom sharding', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'reporter.ts': ` + class Reporter { + async preprocess({ suite }) { + for (const t of suite.allTests()) + console.log('%% ' + t.title + ':' + t.annotations.filter(a => a.type === 'serial').length); + } + } + module.exports = Reporter; + `, + 'playwright.config.ts': `module.exports = { reporter: './reporter.ts' };`, + 'a.test.ts': ` + import { test } from '@playwright/test'; + test('plain', async () => {}); + test.describe.serial('s', () => { + test('serial', async () => {}); + }); + test.describe('c', () => { + test.describe.configure({ mode: 'serial' }); + test('configure', async () => {}); + }); + test.describe.serial('outer', () => { + test.describe.serial('inner', () => { + test('nested', async () => {}); + }); + }); + `, + }, { reporter: '', workers: 1 }); + expect(result.exitCode).toBe(0); + expect(result.outputLines).toEqual([ + 'plain:0', + 'serial:1', + 'configure:1', + 'nested:2', + ]); +}); diff --git a/tests/playwright-test/reporter.spec.ts b/tests/playwright-test/reporter.spec.ts index e0330b1e83b4e..3199c45c1817b 100644 --- a/tests/playwright-test/reporter.spec.ts +++ b/tests/playwright-test/reporter.spec.ts @@ -795,6 +795,31 @@ test('step attachments are referentially equal to result attachments', async ({ ]); }); +test('step annotations are reported in onStepEnd', async ({ runInlineTest }) => { + class TestReporter implements Reporter { + onStepEnd(test: TestCase, result: TestResult, step: TestStep) { + if (step.category === 'test.step') + console.log('%%%', JSON.stringify(step.annotations)); + } + } + const result = await runInlineTest({ + 'reporter.ts': `module.exports = ${TestReporter.toString()}`, + 'playwright.config.ts': `module.exports = { reporter: './reporter' };`, + 'a.spec.ts': ` + import { test } from '@playwright/test'; + test('test', async () => { + await test.step('step', async stepInfo => { + stepInfo.annotations.push({ type: 'expected-result', description: 'step passes' }); + }); + }); + `, + }, { 'reporter': '', 'workers': 1 }); + + expect(result.outputLines).toEqual([ + JSON.stringify([{ type: 'expected-result', description: 'step passes' }]), + ]); +}); + test('step.attach attachments are reported on right steps', async ({ runInlineTest }) => { class TestReporter implements Reporter { onStepEnd(test: TestCase, result: TestResult, step: TestStep) { @@ -987,3 +1012,23 @@ test('AggregateError sub-errors are spread into testInfo.errors', async ({ runIn expect.stringMatching(/^FRAME Error: sub b: at .*a\.spec\.ts:19:/), ]); }); + +test('--add-reporter should append to configured reporters instead of replacing them', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'configured-reporter.js': ` + module.exports = class { onBegin() { console.log('FROM_CONFIGURED_REPORTER'); } }; + `, + 'added-reporter.js': ` + module.exports = class { onBegin() { console.log('FROM_ADDED_REPORTER'); } }; + `, + 'playwright.config.ts': `module.exports = { reporter: [['./configured-reporter.js']] };`, + 'a.spec.js': ` + const { test } = require('@playwright/test'); + test('test', () => {}); + `, + }, { 'workers': 1 }, undefined, { additionalArgs: ['--add-reporter=./added-reporter.js'] }); + + expect(result.exitCode).toBe(0); + expect(result.output).toContain('FROM_CONFIGURED_REPORTER'); + expect(result.output).toContain('FROM_ADDED_REPORTER'); +}); diff --git a/tests/playwright-test/resolver.spec.ts b/tests/playwright-test/resolver.spec.ts index c9909e0f871f4..6215abb16b608 100644 --- a/tests/playwright-test/resolver.spec.ts +++ b/tests/playwright-test/resolver.spec.ts @@ -14,6 +14,9 @@ * limitations under the License. */ +import fs from 'fs'; +import path from 'path'; + import { test, expect } from './playwright-test-fixtures'; test('should print tsconfig parsing error', async ({ runInlineTest }) => { @@ -622,32 +625,91 @@ test('should resolve extends from an explicit node_modules subpath', async ({ ru expect(result.exitCode).toBe(0); }); -test('should fail loudly when extends path cannot be resolved', async ({ runInlineTest }) => { - test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41543' }); +test('should ignore extends bare specifier resolvable only via node_modules walk-up', async ({ runInlineTest }, testInfo) => { + test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41989' }); + + const baseDir = testInfo.outputPath(); + const symlinkType = process.platform === 'win32' ? 'junction' : 'dir'; + // npm symlinks "file:" dependencies into node_modules. The realpath escapes node_modules, + // so the dependency goes through per-file tsconfig discovery. + await fs.promises.mkdir(path.join(baseDir, 'node_modules'), { recursive: true }); + await fs.promises.symlink(path.join(baseDir, 'dep-src'), path.join(baseDir, 'node_modules', 'dep'), symlinkType); const result = await runInlineTest({ - 'tsconfig.json': `{ - "extends": "./tsconfig.bas.json", - }`, + 'package.json': JSON.stringify({ name: 'repro', private: true, dependencies: { 'dep': 'file:./dep-src', 'foo': '1.0.0' } }), + // "foo" is hoisted to the root node_modules and is absent from dep's own node_modules. + // tsc resolves the "extends" below by walking up node_modules from dep-src. + 'node_modules/foo/package.json': JSON.stringify({ name: 'foo', version: '1.0.0', main: 'index.js' }), + 'node_modules/foo/index.js': `module.exports = 'foo';`, + 'node_modules/foo/tsconfig.json': `{ "compilerOptions": {} }`, + 'dep-src/package.json': JSON.stringify({ name: 'dep', version: '1.0.0', main: 'index.js' }), + 'dep-src/tsconfig.json': `{ "extends": "foo/tsconfig.json" }`, + 'dep-src/index.js': ` + require('foo'); + module.exports = 'dep'; + `, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + const dep = require('dep'); + test('test', () => { + expect(dep).toBe('dep'); + }); + `, + }); + + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); + +test('should ignore extends subpath resolvable only via package.json exports', async ({ runInlineTest }) => { + test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41989' }); + + const result = await runInlineTest({ + 'package.json': JSON.stringify({ name: 'test-project' }), + // The "./next" subpath only exists in the exports map, there is no "next.json" file. + 'node_modules/@repo/tsconfig/package.json': JSON.stringify({ name: '@repo/tsconfig', version: '1.0.0', exports: { './next': './tsconfig.next.json' } }), + 'node_modules/@repo/tsconfig/tsconfig.next.json': `{ "compilerOptions": {} }`, + 'tsconfig.json': `{ "extends": "@repo/tsconfig/next" }`, 'a.test.ts': ` import { test, expect } from '@playwright/test'; test('test', () => {}); `, }); - expect(result.exitCode).toBe(1); - expect(result.output).toContain('Failed to resolve "extends" path "./tsconfig.bas.json"'); + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); }); -test('should fail loudly when references path cannot be resolved', async ({ runInlineTest }) => { - test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41543' }); +test('should ignore extends bare package name resolvable only via package.json exports', async ({ runInlineTest }) => { + test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41989' }); const result = await runInlineTest({ + 'package.json': JSON.stringify({ name: 'test-project' }), + 'node_modules/typescript-config-silverwind/package.json': JSON.stringify({ name: 'typescript-config-silverwind', version: '1.0.0', exports: './tsconfig.json' }), + 'node_modules/typescript-config-silverwind/tsconfig.json': `{ "compilerOptions": {} }`, + 'tsconfig.json': `{ "extends": "typescript-config-silverwind" }`, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('test', () => {}); + `, + }); + + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); + +test('should ignore tsconfig references pointing to a directory', async ({ runInlineTest }) => { + test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41998' }); + + const result = await runInlineTest({ + // tsc resolves a directory-form reference to "./sub/tsconfig.json", + // as written by "tsc --build" and "nx sync". 'tsconfig.json': `{ - "files": [], - "references": [ - { "path": "./tsconfig.doesnotexist.json" } - ] + "compilerOptions": { "composite": true }, + "references": [{ "path": "./sub" }], + }`, + 'sub/tsconfig.json': `{ + "compilerOptions": { "composite": true }, }`, 'a.test.ts': ` import { test, expect } from '@playwright/test'; @@ -655,8 +717,8 @@ test('should fail loudly when references path cannot be resolved', async ({ runI `, }); - expect(result.exitCode).toBe(1); - expect(result.output).toContain('Failed to resolve "references" path "./tsconfig.doesnotexist.json"'); + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); }); test('should respect tsconfig project references', async ({ runInlineTest }) => { @@ -671,9 +733,6 @@ test('should respect tsconfig project references', async ({ runInlineTest }) => { "path": "./tsconfig.test.json" } ] }`, - 'tsconfig.app.json': `{ - "compilerOptions": {}, - }`, 'tsconfig.test.json': `{ "compilerOptions": { "baseUrl": ".", diff --git a/tests/playwright-test/runner.spec.ts b/tests/playwright-test/runner.spec.ts index 4973103484383..812f6cade29ed 100644 --- a/tests/playwright-test/runner.spec.ts +++ b/tests/playwright-test/runner.spec.ts @@ -866,6 +866,55 @@ test('should run nothing with --last-failed when previous run had no failures', expect(result2.didNotRun).toBe(0); }); +test('should run last failed tests that did not run because a beforeAll hook failed', async ({ runInlineTest }) => { + const workspace = { + 'a.spec.js': ` + import fs from 'fs'; + import { test, expect } from '@playwright/test'; + test.beforeAll(() => { + if (!fs.existsSync('marker.txt')) { + fs.writeFileSync('marker.txt', ''); + throw new Error('from beforeAll'); + } + }); + test('one', () => {}); + test('two', () => {}); + ` + }; + const result1 = await runInlineTest(workspace); + expect(result1.exitCode).toBe(1); + expect(result1.failed).toBe(1); + expect(result1.didNotRun).toBe(1); + + const result2 = await runInlineTest(workspace, {}, {}, { additionalArgs: ['--last-failed'] }); + expect(result2.exitCode).toBe(0); + expect(result2.passed).toBe(2); + expect(result2.didNotRun).toBe(0); +}); + +test('should not run intentionally skipped tests with --last-failed', async ({ runInlineTest }) => { + const workspace = { + 'a.spec.js': ` + import { test, expect } from '@playwright/test'; + test('fail', () => { + expect(1).toBe(2); + }); + test('skipped', () => { + test.skip(); + }); + ` + }; + const result1 = await runInlineTest(workspace); + expect(result1.exitCode).toBe(1); + expect(result1.failed).toBe(1); + expect(result1.skipped).toBe(1); + + const result2 = await runInlineTest(workspace, {}, {}, { additionalArgs: ['--last-failed'] }); + expect(result2.exitCode).toBe(1); + expect(result2.failed).toBe(1); + expect(result2.skipped).toBe(0); +}); + test('should run last failed tests in a shard', async ({ runInlineTest }) => { const workspace = { 'a.spec.js': ` diff --git a/tests/playwright-test/stable-test-runner/package-lock.json b/tests/playwright-test/stable-test-runner/package-lock.json index b234a13dbbfad..059b30707ec6e 100644 --- a/tests/playwright-test/stable-test-runner/package-lock.json +++ b/tests/playwright-test/stable-test-runner/package-lock.json @@ -5,16 +5,16 @@ "packages": { "": { "dependencies": { - "@playwright/test": "^1.62.0-alpha-2026-07-20" + "@playwright/test": "^1.63.0-alpha-2026-08-24" } }, "node_modules/@playwright/test": { - "version": "1.62.0-alpha-2026-07-20", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.0-alpha-2026-07-20.tgz", - "integrity": "sha512-Lb218lZdfhxIJpDF3QRaVqDjNyruDfN7gKIYbMRCHRfTsgQqz6cp2xJznDrc80k3gLC6Wby/twFkbfaKWUd/rg==", + "version": "1.63.0-alpha-2026-08-24", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0-alpha-2026-08-24.tgz", + "integrity": "sha512-6VsurUJMvpTf/C0c6FzCwcHGinMRpHjjok4ltOmbKtwyVuj0nu8DHGsV6mcG3x7RcW8yOHzx85rMkzHlUlMWmw==", "license": "Apache-2.0", "dependencies": { - "playwright": "1.62.0-alpha-2026-07-20" + "playwright": "1.63.0-alpha-2026-08-24" }, "bin": { "playwright": "cli.js" @@ -23,42 +23,25 @@ "node": ">=20" } }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/playwright": { - "version": "1.62.0-alpha-2026-07-20", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0-alpha-2026-07-20.tgz", - "integrity": "sha512-/h5GCcC7LUQOUS+XpGhY5B+UWsyU7PC2oEVzSjqn6dBf2FQBohD+JV0FDrmQkhcTqn5AoWNTbEEPswu9WZvtZg==", + "version": "1.63.0-alpha-2026-08-24", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0-alpha-2026-08-24.tgz", + "integrity": "sha512-EAFO5t3j3gV9yXO1YL4xEVYfPPZGP3Z/H/vYKaCDV+Dz/9TOZ0lxVh2NS9r0BcHsfTmhL8m1SixYLu3S2W/Bag==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.62.0-alpha-2026-07-20" + "playwright-core": "1.63.0-alpha-2026-08-24" }, "bin": { "playwright": "cli.js" }, "engines": { "node": ">=20" - }, - "optionalDependencies": { - "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.62.0-alpha-2026-07-20", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0-alpha-2026-07-20.tgz", - "integrity": "sha512-JCPvar5AJouLPK5dce8+M/tC3f0gfSt564h1pdL5aHSW5SAaTyPPKdZ01DnGeuftrCzXQIExEc5SDKBV109JHA==", + "version": "1.63.0-alpha-2026-08-24", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0-alpha-2026-08-24.tgz", + "integrity": "sha512-Gk9nefGTHgY8rFGZ6b1TDe4TTh7weGSDtPFdKejvo9n2clZ3wMZesdEmAAY2sr8lCVdhfpm2TzOInwsOIPF1gw==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" diff --git a/tests/playwright-test/stable-test-runner/package.json b/tests/playwright-test/stable-test-runner/package.json index 009d78eecef81..fd3f7abf29854 100644 --- a/tests/playwright-test/stable-test-runner/package.json +++ b/tests/playwright-test/stable-test-runner/package.json @@ -1,6 +1,6 @@ { "private": true, "dependencies": { - "@playwright/test": "^1.62.0-alpha-2026-07-20" + "@playwright/test": "^1.63.0-alpha-2026-08-24" } } diff --git a/tests/playwright-test/test-locks.spec.ts b/tests/playwright-test/test-locks.spec.ts new file mode 100644 index 0000000000000..1c4802ac49616 --- /dev/null +++ b/tests/playwright-test/test-locks.spec.ts @@ -0,0 +1,238 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +// Given '%%begin:' and '%%end:' lines, returns pairs from the +// `conflicts` list that were running at the same time. +function conflictingOverlaps(lines: string[], conflicts: [string, string][]): [string, string][] { + const running = new Set(); + const overlaps: [string, string][] = []; + for (const line of lines) { + const [kind, name] = line.split(':'); + if (kind === 'begin') { + for (const [x, y] of conflicts) { + if ((name === x && running.has(y)) || (name === y && running.has(x))) + overlaps.push([x, y]); + } + running.add(name); + } else if (kind === 'end') { + running.delete(name); + } + } + return overlaps; +} + +const lockedTest = (name: string, delay: number, lock?: string | string[]) => ` + test('${name}'${lock !== undefined ? `, { lock: ${JSON.stringify(lock)} }` : ''}, async () => { + console.log('\\n%%begin:${name}'); + await new Promise(f => setTimeout(f, ${delay})); + console.log('\\n%%end:${name}'); + }); +`; + +test('should not run tests with the same lock at the same time', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { fullyParallel: true }; + `, + 'a.test.ts': ` + import { test } from '@playwright/test'; + ${lockedTest('test1', 1000, 'shared')} + `, + 'b.test.ts': ` + import { test } from '@playwright/test'; + ${lockedTest('test2', 1000, 'shared')} + `, + }, { workers: 2 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(2); + expect(conflictingOverlaps(result.outputLines, [['test1', 'test2']])).toEqual([]); +}); + +test('should run tests with different locks at the same time', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { fullyParallel: true }; + `, + 'helper.ts': ` + import fs from 'fs'; + import path from 'path'; + export async function signalAndWait(signal: string, waitFor: string) { + fs.mkdirSync(process.env.SIGNAL_DIR, { recursive: true }); + fs.writeFileSync(path.join(process.env.SIGNAL_DIR, signal), ''); + while (!fs.existsSync(path.join(process.env.SIGNAL_DIR, waitFor))) + await new Promise(f => setTimeout(f, 100)); + } + `, + 'a.test.ts': ` + import { test } from '@playwright/test'; + import { signalAndWait } from './helper'; + test('test1', { lock: 'lock-a' }, async () => { + // Only finishes when both tests run at the same time. + await signalAndWait('a.txt', 'b.txt'); + }); + `, + 'b.test.ts': ` + import { test } from '@playwright/test'; + import { signalAndWait } from './helper'; + test('test2', { lock: 'lock-b' }, async () => { + await signalAndWait('b.txt', 'a.txt'); + }); + `, + }, { workers: 2 }, { SIGNAL_DIR: test.info().outputDir }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(2); +}); + +test('should not run tests with the same lock from different projects at the same time', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + projects: [ + { name: 'project1' }, + { name: 'project2' }, + ], + }; + `, + 'a.test.ts': ` + import { test } from '@playwright/test'; + test('test1', { lock: 'shared' }, async ({}, testInfo) => { + console.log('\\n%%begin:' + testInfo.project.name); + await new Promise(f => setTimeout(f, 1000)); + console.log('\\n%%end:' + testInfo.project.name); + }); + `, + }, { workers: 2 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(2); + expect(conflictingOverlaps(result.outputLines, [['project1', 'project2']])).toEqual([]); +}); + +test('should support locks declared on a describe group', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { fullyParallel: true }; + `, + 'a.test.ts': ` + import { test } from '@playwright/test'; + test.describe('locked suite', { lock: 'shared' }, () => { + ${lockedTest('test1', 1000)} + ${lockedTest('test2', 1000)} + }); + `, + }, { workers: 2 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(2); + expect(conflictingOverlaps(result.outputLines, [['test1', 'test2']])).toEqual([]); +}); + +test('should support multiple locks on a single test', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { fullyParallel: true }; + `, + 'a.test.ts': ` + import { test } from '@playwright/test'; + ${lockedTest('test1', 1000, ['lock-a', 'lock-b'])} + `, + 'b.test.ts': ` + import { test } from '@playwright/test'; + ${lockedTest('test2', 1000, 'lock-a')} + `, + 'c.test.ts': ` + import { test } from '@playwright/test'; + ${lockedTest('test3', 1000, 'lock-b')} + `, + }, { workers: 3 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(3); + expect(conflictingOverlaps(result.outputLines, [['test1', 'test2'], ['test1', 'test3']])).toEqual([]); +}); + +test('should hold the lock for the whole file group in default mode', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test } from '@playwright/test'; + ${lockedTest('a1', 500, 'shared')} + ${lockedTest('a2', 500)} + `, + 'b.test.ts': ` + import { test } from '@playwright/test'; + ${lockedTest('b1', 1000, 'shared')} + `, + }, { workers: 2 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(3); + // The lock declared on a1 covers the whole file, including a2. + expect(conflictingOverlaps(result.outputLines, [['a1', 'b1'], ['a2', 'b1']])).toEqual([]); +}); + +test('should respect locks on tests from a parallel suite with beforeAll hooks', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { fullyParallel: true }; + `, + 'a.test.ts': ` + import { test } from '@playwright/test'; + test.beforeAll(() => { + console.log('\\n%%beforeAll'); + }); + test('plain1', async () => {}); + test('plain2', async () => {}); + ${lockedTest('test1', 1000, 'shared')} + `, + 'b.test.ts': ` + import { test } from '@playwright/test'; + ${lockedTest('test2', 1000, 'shared')} + `, + }, { workers: 2 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(4); + expect(result.output).toContain('%%beforeAll'); + expect(conflictingOverlaps(result.outputLines, [['test1', 'test2']])).toEqual([]); +}); + +test('should not count waiting for a lock towards the test timeout', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { timeout: 3000 }; + `, + 'a.test.ts': ` + import { test } from '@playwright/test'; + ${lockedTest('test1', 2000, 'shared')} + `, + 'b.test.ts': ` + import { test } from '@playwright/test'; + ${lockedTest('test2', 2000, 'shared')} + `, + }, { workers: 2 }); + // Together the tests exceed the 3000ms timeout; waiting for the lock is not test time. + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(2); + expect(conflictingOverlaps(result.outputLines, [['test1', 'test2']])).toEqual([]); +}); + +test('should validate lock in test details', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test } from '@playwright/test'; + test('test1', { lock: 42 }, async () => {}); + `, + }); + expect(result.exitCode).toBe(1); + expect(result.output).toContain('details.lock'); +}); diff --git a/tests/playwright-test/test-modifiers.spec.ts b/tests/playwright-test/test-modifiers.spec.ts index abe3f1682c5a9..9a31b5ae72461 100644 --- a/tests/playwright-test/test-modifiers.spec.ts +++ b/tests/playwright-test/test-modifiers.spec.ts @@ -695,10 +695,11 @@ test('static modifiers should be added in serial mode', async ({ runInlineTest } expect(result.passed).toBe(0); expect(result.skipped).toBe(2); expect(result.didNotRun).toBe(1); - expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([{ type: 'slow', location: { file: expect.any(String), line: 6, column: 14 } }]); - expect(result.report.suites[0].specs[1].tests[0].annotations).toEqual([{ type: 'fixme', location: { file: expect.any(String), line: 9, column: 12 } }]); - expect(result.report.suites[0].specs[2].tests[0].annotations).toEqual([{ type: 'skip', location: { file: expect.any(String), line: 11, column: 12 } }]); - expect(result.report.suites[0].specs[3].tests[0].annotations).toEqual([]); + const serial = { type: 'serial', location: { file: expect.any(String), line: 0, column: 0 } }; + expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([serial, { type: 'slow', location: { file: expect.any(String), line: 6, column: 14 } }]); + expect(result.report.suites[0].specs[1].tests[0].annotations).toEqual([serial, { type: 'fixme', location: { file: expect.any(String), line: 9, column: 12 } }]); + expect(result.report.suites[0].specs[2].tests[0].annotations).toEqual([serial, { type: 'skip', location: { file: expect.any(String), line: 11, column: 12 } }]); + expect(result.report.suites[0].specs[3].tests[0].annotations).toEqual([serial]); }); test('should contain only one slow modifier', async ({ runInlineTest }) => { diff --git a/tests/playwright-test/test-server.spec.ts b/tests/playwright-test/test-server.spec.ts index e5cc1c31b2330..5cc9544da8967 100644 --- a/tests/playwright-test/test-server.spec.ts +++ b/tests/playwright-test/test-server.spec.ts @@ -18,7 +18,6 @@ import { test as baseTest, expect } from './ui-mode-fixtures'; import { TestServerConnection } from '../../packages/playwright/lib/isomorphic'; -import { playwrightCtConfigText } from './playwright-test-fixtures'; import ws from 'ws'; import type { TestChildProcess } from '../config/commonFixtures'; @@ -80,20 +79,16 @@ const test = baseTest.extend<{ startTestServer: (options?: { env?: NodeJS.Proces } }); -const ctFiles = { - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - 'src/button.tsx': ` - export const Button = () => ; +const filesWithDependency = { + 'src/button.ts': ` + export const label = 'Button'; `, - 'src/button.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button'; + 'src/button.test.ts': ` + import { test, expect } from '@playwright/test'; + import { label } from './button'; - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button', { timeout: 1 }); + test('pass', async () => { + expect(label).toBe('Button'); }); `, }; @@ -199,21 +194,21 @@ test('find related test files errors', async ({ startTestServer, writeFiles }) = }); test('find related test files', async ({ startTestServer, writeFiles }) => { - await writeFiles(ctFiles); + await writeFiles(filesWithDependency); const testServerConnection = await startTestServer(); await testServerConnection.initialize({ interceptStdio: true }); expect((await testServerConnection.runGlobalSetup({})).status).toBe('passed'); - const buttonTsx = test.info().outputPath('src/button.tsx'); - const buttonTestTsx = test.info().outputPath('src/button.test.tsx'); - const result = await testServerConnection.findRelatedTestFiles({ files: [buttonTsx] }); - expect(result).toEqual({ testFiles: [buttonTestTsx] }); + const buttonTs = test.info().outputPath('src/button.ts'); + const buttonTestTs = test.info().outputPath('src/button.test.ts'); + const result = await testServerConnection.findRelatedTestFiles({ files: [buttonTs] }); + expect(result).toEqual({ testFiles: [buttonTestTs] }); expect((await testServerConnection.runGlobalTeardown({})).status).toBe('passed'); }); test('clear cache', async ({ startTestServer, writeFiles }) => { - await writeFiles(ctFiles); + await writeFiles(filesWithDependency); const testServerConnection = await startTestServer(); await testServerConnection.initialize({ interceptStdio: true }); expect((await testServerConnection.runGlobalSetup({})).status).toBe('passed'); diff --git a/tests/playwright-test/test-step.spec.ts b/tests/playwright-test/test-step.spec.ts index 0a994c6d8576f..af90f0215dbbf 100644 --- a/tests/playwright-test/test-step.spec.ts +++ b/tests/playwright-test/test-step.spec.ts @@ -77,7 +77,8 @@ export default class MyReporter implements Reporter { location = formatLocation(step.location); const skip = step.annotations?.find(a => a.type === 'skip'); const skipped = skip?.description ? ' (skipped: ' + skip.description + ')' : skip ? ' (skipped)' : ''; - console.log(formatPrefix(step.category) + indent + step.title + location + skipped); + const title = step.subtitle ? step.title + ' ' + step.subtitle : step.title; + console.log(formatPrefix(step.category) + indent + title + location + skipped); if (step.error) { const errorLocation = this.printErrorLocation ? formatLocation(step.error.location) : ''; console.log(formatPrefix(step.category) + indent + '↪ error: ' + this.trimError(step.error.message!) + errorLocation); @@ -1001,12 +1002,12 @@ pw:api | Create context fixture | Fixture "page" pw:api | Create page expect |Expect "toPass" @ a.test.ts:11 -pw:api | Navigate to "about:blank" @ a.test.ts:6 +pw:api | Navigate about:blank @ a.test.ts:6 test.step | inner step attempt: 0 @ a.test.ts:7 test.step | ↪ error: Error: expect(received).toBe(expected) // Object.is equality expect | Expect "toBe" @ a.test.ts:9 expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality -pw:api | Navigate to "about:blank" @ a.test.ts:6 +pw:api | Navigate about:blank @ a.test.ts:6 test.step | inner step attempt: 1 @ a.test.ts:7 expect | Expect "toBe" @ a.test.ts:9 hook |After Hooks @@ -1053,12 +1054,12 @@ pw:api | Create context fixture | Fixture "page" pw:api | Create page expect |Expect "poll toHaveLength" @ a.test.ts:14 -pw:api | Navigate to "about:blank" @ a.test.ts:7 +pw:api | Navigate about:blank @ a.test.ts:7 test.step | inner step attempt: 0 @ a.test.ts:8 expect | Expect "toBe" @ a.test.ts:10 expect | Expect "toHaveLength" @ a.test.ts:6 expect | ↪ error: Error: expect(received).toHaveLength(expected) -pw:api | Navigate to "about:blank" @ a.test.ts:7 +pw:api | Navigate about:blank @ a.test.ts:7 test.step | inner step attempt: 1 @ a.test.ts:8 expect | Expect "toBe" @ a.test.ts:10 expect | Expect "toHaveLength" @ a.test.ts:6 @@ -1246,12 +1247,12 @@ pw:api | Create page fixture | Fixture "request" pw:api | Create request context pw:api |Wait for navigation @ a.test.ts:5 -pw:api |Navigate to "data:" @ a.test.ts:6 +pw:api |Navigate data: @ a.test.ts:6 pw:api |Click locator('button') @ a.test.ts:8 pw:api |Click getByRole('button') @ a.test.ts:9 -pw:api |GET "/empty.html" @ a.test.ts:10 +pw:api |GET ${server.HOST}/empty.html @ a.test.ts:10 pw:api |↪ error: -pw:api |GET "/empty.html" @ a.test.ts:11 +pw:api |GET ${server.HOST}/empty.html @ a.test.ts:11 pw:api |↪ error: hook |After Hooks fixture | Fixture "request" @@ -1463,7 +1464,7 @@ fixture | Fixture "context" pw:api | Create context fixture | Fixture "page" pw:api | Create page -pw:api |Navigate to "/empty.html" @ a.test.ts:4 +pw:api |Navigate ${server.HOST}/empty.html @ a.test.ts:4 pw:api |Set content @ a.test.ts:5 test.step |custom step @ a.test.ts:6 pw:api | Wait for event "response" @ a.test.ts:7 @@ -1510,7 +1511,7 @@ fixture | Fixture "page" pw:api | Create page pw:api |Wait for event "request" @ a.test.ts:5 pw:api |Wait for event "response" @ a.test.ts:6 -pw:api |Navigate to "/empty.html" @ a.test.ts:7 +pw:api |Navigate ${server.HOST}/empty.html @ a.test.ts:7 hook |After Hooks fixture | Fixture "page" fixture | Fixture "context" @@ -1553,8 +1554,8 @@ pw:api | Create context fixture | Fixture "page" pw:api | Create page test.step |custom step @ a.test.ts:4 -pw:api | Navigate to "/empty.html" @ a.test.ts:12 -pw:api | GET "/empty.html" @ a.test.ts:6 +pw:api | Navigate ${server.HOST}/empty.html @ a.test.ts:12 +pw:api | GET ${server.HOST}/empty.html @ a.test.ts:6 expect | Expect "toBe" @ a.test.ts:8 hook |After Hooks fixture | Fixture "page" @@ -1766,19 +1767,19 @@ pw:api | Create page pw:api |Set content @ a.test.ts:16 expect |Expect "toBeInvisible" locator('div') @ a.test.ts:17 expect | Expect "poll toBe" @ a.test.ts:7 -pw:api | Query count locator('div').filter({ visible: true }) @ a.test.ts:7 +pw:api | Query count locator('div').visible() @ a.test.ts:7 expect | Expect "toBe" @ a.test.ts:7 expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality -pw:api | Query count locator('div').filter({ visible: true }) @ a.test.ts:7 +pw:api | Query count locator('div').visible() @ a.test.ts:7 expect | Expect "toBe" @ a.test.ts:7 expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality -pw:api | Query count locator('div').filter({ visible: true }) @ a.test.ts:7 +pw:api | Query count locator('div').visible() @ a.test.ts:7 expect | Expect "toBe" @ a.test.ts:7 expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality -pw:api | Query count locator('div').filter({ visible: true }) @ a.test.ts:7 +pw:api | Query count locator('div').visible() @ a.test.ts:7 expect | Expect "toBe" @ a.test.ts:7 expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality -pw:api | Query count locator('div').filter({ visible: true }) @ a.test.ts:7 +pw:api | Query count locator('div').visible() @ a.test.ts:7 expect | Expect "toBe" @ a.test.ts:7 pw:api |Wait for timeout @ a.test.ts:18 pw:api |Set content @ a.test.ts:19 @@ -1844,10 +1845,10 @@ pw:api | Create page fixture | Fixture "bar" @ a.test.ts:4 pw:api | Set content @ a.test.ts:14 test.step | inner step @ a.test.ts:15 -pw:api | Navigate to "data:" @ a.test.ts:16 +pw:api | Navigate data: @ a.test.ts:16 pw:api | Set content @ a.test.ts:22 test.step | inner step @ a.test.ts:23 -pw:api | Navigate to "data:" @ a.test.ts:24 +pw:api | Navigate data: @ a.test.ts:24 expect |Expect "toBeVisible" locator('body') @ a.test.ts:32 expect |Expect "toBe" @ a.test.ts:33 expect |Expect "toBe" @ a.test.ts:34 @@ -1861,3 +1862,150 @@ fixture | Fixture "context" pw:api | Close context `); }); + +test('should report step params', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'reporter.ts': ` + import type { Reporter, TestCase, TestResult, TestStep } from '@playwright/test/reporter'; + export default class MyReporter implements Reporter { + onStepEnd(test: TestCase, result: TestResult, step: TestStep) { + if (step.location?.file.endsWith('a.test.ts')) + console.log('%%' + step.category + ' | ' + step.title + ' | ' + step.subtitle + ' | ' + JSON.stringify(step.params)); + } + } + `, + 'playwright.config.ts': ` + module.exports = { reporter: './reporter' }; + `, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('pass', async ({ page }) => { + await page.goto('about:blank'); + await page.setContent(''); + await page.getByRole('button').click(); + await expect(page.getByRole('button')).toBeVisible(); + await test.step('my step', async () => {}, { params: { foo: 'bar', count: 7 } }); + }); + ` + }, { reporter: '' }); + + expect(result.exitCode).toBe(0); + expect(result.outputLines).toEqual([ + `pw:api | Navigate | about:blank | {"url":"about:blank"}`, + `pw:api | Set content | undefined | undefined`, + `pw:api | Click | getByRole('button') | {"locator":"getByRole('button')"}`, + `expect | Expect "toBeVisible" | getByRole('button') | {"locator":"getByRole('button')"}`, + `test.step | my step | undefined | {"foo":"bar","count":7}`, + ]); +}); + +test('should report step subtitle', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'reporter.ts': ` + import type { Reporter, TestCase, TestResult, TestStep } from '@playwright/test/reporter'; + export default class MyReporter implements Reporter { + onStepEnd(test: TestCase, result: TestResult, step: TestStep) { + if (step.category === 'test.step') + console.log('%%' + step.title + ' | ' + step.subtitle); + } + } + `, + 'playwright.config.ts': ` + module.exports = { reporter: './reporter' }; + `, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('pass', async ({}) => { + await test.step('my step', async () => {}, { subtitle: 'my subtitle' }); + await test.step.skip('skipped step', async () => {}, { subtitle: 'skipped subtitle' }); + await test.step('plain step', async () => {}); + }); + ` + }, { reporter: '' }); + + expect(result.exitCode).toBe(0); + expect(result.outputLines).toEqual([ + `my step | my subtitle`, + `skipped step | skipped subtitle`, + `plain step | undefined`, + ]); +}); + +test('should report input step params', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'reporter.ts': ` + import type { Reporter, TestCase, TestResult, TestStep } from '@playwright/test/reporter'; + export default class MyReporter implements Reporter { + onStepEnd(test: TestCase, result: TestResult, step: TestStep) { + if (step.location?.file.endsWith('a.test.ts')) + console.log('%%' + (step.subtitle ? step.title + ' ' + step.subtitle : step.title) + ' | ' + JSON.stringify(step.params)); + } + } + `, + 'playwright.config.ts': ` + module.exports = { reporter: './reporter' }; + `, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('pass', async ({ page }) => { + await page.setContent(''); + await page.locator('#i').fill('value'); + await page.locator('#i').press('Enter'); + await page.keyboard.type('typed'); + await page.getByRole('button').click({ button: 'right', clickCount: 2, modifiers: ['Shift'], position: { x: 3, y: 4 } }); + await page.mouse.move(10, 20); + await page.mouse.wheel(0, 100); + await page.locator('select').selectOption('b'); + await page.dispatchEvent('#i', 'focus'); + await page.locator('#i').waitFor({ state: 'visible' }); + await page.setViewportSize({ width: 800, height: 600 }); + await page.dragAndDrop('#i', 'select'); + await page.evaluate(() => 1); + }); + ` + }, { reporter: '' }); + + expect(result.exitCode).toBe(0); + expect(result.outputLines).toEqual([ + `Set content | undefined`, + `Fill "value" locator('#i') | {"locator":"locator('#i')","value":"value"}`, + `Press "Enter" locator('#i') | {"locator":"locator('#i')","key":"Enter"}`, + `Type "typed" | {"text":"typed"}`, + `Click getByRole('button') | {"locator":"getByRole('button')","button":"right","clickCount":2,"modifiers":["Shift"],"position":{"x":3,"y":4}}`, + `Mouse move | {"x":10,"y":20}`, + `Mouse wheel | {"deltaX":0,"deltaY":100}`, + `Select option locator('select') | {"locator":"locator('select')","options":[{"valueOrLabel":"b"}]}`, + `Dispatch "focus" locator('#i') | {"locator":"locator('#i')","type":"focus"}`, + `Wait for selector locator('#i') | {"locator":"locator('#i')","state":"visible"}`, + `Set viewport size | {"width":800,"height":600}`, + `Drag and drop | {"source":"locator('#i')","target":"locator('select')"}`, + `Evaluate | undefined`, + ]); +}); + +test('should truncate long step params', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'reporter.ts': ` + import type { Reporter, TestCase, TestResult, TestStep } from '@playwright/test/reporter'; + export default class MyReporter implements Reporter { + onStepEnd(test: TestCase, result: TestResult, step: TestStep) { + if (step.params?.value) + console.log('%%' + step.params.value); + } + } + `, + 'playwright.config.ts': ` + module.exports = { reporter: './reporter' }; + `, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('pass', async ({ page }) => { + await page.setContent(''); + await page.locator('#i').fill('x'.repeat(1000)); + }); + ` + }, { reporter: '' }); + + expect(result.exitCode).toBe(0); + expect(result.outputLines).toEqual(['x'.repeat(200) + '\u2026']); +}); diff --git a/tests/playwright-test/to-have-screenshot.spec.ts b/tests/playwright-test/to-have-screenshot.spec.ts index 22551fa95d1c8..9e83293d77c4e 100644 --- a/tests/playwright-test/to-have-screenshot.spec.ts +++ b/tests/playwright-test/to-have-screenshot.spec.ts @@ -478,7 +478,7 @@ test('should fail to screenshot an element with infinite animation', async ({ ru }); expect(result.exitCode).toBe(1); expect(result.output).toContain(`Timeout 2000ms exceeded`); - expect(result.output).toContain(`Expect "toHaveScreenshot" with timeout 2000ms`); + expect(result.output).toContain(`Expect "toHaveScreenshot" locator('body') with timeout 2000ms`); expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-previous.png'))).toBe(true); expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-actual.png'))).toBe(true); expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-expected.png'))).toBe(false); diff --git a/tests/playwright-test/types-2.spec.ts b/tests/playwright-test/types-2.spec.ts index 79b36aacdc0ec..6fb76ec658563 100644 --- a/tests/playwright-test/types-2.spec.ts +++ b/tests/playwright-test/types-2.spec.ts @@ -28,6 +28,9 @@ test('basics should work', async ({ runTSC }) => { test('my test', async({}, testInfo) => { expect(testInfo.title).toBe('my test'); testInfo.annotations[0].type; + await test.step('step', async stepInfo => { + stepInfo.annotations.push({ type: 'expected-result', description: 'step passes' }); + }); test.setTimeout(123); testInfo.snapshotPath('a', 'b'); testInfo.snapshotPath(); diff --git a/tests/playwright-test/types.spec.ts b/tests/playwright-test/types.spec.ts index e9fea813f066c..aa62063f25502 100644 --- a/tests/playwright-test/types.spec.ts +++ b/tests/playwright-test/types.spec.ts @@ -357,16 +357,6 @@ test('should check types of fixtures', async ({ runTSC }) => { timeout: 2, }); `, - 'playwright-define-merge-ct.config.ts': ` - import { defineConfig } from '@playwright/experimental-ct-vue'; - const config0 = defineConfig({ - timeout: 1, - // @ts-expect-error - grep: 23, - }, { - timeout: 2, - }); - `, }); expect(result.exitCode).toBe(0); }); diff --git a/tests/playwright-test/ui-mode-test-ct.spec.ts b/tests/playwright-test/ui-mode-test-ct.spec.ts deleted file mode 100644 index 48683580a71d1..0000000000000 --- a/tests/playwright-test/ui-mode-test-ct.spec.ts +++ /dev/null @@ -1,387 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect, retries, dumpTestTree } from './ui-mode-fixtures'; - -test.describe.configure({ mode: 'parallel', retries }); - -const basicTestTree = { - 'playwright.config.ts': ` - import { defineConfig } from '@playwright/experimental-ct-react'; - export default defineConfig({ - use: { - ctPort: ${3200 + (+process.env.TEST_PARALLEL_INDEX)} - } - }); - `, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - 'src/button.tsx': ` - export const Button = () => ; - `, - 'src/button.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button'; - - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button', { timeout: 1 }); - }); - `, -}; - -test('should run component tests', async ({ runUITest }) => { - const { page } = await runUITest(basicTestTree); - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ◯ button.test.tsx - ◯ pass - `); - await page.getByTitle('Run all').click(); - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ✅ button.test.tsx - ✅ pass - `); -}); - -test('should run component tests after editing test', async ({ runUITest, writeFiles }) => { - const { page } = await runUITest(basicTestTree); - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ◯ button.test.tsx - ◯ pass - `); - await page.getByTitle('Run all').click(); - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ✅ button.test.tsx - ✅ pass - `); - - await writeFiles({ - 'src/button.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button'; - - test('fail', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button2', { timeout: 1 }); - }); - ` - }); - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ◯ button.test.tsx - ◯ fail - `); - await page.getByTitle('Run all').click(); - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ❌ button.test.tsx - ❌ fail <= - `); -}); - -test('should run component tests after editing component', async ({ runUITest, writeFiles }) => { - const { page } = await runUITest(basicTestTree); - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ◯ button.test.tsx - ◯ pass - `); - await page.getByTitle('Run all').click(); - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ✅ button.test.tsx - ✅ pass - `); - - await writeFiles({ - 'src/button.tsx': ` - export const Button = () => ; - ` - }); - await page.getByTitle('Run all').click(); - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ❌ button.test.tsx - ❌ pass <= - `); -}); - -test('should run component tests after editing test and component', async ({ runUITest, writeFiles }) => { - const { page } = await runUITest(basicTestTree); - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ◯ button.test.tsx - ◯ pass - `); - await page.getByTitle('Run all').click(); - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ✅ button.test.tsx - ✅ pass - `); - - await writeFiles({ - 'src/button.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button'; - - test('pass 2', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button2', { timeout: 1 }); - }); - `, - 'src/button.tsx': ` - export const Button = () => ; - ` - }); - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ◯ button.test.tsx - ◯ pass 2 - `); - - await page.getByTitle('Run all').click(); - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ✅ button.test.tsx - ✅ pass 2 - `); -}); - -test('should watch test', async ({ runUITest, writeFiles }) => { - const { page } = await runUITest(basicTestTree); - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ◯ button.test.tsx - ◯ pass - `); - - await page.getByTitle('Watch all').click(); - await page.getByTitle('Run all').click(); - - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ✅ button.test.tsx - ✅ pass - `); - - await writeFiles({ - 'src/button.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button'; - - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button2', { timeout: 1 }); - }); - ` - }); - - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ❌ button.test.tsx - ❌ pass <= - `); -}); - -test('should watch component', async ({ runUITest, writeFiles }) => { - const { page } = await runUITest(basicTestTree); - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ◯ button.test.tsx - ◯ pass - `); - - await page.getByTitle('Watch all').click(); - await page.getByTitle('Run all').click(); - - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ✅ button.test.tsx - ✅ pass - `); - - await writeFiles({ - 'src/button.tsx': ` - export const Button = () => ; - ` - }); - - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ❌ button.test.tsx - ❌ pass <= - `); -}); - -test('should watch component via util', async ({ runUITest, writeFiles }) => { - const { page } = await runUITest({ - ...basicTestTree, - 'src/button.tsx': undefined, - 'src/button.ts': ` - import { Button } from './buttonComponent'; - export { Button }; - `, - 'src/buttonComponent.tsx': ` - export const Button = () => ; - `, - }); - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ◯ button.test.tsx - ◯ pass - `); - - await page.getByTitle('Watch all').click(); - await page.getByTitle('Run all').click(); - - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ✅ button.test.tsx - ✅ pass - `); - - await writeFiles({ - 'src/buttonComponent.tsx': ` - export const Button = () => ; - ` - }); - - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ❌ button.test.tsx - ❌ pass <= - `); -}); - -test('should watch component when editing util', async ({ runUITest, writeFiles }) => { - const { page } = await runUITest({ - ...basicTestTree, - 'src/button.tsx': undefined, - 'src/button.ts': ` - import { Button } from './buttonComponent'; - export { Button }; - `, - 'src/buttonComponent.tsx': ` - export const Button = () => ; - `, - 'src/buttonComponent2.tsx': ` - export const Button = () => ; - `, - }); - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ◯ button.test.tsx - ◯ pass - `); - - await page.getByTitle('Watch all').click(); - await page.getByTitle('Run all').click(); - - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ✅ button.test.tsx - ✅ pass - `); - - await writeFiles({ - 'src/button.ts': ` - import { Button } from './buttonComponent2'; - export { Button }; - `, - }); - - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ❌ button.test.tsx - ❌ pass <= - `); -}); - -test('should watch component when editing inline css', async ({ runUITest, writeFiles }) => { - const { page } = await runUITest({ - ...basicTestTree, - 'src/button.tsx': undefined, - 'src/button.ts': ` - import { Button } from './buttonComponent'; - export { Button }; - `, - 'src/buttonComponent.tsx': ` - import cssText from './style.css?inline'; - export const Button = () => ; - `, - 'src/style.css': ` - .button{color:red} - `, - 'src/button.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button'; - - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('.button{color:red}', { timeout: 1 }); - }); - `, - }); - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ◯ button.test.tsx - ◯ pass - `); - - await page.getByTitle('Watch all').click(); - await page.getByTitle('Run all').click(); - - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ✅ button.test.tsx - ✅ pass - `); - - await writeFiles({ - 'src/style.css': ` - .button{color:blue} - `, - }); - - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ❌ button.test.tsx - ❌ pass <= - `); -}); - -test('should watch component when editing story import chain', async ({ runUITest, writeFiles }) => { - const { page } = await runUITest({ - ...basicTestTree, - 'src/button.tsx': undefined, - 'src/button.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { StoryButton } from './button.story'; - - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button', { timeout: 1 }); - }); - `, - 'src/button.story.tsx': ` - import { Button } from './buttonComponent'; - export const StoryButton = () => ; - `, - 'src/buttonComponent.tsx': ` - export const Button = () => ; - `, - }); - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ◯ button.test.tsx - ◯ pass - `); - - await page.getByTitle('Watch all').click(); - await page.getByTitle('Run all').click(); - - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ✅ button.test.tsx - ✅ pass - `); - - await writeFiles({ - 'src/buttonComponent.tsx': ` - export const Button = () => ; - `, - }); - - await expect.poll(dumpTestTree(page)).toBe(` - ▼ ❌ button.test.tsx - ❌ pass <= - `); -}); diff --git a/tests/playwright-test/ui-mode-test-filters.spec.ts b/tests/playwright-test/ui-mode-test-filters.spec.ts index ef72b00b5759a..f549d150e4b09 100644 --- a/tests/playwright-test/ui-mode-test-filters.spec.ts +++ b/tests/playwright-test/ui-mode-test-filters.spec.ts @@ -72,6 +72,25 @@ test('should display native tags and filter by them on click', async ({ runUITes `); }); +test('should toggle filters from the keyboard', async ({ runUITest }) => { + const { page } = await runUITest(basicTestTree); + const summary = page.locator('.filter-summary'); + + await expect(summary).toHaveRole('button'); + await expect(summary).toHaveAttribute('aria-expanded', 'false'); + + await summary.focus(); + await expect(summary).toBeFocused(); + + await summary.press('Enter'); + await expect(page.getByTestId('status-filters')).toBeVisible(); + await expect(summary).toHaveAttribute('aria-expanded', 'true'); + + await summary.press('Space'); + await expect(page.getByTestId('status-filters')).toBeHidden(); + await expect(summary).toHaveAttribute('aria-expanded', 'false'); +}); + test('should filter by status', async ({ runUITest }) => { const { page } = await runUITest(basicTestTree); diff --git a/tests/playwright-test/ui-mode-test-network-tab.spec.ts b/tests/playwright-test/ui-mode-test-network-tab.spec.ts index 406336e75cae2..da3369f843b53 100644 --- a/tests/playwright-test/ui-mode-test-network-tab.spec.ts +++ b/tests/playwright-test/ui-mode-test-network-tab.spec.ts @@ -283,6 +283,43 @@ test('should pretty-print response bodies and show formatting errors', async ({ await expect(prettyPrintError).toBeVisible(); }); +test('should pretty-print JSON without losing number precision', async ({ runUITest, server }) => { + server.setRoute('/response-bigint', (_, res) => res.setHeader('Content-Type', 'application/json').end('{"id":12345678901234567890,"amount":9007199254740993,"ratio":1.0,"scaled":1e3}')); + + const { page } = await runUITest({ + 'network-tab.test.ts': ` + import { test } from '@playwright/test'; + test('network response tab', async ({ request }) => { + await request.get('${server.PREFIX}/response-bigint').then(res => res.text()); + }); + `, + }); + + await page.getByText('network response tab').dblclick(); + await expect(page.getByTestId('workbench-run-status')).toContainText('Passed'); + await page.getByRole('tab', { name: 'Network' }).click(); + + await page.getByRole('listbox', { name: 'Network requests' }).getByRole('option').filter({ hasText: 'response-bigint' }).click(); + await page.getByRole('tabpanel', { name: 'Network' }).getByRole('tab', { name: 'Response' }).click(); + + const responsePanel = page.getByRole('tabpanel', { name: 'Response' }); + await expect(responsePanel.locator('.CodeMirror-code .CodeMirror-line')).toHaveText([ + '{', + ' "id": 12345678901234567890,', + ' "amount": 9007199254740993,', + ' "ratio": 1.0,', + ' "scaled": 1e3', + '}', + ], { useInnerText: true }); + await expect(responsePanel.getByTitle('Formatting failed')).toBeHidden(); + + // Raw view keeps the exact bytes as well. + await responsePanel.getByRole('button', { name: 'Pretty print', exact: true }).click(); + await expect(responsePanel.locator('.CodeMirror-code .CodeMirror-line')).toHaveText([ + '{"id":12345678901234567890,"amount":9007199254740993,"ratio":1.0,"scaled":1e3}', + ], { useInnerText: true }); +}); + test('should display list of query parameters (only if present)', async ({ runUITest, server }) => { const { page } = await runUITest({ 'network-tab.test.ts': ` @@ -392,6 +429,35 @@ test('should toggle sections inside network details', async ({ runUITest, server await expect(headersPanel.getByRole('region', { name: 'General' })).toContainText(/Start.+Duration\d+ms/); }); +test('should toggle sections inside network details with keyboard', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42263' }, +}, async ({ runUITest, server }) => { + const { page } = await runUITest({ + 'network-tab.test.ts': ` + import { test, expect } from '@playwright/test'; + test('network tab test', async ({ page }) => { + await page.goto('${server.PREFIX}/network-tab/network.html'); + await page.evaluate(() => (window as any).donePromise); + }); + `, + }); + + await page.getByRole('treeitem', { name: 'network tab test' }).dblclick(); + await expect(page.getByTestId('workbench-run-status')).toContainText('Passed'); + + await page.getByRole('tab', { name: 'Network' }).click(); + await page.getByRole('option').filter({ hasText: 'post-data-1' }).click(); + const headersPanel = page.getByRole('tabpanel', { name: 'Headers' }); + + const header = headersPanel.getByRole('button', { name: 'Request Headers × 16' }); + await header.focus(); + await expect(header).toBeFocused(); + await header.press('Enter'); + await expect(headersPanel.getByRole('region', { name: 'Request Headers × 16' })).toBeHidden(); + await header.press(' '); + await expect(headersPanel.getByRole('region', { name: 'Request Headers × 16' })).toBeVisible(); +}); + test('should copy network request', async ({ runUITest, server }) => { const { page } = await runUITest({ 'network-tab.test.ts': ` diff --git a/tests/playwright-test/ui-mode-test-progress.spec.ts b/tests/playwright-test/ui-mode-test-progress.spec.ts index 932f3c5cf1303..e144c5e929ca7 100644 --- a/tests/playwright-test/ui-mode-test-progress.spec.ts +++ b/tests/playwright-test/ui-mode-test-progress.spec.ts @@ -55,13 +55,13 @@ test('should update trace live', async ({ runUITest, server }) => { 'action list' ).toHaveText([ /Before Hooks[\d.]+m?s/, - /Navigate to "\/one.html"/, + /Navigate.*\/one.html/, ]); await expect( listItem.locator(':scope[aria-selected="true"]'), 'last action to be selected' - ).toHaveText(/Navigate to/); + ).toHaveText(/Navigate/); await expect( listItem.locator(':scope[aria-selected="true"] .codicon.codicon-loading'), 'spinner' @@ -81,13 +81,13 @@ test('should update trace live', async ({ runUITest, server }) => { ).toHaveText('One'); await expect(listItem).toHaveText([ /Before Hooks[\d.]+m?s/, - /Navigate to "\/one.html"/, - /Navigate to "\/two.html"/ + /Navigate.*\/one.html/, + /Navigate.*\/two.html/ ]); await expect( listItem.locator(':scope[aria-selected="true"]'), 'last action to be selected' - ).toHaveText(/Navigate to/); + ).toHaveText(/Navigate/); await expect( listItem.locator(':scope[aria-selected="true"] .codicon.codicon-loading'), 'spinner' @@ -108,8 +108,8 @@ test('should update trace live', async ({ runUITest, server }) => { await expect(listItem).toHaveText([ /Before Hooks[\d.]+m?s/, - /Navigate to "\/one.html"/, - /Navigate to "\/two.html"/, + /Navigate.*\/one.html/, + /Navigate.*\/two.html/, /After Hooks[\d.]+m?s/, ]); }); @@ -140,12 +140,12 @@ test('should preserve action list selection upon live trace update', async ({ ru 'action list' ).toHaveText([ /Before Hooks[\d.]+m?s/, - /Navigate to "about:blank"/, + /Navigate.*about:blank/, /Set content/, ]); // Manually select page.goto. - await page.getByTestId('actions-tree').getByText('Navigate to').click(); + await page.getByTestId('actions-tree').getByText('Navigate').click(); // Generate more actions and check that we are still on the page.goto action. latch.open(); @@ -154,14 +154,14 @@ test('should preserve action list selection upon live trace update', async ({ ru 'action list' ).toHaveText([ /Before Hooks[\d.]+m?s/, - /Navigate to "about:blank"/, + /Navigate.*about:blank/, /Set content/, /Set content/, ]); await expect( listItem.locator(':scope[aria-selected="true"]'), 'selected action stays the same' - ).toHaveText(/Navigate to/); + ).toHaveText(/Navigate/); }); test('should update tracing network live', async ({ runUITest, server }) => { @@ -201,7 +201,7 @@ test('should update tracing network live', async ({ runUITest, server }) => { 'action list' ).toHaveText([ /Before Hooks[\d.]+m?s/, - /Navigate to "\/one.html"/, + /Navigate.*\/one.html/, /Set content/, ]); @@ -241,7 +241,7 @@ test('should show trace w/ multiple contexts', async ({ runUITest, server, creat 'action list' ).toHaveText([ /Before Hooks[\d.]+m?s/, - /Navigate to "about:blank"/, + /Navigate.*about:blank/, ]); latch.open(); diff --git a/tests/playwright-test/ui-mode-trace.spec.ts b/tests/playwright-test/ui-mode-trace.spec.ts index b337fba3b44ff..14aa61d3e93a2 100644 --- a/tests/playwright-test/ui-mode-trace.spec.ts +++ b/tests/playwright-test/ui-mode-trace.spec.ts @@ -314,7 +314,7 @@ test('should not fail on internal page logs', async ({ runUITest, server }) => { /Before Hooks/, /Create context/, /Create page/, - /Navigate to "\/empty.html"/, + /Navigate.*\/empty.html/, /After Hooks/, ]); }); @@ -492,12 +492,12 @@ test('should filter actions tab on double-click', async ({ runUITest, server }) const actionsTree = page.getByTestId('actions-tree'); await expect(actionsTree.getByRole('treeitem')).toHaveText([ /Before Hooks/, - /Navigate to "\/empty.html"/, + /Navigate.*\/empty.html/, /After Hooks/, ]); - await actionsTree.getByRole('treeitem', { name: 'Navigate to "\/empty.html"' }).dblclick(); + await actionsTree.getByRole('treeitem', { name: 'Navigate' }).dblclick(); await expect(actionsTree.getByRole('treeitem')).toHaveText([ - /Navigate to "\/empty.html"/, + /Navigate.*\/empty.html/, ]); }); diff --git a/tests/playwright-test/update-aria-snapshot.spec.ts b/tests/playwright-test/update-aria-snapshot.spec.ts index ae11bfcaf35bc..bf9289a56de33 100644 --- a/tests/playwright-test/update-aria-snapshot.spec.ts +++ b/tests/playwright-test/update-aria-snapshot.spec.ts @@ -15,7 +15,7 @@ */ import * as fs from 'fs'; -import { test, expect, playwrightCtConfigText, stripAnsi } from './playwright-test-fixtures'; +import { test, expect, stripAnsi } from './playwright-test-fixtures'; import { execSync } from 'child_process'; test.describe.configure({ mode: 'parallel' }); @@ -299,21 +299,12 @@ test('should generate baseline with special characters', async ({ runInlineTest test('should update missing snapshots in tsx', async ({ runInlineTest }, testInfo) => { const result = await runInlineTest({ '.git/marker': '', - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - - 'src/button.tsx': ` - export const Button = () => ; - `, - 'src/button.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button.tsx'; + import { test, expect } from '@playwright/test'; - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toMatchAriaSnapshot(\`\`); + test('pass', async ({ page }) => { + await page.setContent(\`\`); + await expect(page.locator('body')).toMatchAriaSnapshot(\`\`); }); `, }); @@ -324,12 +315,12 @@ test('should update missing snapshots in tsx', async ({ runInlineTest }, testInf expect(trimPatch(data)).toBe(`diff --git a/src/button.test.tsx b/src/button.test.tsx --- a/src/button.test.tsx +++ b/src/button.test.tsx -@@ -4,6 +4,8 @@ +@@ -3,6 +3,8 @@ - test('pass', async ({ mount }) => { - const component = await mount(); -- await expect(component).toMatchAriaSnapshot(\`\`); -+ await expect(component).toMatchAriaSnapshot(\` + test('pass', async ({ page }) => { + await page.setContent(\`\`); +- await expect(page.locator('body')).toMatchAriaSnapshot(\`\`); ++ await expect(page.locator('body')).toMatchAriaSnapshot(\` + - button \"Button\" + \`); }); @@ -345,31 +336,21 @@ test('should update missing snapshots in tsx', async ({ runInlineTest }, testInf test('should update multiple files', async ({ runInlineTest }, testInfo) => { const result = await runInlineTest({ '.git/marker': '', - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - - 'src/button.tsx': ` - export const Button = () => ; - `, - - 'src/button-1.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button.tsx'; + 'src/button-1.test.ts': ` + import { test, expect } from '@playwright/test'; - test('pass 1', async ({ mount }) => { - const component = await mount(); - await expect(component).toMatchAriaSnapshot(\`\`); + test('pass 1', async ({ page }) => { + await page.setContent(\`\`); + await expect(page.locator('body')).toMatchAriaSnapshot(\`\`); }); `, - 'src/button-2.test.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button.tsx'; + 'src/button-2.test.ts': ` + import { test, expect } from '@playwright/test'; - test('pass 2', async ({ mount }) => { - const component = await mount(); - await expect(component).toMatchAriaSnapshot(\`\`); + test('pass 2', async ({ page }) => { + await page.setContent(\`\`); + await expect(page.locator('body')).toMatchAriaSnapshot(\`\`); }); `, }); @@ -378,38 +359,38 @@ test('should update multiple files', async ({ runInlineTest }, testInfo) => { expect(stripAnsi(result.output).replace(/\\/g, '/')).toContain(`New baselines created for: - src/button-1.test.tsx - src/button-2.test.tsx + src/button-1.test.ts + src/button-2.test.ts git apply test-results/rebaselines.patch `); const patchPath = testInfo.outputPath('test-results/rebaselines.patch'); const data = fs.readFileSync(patchPath, 'utf-8'); - expect(trimPatch(data)).toBe(`diff --git a/src/button-1.test.tsx b/src/button-1.test.tsx ---- a/src/button-1.test.tsx -+++ b/src/button-1.test.tsx -@@ -4,6 +4,8 @@ - - test('pass 1', async ({ mount }) => { - const component = await mount(); -- await expect(component).toMatchAriaSnapshot(\`\`); -+ await expect(component).toMatchAriaSnapshot(\` + expect(trimPatch(data)).toBe(`diff --git a/src/button-1.test.ts b/src/button-1.test.ts +--- a/src/button-1.test.ts ++++ b/src/button-1.test.ts +@@ -3,6 +3,8 @@ + + test('pass 1', async ({ page }) => { + await page.setContent(\`\`); +- await expect(page.locator('body')).toMatchAriaSnapshot(\`\`); ++ await expect(page.locator('body')).toMatchAriaSnapshot(\` + - button \"Button\" + \`); }); \\ No newline at end of file -diff --git a/src/button-2.test.tsx b/src/button-2.test.tsx ---- a/src/button-2.test.tsx -+++ b/src/button-2.test.tsx -@@ -4,6 +4,8 @@ +diff --git a/src/button-2.test.ts b/src/button-2.test.ts +--- a/src/button-2.test.ts ++++ b/src/button-2.test.ts +@@ -3,6 +3,8 @@ - test('pass 2', async ({ mount }) => { - const component = await mount(); -- await expect(component).toMatchAriaSnapshot(\`\`); -+ await expect(component).toMatchAriaSnapshot(\` + test('pass 2', async ({ page }) => { + await page.setContent(\`\`); +- await expect(page.locator('body')).toMatchAriaSnapshot(\`\`); ++ await expect(page.locator('body')).toMatchAriaSnapshot(\` + - button \"Button\" + \`); }); diff --git a/tests/playwright-test/watch.spec.ts b/tests/playwright-test/watch.spec.ts index e32239534ba6c..02c6e0bbb9340 100644 --- a/tests/playwright-test/watch.spec.ts +++ b/tests/playwright-test/watch.spec.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import path from 'path'; import timers from 'timers/promises'; -import { test, expect, playwrightCtConfigText } from './playwright-test-fixtures'; +import { test, expect } from './playwright-test-fixtures'; test.describe.configure({ mode: 'parallel' }); @@ -687,129 +686,6 @@ test('should not watch unfiltered files', async ({ runWatchTest, writeFiles }) = await testProcess.waitForOutput('Waiting for file changes.'); }); -test('should run CT on changed deps', async ({ runWatchTest, writeFiles }) => { - const testProcess = await runWatchTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - 'src/button.tsx': ` - export const Button = () => ; - `, - 'src/button.spec.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button'; - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button', { timeout: 1000 }); - }); - `, - 'src/link.spec.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - test('pass', async ({ mount }) => { - const component = await mount(hello); - await expect(component).toHaveText('hello'); - }); - `, - }, undefined, { PWTEST_RECOVERY_DISABLED: '1' }); - await testProcess.waitForOutput('Waiting for file changes.'); - await writeFiles({ - 'src/button.tsx': ` - export const Button = () => ; - `, - }); - - await testProcess.waitForOutput(`src${path.sep}button.spec.tsx:4:11 › pass`); - expect(testProcess.output).not.toContain(`src${path.sep}link.spec.tsx`); - await testProcess.waitForOutput(`Error: expect(locator).toHaveText(expected) failed`); - await testProcess.waitForOutput('Timeout: 1000ms'); - await testProcess.waitForOutput('Waiting for file changes.'); -}); - -test('should run CT on indirect deps change', async ({ runWatchTest, writeFiles }) => { - const testProcess = await runWatchTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - 'src/button.css': ` - button { color: red; } - `, - 'src/button.tsx': ` - import './button.css'; - export const Button = () => ; - `, - 'src/helper.tsx': ` - import { Button } from "./button"; - export const buttonInstance = - `, - 'src/button.spec.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { buttonInstance } from './helper'; - test('pass', async ({ mount }) => { - const component = await mount(buttonInstance); - await expect(component).toHaveText('Button', { timeout: 1000 }); - }); - `, - 'src/link.spec.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - test('pass', async ({ mount }) => { - const component = await mount(hello); - await expect(component).toHaveText('hello'); - }); - `, - }); - await testProcess.waitForOutput('Waiting for file changes.'); - await writeFiles({ - 'src/button.css': ` - button { color: blue; } - `, - }); - - await testProcess.waitForOutput(`src${path.sep}button.spec.tsx:4:11 › pass`); - expect(testProcess.output).not.toContain(`src${path.sep}link.spec.tsx`); - await testProcess.waitForOutput('Waiting for file changes.'); -}); - -test('should run CT on indirect deps change ESM mode', async ({ runWatchTest, writeFiles }) => { - const testProcess = await runWatchTest({ - 'playwright.config.ts': playwrightCtConfigText, - 'package.json': `{ "type": "module" }`, - 'playwright/index.html': ``, - 'playwright/index.ts': ``, - 'src/button.css': ` - button { color: red; } - `, - 'src/button.tsx': ` - import './button.css'; - export const Button = () => ; - `, - 'src/button.spec.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - import { Button } from './button.jsx'; - test('pass', async ({ mount }) => { - const component = await mount(); - await expect(component).toHaveText('Button', { timeout: 1000 }); - }); - `, - 'src/link.spec.tsx': ` - import { test, expect } from '@playwright/experimental-ct-react'; - test('pass', async ({ mount }) => { - const component = await mount(hello); - await expect(component).toHaveText('hello'); - }); - `, - }); - await testProcess.waitForOutput('Waiting for file changes.'); - await writeFiles({ - 'src/button.css': ` - button { color: blue; } - `, - }); - - await testProcess.waitForOutput(`src${path.sep}button.spec.tsx:4:7 › pass`); - expect(testProcess.output).not.toContain(`src${path.sep}link.spec.tsx`); - await testProcess.waitForOutput('Waiting for file changes.'); -}); - test('should run global teardown before exiting', async ({ runWatchTest }) => { const testProcess = await runWatchTest({ 'playwright.config.ts': ` diff --git a/tests/tsconfig.json b/tests/tsconfig.json index ad46142a25302..43d324cb50475 100644 --- a/tests/tsconfig.json +++ b/tests/tsconfig.json @@ -11,13 +11,11 @@ "baseUrl": "..", "paths": { "@dashboard/*": ["packages/dashboard/src/*"], - "@dvtools/*": ["packages/devtools/src/*"], "@injected/*": ["packages/injected/src/*"], "@isomorphic/*": ["packages/isomorphic/*"], "@utils/*": ["packages/utils/*"], "@testIsomorphic/*": ["packages/playwright/src/isomorphic/*"], "@recorder/*": ["packages/recorder/src/*"], - "@trace/*": ["packages/trace/src/*"], "@web/*": ["packages/web/src/*"], }, "esModuleInterop": true, diff --git a/tsconfig.json b/tsconfig.json index 8401d4f79c081..6526645a8fbd5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,6 @@ "@protocol/*": ["./packages/protocol/src/*"], "@recorder/*": ["./packages/recorder/src/*"], "@testIsomorphic/*": ["./packages/playwright/src/isomorphic/*"], - "@trace/*": ["./packages/trace/src/*"], "@web/*": ["./packages/web/src/*"], "playwright-core/lib/*": ["./packages/playwright-core/src/*"], "playwright/lib/*": ["./packages/playwright/src/*"], @@ -39,6 +38,7 @@ "skipLibCheck": true, }, "compileOnSave": true, + "files": ["packages/html-reporter/tests/stories.d.ts"], "include": ["packages"], "exclude": [ "packages/*/lib", diff --git a/utils/build/build.js b/utils/build/build.js index 2bef516d944d4..75c247284e174 100644 --- a/utils/build/build.js +++ b/utils/build/build.js @@ -22,6 +22,7 @@ const chokidar = require('chokidar'); const fs = require('fs'); const { workspace } = require('../workspace'); const { build, context } = require('esbuild'); +const { minimatch } = require('minimatch'); /** * @typedef {{ @@ -79,6 +80,43 @@ function filePath(relative) { return path.join(ROOT, ...relative.split('/')); } +/** + * @param {string} p + * @returns {string} + */ +function toPosixPath(p) { + return p.split(path.sep).join('/'); +} + +/** + * Chokidar v4 dropped glob support: watch the static directory prefix of a + * glob instead, and filter emitted paths with `pathMatcher`. + * @param {string} pattern + * @returns {string} + */ +function globBase(pattern) { + const magicIndex = pattern.search(/[*?{[]/); + if (magicIndex === -1) + return pattern; + return pattern.slice(0, pattern.lastIndexOf(path.sep, magicIndex)); +} + +/** + * @param {string[]} patterns absolute files, directories or globs + * @returns {(file: string) => boolean} + */ +function pathMatcher(patterns) { + const posixPatterns = patterns.map(toPosixPath); + return file => { + const posixFile = toPosixPath(file); + return posixPatterns.some(pattern => { + if (pattern.search(/[*?{[]/) === -1) + return posixFile === pattern || posixFile.startsWith(pattern + '/'); + return minimatch(posixFile, pattern, { dot: true }); + }); + }; +} + /** * Resolve a CLI shipped by a node_modules package to an absolute path, so we * can spawn it via `node` directly instead of going through `npx`/`npm exec` @@ -185,14 +223,15 @@ async function runWatch() { clearTimeout(timeout); timeout = setTimeout(callback, 500); }; - chokidar.watch([...paths, ...mustExist, onChange.script].filter(Boolean).map(filePath)).on('all', reschedule); + chokidar.watch([...paths, ...mustExist, onChange.script].filter(Boolean).map(filePath).map(globBase)).on('all', reschedule); callback(); } for (const { files, from, to, ignored } of copyFiles) { - const watcher = chokidar.watch([filePath(files)], { ignored }); + const matches = pathMatcher([filePath(files)]); + const watcher = chokidar.watch(globBase(filePath(files)), { ignored: pathMatcher(ignored || []) }); watcher.on('all', (event, file) => { - if (event === 'add' || event === 'change') + if ((event === 'add' || event === 'change') && matches(file)) copyFile(file, from, to); }); } @@ -212,11 +251,11 @@ async function runWatch() { async function runBuild() { for (const { files, from, to, ignored } of copyFiles) { - const watcher = chokidar.watch([filePath(files)], { - ignored - }); + const matches = pathMatcher([filePath(files)]); + const watcher = chokidar.watch(globBase(filePath(files)), { ignored: pathMatcher(ignored || []) }); watcher.on('add', file => { - copyFile(file, from, to); + if (matches(file)) + copyFile(file, from, to); }); await new Promise(x => watcher.once('ready', x)); watcher.close(); @@ -330,9 +369,14 @@ class EsbuildStep extends Step { this._context = await context(this._options); disposables.push(() => this._context?.dispose()); - const watcher = chokidar.watch([...this._options.entryPoints, ...(this._watchPaths || [])]); + const watchPaths = [...this._options.entryPoints, ...(this._watchPaths || [])]; + const matches = pathMatcher(watchPaths); + const watcher = chokidar.watch([...new Set(watchPaths.map(globBase))]); await new Promise(x => watcher.once('ready', x)); - watcher.on('all', () => this._rebuild()); + watcher.on('all', (event, file) => { + if (matches(file)) + this._rebuild(); + }); await this._rebuild(); console.log('==== Esbuild watching:', this._relativeEntryPoints().join(', '), `(started in ${Date.now() - start}ms)`); @@ -623,7 +667,6 @@ steps.push(new EsbuildStep({ bundle: true, entryPoints: [filePath('packages/playwright-core/src/serverRegistry.js')], outfile: filePath('packages/playwright-core/lib/serverRegistry.js'), - external: ['fsevents'], }, [filePath('packages/playwright-core/src/*')])); const playwrightCoreSrc = filePath('packages/playwright-core/src'); @@ -634,7 +677,7 @@ steps.push(new EsbuildStep({ bundle: true, entryPoints: [filePath('packages/playwright-core/src/utilsBundle.ts')], outfile: filePath('packages/playwright-core/lib/utilsBundle.js'), - external: ['fsevents', 'express', '@anthropic-ai/sdk'], + external: ['express', '@anthropic-ai/sdk'], alias: { 'raw-body': filePath('utils/build/raw-body.ts'), }, @@ -946,6 +989,7 @@ steps.push(new ProgramStep({ // Generate CLI help. onChanges.push({ inputs: [ + 'packages/playwright-core/src/tools/cli-daemon/command.ts', 'packages/playwright-core/src/tools/cli-daemon/commands.ts', 'packages/playwright-core/src/tools/cli-daemon/helpGenerator.ts', 'utils/generate_cli_help.js', @@ -958,7 +1002,6 @@ onChanges.push({ inputs: [ 'packages/injected/src/**', 'packages/playwright-core/src/third_party/**', - 'packages/playwright-ct-core/src/injected/**', 'packages/isomorphic/**', 'utils/generate_injected_builtins.js', 'utils/generate_injected.js', diff --git a/utils/check_deps.js b/utils/check_deps.js index f6dbf96c125ae..dda7ea3836785 100644 --- a/utils/check_deps.js +++ b/utils/check_deps.js @@ -39,11 +39,9 @@ const depsCache = {}; async function checkDeps() { await innerCheckDeps(path.join(packagesDir, 'html-reporter')); - await innerCheckDeps(path.join(packagesDir, 'playwright-ct-core')); await innerCheckDeps(path.join(packagesDir, 'protocol')); await innerCheckDeps(path.join(packagesDir, 'recorder')); await innerCheckDeps(path.join(packagesDir, 'trace-viewer')); - await innerCheckDeps(path.join(packagesDir, 'trace')); await innerCheckDeps(path.join(packagesDir, 'web')); await innerCheckDeps(path.join(packagesDir, 'injected')); diff --git a/utils/doclint/cli.js b/utils/doclint/cli.js index 3dc1146727c96..3267d6aa32a4f 100755 --- a/utils/doclint/cli.js +++ b/utils/doclint/cli.js @@ -269,6 +269,11 @@ async function run() { } if (dirtyFiles.size) { + if (process.argv.includes('--allow-dirty')) { + console.log('Regenerated files:'); + [...dirtyFiles].forEach(f => console.log(f)); + process.exit(0); + } console.log('============================') console.log('ERROR: generated files have changed, this is only error if happens in CI:'); [...dirtyFiles].forEach(f => console.log(f)); diff --git a/utils/doclint/documentation.js b/utils/doclint/documentation.js index 1fbc1bfe9003d..3858d8f9bc44d 100644 --- a/utils/doclint/documentation.js +++ b/utils/doclint/documentation.js @@ -892,6 +892,9 @@ function csharpOptionOverloadSuffix(option, type) { case 'int': return 'Int'; case 'long': return 'Int64'; case 'Date': return 'Date'; + // Object keeps the original option name, e.g. for `Object|Array` unions. + case 'Object': return ''; + case 'Array': return 'List'; } throw new Error(`CSharp option "${option}" has unsupported type overload "${type}"`); } diff --git a/utils/doclint/linting-code-snippets/cli.js b/utils/doclint/linting-code-snippets/cli.js index 884ef61a0f99a..38b11fa761296 100644 --- a/utils/doclint/linting-code-snippets/cli.js +++ b/utils/doclint/linting-code-snippets/cli.js @@ -134,8 +134,6 @@ class JSLintingService extends LintingService { _knownBadSnippets = [ 'mount(', 'render(', - 'vue-router', - 'experimental-ct', ]; async _init() { diff --git a/utils/generate_channels.js b/utils/generate_channels.js index b0b44bb84647c..ded01189edca6 100755 --- a/utils/generate_channels.js +++ b/utils/generate_channels.js @@ -327,15 +327,33 @@ function generateChannels(target) { throw new Error(`Method "${className}.${methodName}" must should not specify "group" because it is "internal" in protocol.yml`); if (method.group && !['getter', 'configuration', 'route', 'default'].includes(method.group)) throw new Error(`Unknown group "${method.group}" for method "${className}.${methodName}" in protocol.yml`); + if (method.subtitle && method.internal) + throw new Error(`Method "${className}.${methodName}" should not specify "subtitle" because it is "internal" in protocol.yml`); + if (!method.subtitle && !method.internal && (method.parameters || {}).selector) + throw new Error(`Method "${className}.${methodName}" has a "selector" parameter, so it must have an explicit "subtitle" in protocol.yml`); + if (method.renderParams) { + if (method.internal) + throw new Error(`Method "${className}.${methodName}" should not specify "renderParams" because it is "internal" in protocol.yml`); + if (!Array.isArray(method.renderParams) || method.renderParams.some(entry => typeof entry !== 'string')) + throw new Error(`Method "${className}.${methodName}" must specify "renderParams" as a list of strings in protocol.yml`); + for (const entry of method.renderParams) { + // Each entry is "[key=]path[:selector]", path root must be a parameter. + const paramName = entry.split(':')[0].split('=').pop().split('.')[0]; + if (!(method.parameters || {})[paramName]) + throw new Error(`Method "${className}.${methodName}" renderParams entry "${entry}" does not match any parameter in protocol.yml`); + } + } const internalProp = method.internal ? ` internal: ${method.internal},` : ''; const titleProp = method.title ? ` title: '${method.title}',` : ''; + const subtitleProp = method.subtitle ? ` subtitle: '${method.subtitle}',` : ''; + const renderParamsProp = method.renderParams ? ` renderParams: [${method.renderParams.map(entry => `'${entry}'`).join(', ')}],` : ''; const groupProp = method.group ? ` group: '${method.group}',` : ''; const slowMoProp = method.flags?.slowMo ? ` slowMo: ${method.flags.slowMo},` : ''; const snapshotProp = method.flags?.snapshot ? ` snapshot: ${method.flags.snapshot},` : ''; const pauseProp = method.flags?.pause ? ` pause: ${method.flags.pause},` : ''; const inputProp = method.flags?.input ? ` input: ${method.flags.input},` : ''; const isAutoWaitingProp = method.flags?.isAutoWaiting ? ` isAutoWaiting: ${method.flags.isAutoWaiting},` : ''; - methodMetainfo.push(`['${className + '.' + methodName}', {${internalProp}${titleProp}${slowMoProp}${snapshotProp}${pauseProp}${inputProp}${isAutoWaitingProp}${groupProp} }]`); + methodMetainfo.push(`['${className + '.' + methodName}', {${internalProp}${titleProp}${subtitleProp}${renderParamsProp}${slowMoProp}${snapshotProp}${pauseProp}${inputProp}${isAutoWaitingProp}${groupProp} }]`); } const parameters = objectType(method.parameters || {}, ''); @@ -432,7 +450,7 @@ const structs_ts = generateStructs(); const client_channels_ts = generateChannels('Channel'); -metainfo_ts.push(`export type MethodMetainfo = { internal?: boolean, title?: string, slowMo?: boolean, snapshot?: boolean, pause?: boolean, isAutoWaiting?: boolean, input?: boolean, group?: string }; +metainfo_ts.push(`export type MethodMetainfo = { internal?: boolean, title?: string, subtitle?: string, renderParams?: string[], slowMo?: boolean, snapshot?: boolean, pause?: boolean, isAutoWaiting?: boolean, input?: boolean, group?: string }; export const methodMetainfo = new Map([ ${methodMetainfo.join(`,\n `)} diff --git a/utils/generate_injected.js b/utils/generate_injected.js index f62221f0d06cd..4162e312d2e03 100644 --- a/utils/generate_injected.js +++ b/utils/generate_injected.js @@ -23,81 +23,64 @@ const ROOT = path.join(__dirname, '..'); const esbuild = require('esbuild'); /** - * @type {[string, string, string, boolean][]} + * @type {[string, string, string][]} */ const injectedScripts = [ [ path.join(ROOT, 'packages', 'injected', 'src', 'utilityScript.ts'), path.join(ROOT, 'packages', 'injected', 'lib'), path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), - true, ], [ path.join(ROOT, 'packages', 'injected', 'src', 'injectedScript.ts'), path.join(ROOT, 'packages', 'injected', 'lib'), path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), - true, ], [ path.join(ROOT, 'packages', 'injected', 'src', 'recorder', 'pollingRecorder.ts'), path.join(ROOT, 'packages', 'injected', 'lib'), path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), - true, ], [ path.join(ROOT, 'packages', 'injected', 'src', 'clock.ts'), path.join(ROOT, 'packages', 'injected', 'lib'), path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), - true, ], [ path.join(ROOT, 'packages', 'injected', 'src', 'storageScript.ts'), path.join(ROOT, 'packages', 'injected', 'lib'), path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), - true, ], [ path.join(ROOT, 'packages', 'injected', 'src', 'bindingsController.ts'), path.join(ROOT, 'packages', 'injected', 'lib'), path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), - true, ], [ path.join(ROOT, 'packages', 'injected', 'src', 'webSocketMock.ts'), path.join(ROOT, 'packages', 'injected', 'lib'), path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), - true, ], [ path.join(ROOT, 'packages', 'injected', 'src', 'webAuthn.ts'), path.join(ROOT, 'packages', 'injected', 'lib'), path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), - true, ], [ path.join(ROOT, 'packages', 'injected', 'src', 'bidiInsertText.ts'), path.join(ROOT, 'packages', 'injected', 'lib'), path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), - true, ], [ path.join(ROOT, 'packages', 'injected', 'src', 'webview', 'webViewInput.ts'), path.join(ROOT, 'packages', 'injected', 'lib'), path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), - true, ], [ path.join(ROOT, 'packages', 'injected', 'src', 'webview', 'webViewDialog.ts'), path.join(ROOT, 'packages', 'injected', 'lib'), path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), - true, ], - [ - path.join(ROOT, 'packages', 'playwright-ct-core', 'src', 'injected', 'index.ts'), - path.join(ROOT, 'packages', 'playwright-ct-core', 'lib', 'injected', 'packed'), - path.join(ROOT, 'packages', 'playwright-ct-core', 'src', 'generated'), - false, - ] ]; const modulePrefix = ` @@ -150,7 +133,7 @@ const inlineCSSPlugin = { }; (async () => { - for (const [injected, outdir, generatedFolder, hasExports] of injectedScripts) { + for (const [injected, outdir, generatedFolder] of injectedScripts) { await fs.promises.mkdir(generatedFolder, { recursive: true }); const buildOutput = await esbuild.build({ entryPoints: [injected], @@ -165,9 +148,7 @@ const inlineCSSPlugin = { console.log(message.text); const baseName = path.basename(injected); const outFileJs = path.join(outdir, baseName.replace('.ts', '.js')); - let content = await fs.promises.readFile(outFileJs, 'utf-8'); - if (hasExports) - content = await replaceEsbuildHeader(content, outFileJs); + const content = await replaceEsbuildHeader(await fs.promises.readFile(outFileJs, 'utf-8'), outFileJs); const newContent = `export const source = ${JSON.stringify(content)};`; await fs.promises.writeFile(path.join(generatedFolder, baseName.replace('.ts', 'Source.ts')), newContent); } diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index 1dee2fe602361..3a734e0c6015c 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -298,14 +298,19 @@ class TypesGenerator { let type = this.stringifyComplexType(member.type, 'out', indent, [classDesc.name, member.alias]); if (member.async) type = `Promise<${type}>`; + let typeParams = ''; + if (type === 'Promise') { + typeParams = ''; + type = 'Promise>'; + } // do this late, because we still want object definitions for overridden types if (!this.hasOwnMethod(classDesc, member)) return ''; if (exportMembersAsGlobals) { - const memberType = member.kind === 'method' ? `${args} => ${type}` : type; + const memberType = member.kind === 'method' ? `${typeParams}${args} => ${type}` : type; return `${jsdoc}${exportMembersAsGlobals ? 'export const ' : ''}${member.alias}: ${memberType};` } - return `${jsdoc}${member.alias}${member.required ? '' : '?'}${args}: ${type};` + return `${jsdoc}${member.alias}${member.required ? '' : '?'}${typeParams}${args}: ${type};` }).filter(x => x).join('\n\n')); return parts.join('\n') + '\n'; } @@ -583,6 +588,7 @@ class TypesGenerator { 'PlaywrightWorkerOptions.defaultBrowserType', 'PlaywrightWorkerOptions.reuseContext', 'Project', + 'Stories', ]), doNotExportClassNames: assertionClasses, }); diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 4a7273f2231a4..90a983fdcea29 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -18,9 +18,13 @@ import type { APIRequestContext, Browser, BrowserContext, BrowserContextOptions, export * from 'playwright-core'; export type BlobReporterOptions = { outputDir?: string, fileName?: string }; -export type ListReporterOptions = { printSteps?: boolean, printFailuresInline?: boolean }; -export type JUnitReporterOptions = { outputFile?: string, stripANSIControlSequences?: boolean, includeProjectInTestName?: boolean, includeRetries?: boolean }; +export type DotReporterOptions = { omitTags?: boolean }; +export type LineReporterOptions = { omitTags?: boolean }; +export type ListReporterOptions = { printSteps?: boolean, printFailuresInline?: boolean, omitTags?: boolean }; +export type GitHubReporterOptions = { omitTags?: boolean }; +export type JUnitReporterOptions = { outputFile?: string, stripANSIControlSequences?: boolean, includeProjectInTestName?: boolean, includeRetries?: boolean, omitTags?: boolean }; export type JsonReporterOptions = { outputFile?: string }; +export type PerfettoReporterOptions = { outputFile?: string }; export type HtmlReporterOptions = { outputFolder?: string; open?: 'always' | 'never' | 'on-failure'; @@ -36,12 +40,13 @@ export type HtmlReporterOptions = { export type ReporterDescription = Readonly< ['blob'] | ['blob', BlobReporterOptions] | - ['dot'] | - ['line'] | + ['dot'] | ['dot', DotReporterOptions] | + ['line'] | ['line', LineReporterOptions] | ['list'] | ['list', ListReporterOptions] | - ['github'] | + ['github'] | ['github', GitHubReporterOptions] | ['junit'] | ['junit', JUnitReporterOptions] | ['json'] | ['json', JsonReporterOptions] | + ['perfetto'] | ['perfetto', PerfettoReporterOptions] | ['html'] | ['html', HtmlReporterOptions] | ['null'] | [string] | [string, any] @@ -64,7 +69,7 @@ type LiteralUnion = T | (U & { zz_IGNORE_ME?: never }); interface TestConfig { projects?: Project[]; - reporter?: LiteralUnion<'list'|'dot'|'line'|'github'|'json'|'junit'|'null'|'html', string> | ReporterDescription[]; + reporter?: LiteralUnion<'list'|'dot'|'line'|'github'|'json'|'junit'|'null'|'html'|'perfetto', string> | ReporterDescription[]; use?: UseOptions; webServer?: TestConfigWebServer | TestConfigWebServer[]; } @@ -99,6 +104,7 @@ export type TestAnnotation = TestDetailsAnnotation & { export type TestDetails = { tag?: string | string[]; annotation?: TestDetailsAnnotation | TestDetailsAnnotation[]; + lock?: string | string[]; } type TestBody = (args: TestArgs, testInfo: TestInfo) => Promise | unknown; @@ -191,8 +197,8 @@ export interface TestType { afterAll(title: string, inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise | any): void; use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void; step: { - (title: string, body: (step: TestStepInfo) => T | Promise, options?: { box?: boolean, location?: Location, timeout?: number }): Promise; - skip(title: string, body: (step: TestStepInfo) => any | Promise, options?: { box?: boolean, location?: Location, timeout?: number }): Promise; + (title: string, body: (step: TestStepInfo) => T | Promise, options?: { box?: boolean, location?: Location, timeout?: number, params?: { [key: string]: any }, subtitle?: string }): Promise; + skip(title: string, body: (step: TestStepInfo) => any | Promise, options?: { box?: boolean, location?: Location, timeout?: number, params?: { [key: string]: any }, subtitle?: string }): Promise; } expect: Expect<{}>; extend(fixtures: Fixtures): TestType; @@ -216,6 +222,9 @@ export type Fixtures; type ColorScheme = Exclude; +type Contrast = Exclude; +type ForcedColors = Exclude; +type ReducedMotion = Exclude; type ClientCertificate = Exclude[0]; type ExtraHTTPHeaders = Exclude; type Proxy = Exclude; @@ -263,7 +272,7 @@ export interface PlaywrightWorkerOptions { connectOptions: ConnectOptions | undefined; reuseContext: boolean; screenshot: ScreenshotMode | { mode: ScreenshotMode } & Pick; - trace: TraceMode | /** deprecated */ 'retry-with-trace' | { mode: TraceMode, snapshots?: boolean, screenshots?: boolean, sources?: boolean, attachments?: boolean }; + trace: TraceMode | /** deprecated */ 'retry-with-trace' | { mode: TraceMode, snapshots?: boolean | { dom?: boolean, aria?: boolean, screen?: boolean }, screenshots?: boolean, sources?: boolean, attachments?: boolean }; video: VideoMode | /** deprecated */ 'retry-with-video' | { mode: VideoMode, size?: ViewportSize, show?: { actions?: { duration?: number, position?: 'top-left' | 'top' | 'top-right' | 'bottom-left' | 'bottom' | 'bottom-right', fontSize?: number, cursor?: 'none' | 'pointer' }, test?: { level?: 'file' | 'title' | 'step', position?: 'top-left' | 'top' | 'top-right' | 'bottom-left' | 'bottom' | 'bottom-right', fontSize?: number } } }; } @@ -274,12 +283,14 @@ export interface PlaywrightTestOptions { acceptDownloads: boolean; bypassCSP: boolean; colorScheme: ColorScheme; + contrast: Contrast; clientCertificates: ClientCertificate[] | undefined; deviceScaleFactor: number | undefined; extraHTTPHeaders: ExtraHTTPHeaders | undefined; + forcedColors: ForcedColors; geolocation: Geolocation | undefined; hasTouch: boolean; - httpCredentials: HTTPCredentials | undefined; + httpCredentials: HTTPCredentials | HTTPCredentials[] | undefined; ignoreHTTPSErrors: boolean; isMobile: boolean; javaScriptEnabled: boolean; @@ -287,6 +298,7 @@ export interface PlaywrightTestOptions { offline: boolean; permissions: string[] | undefined; proxy: Proxy | undefined; + reducedMotion: ReducedMotion; storageState: StorageState | undefined; timezoneId: string | undefined; userAgent: string | undefined; @@ -305,17 +317,23 @@ export interface PlaywrightWorkerArgs { browser: Browser; } +export interface Stories {} + type StoryProps = Story extends (props: infer Props) => any ? Props : Story extends new (...args: any[]) => { $props: infer Props } ? Props : Story extends new (props: infer Props, ...args: any[]) => any ? Props : Story; +type StoryId = keyof Stories | (string & {}); +type StoryPropsFor = Id extends keyof Stories ? StoryProps : Record; +// Explicit mount() wins over the id lookup; the indexed access keeps Story from being inferred from props. +type MountProps = [Story] extends [never] ? StoryPropsFor : StoryProps<[Story][Story extends any ? 0 : never]>; export interface PlaywrightTestArgs { context: BrowserContext; page: Page; request: APIRequestContext; - mount: >(storyId: string, props?: StoryProps) => Promise): Promise, unmount(): Promise }>; + mount: (storyId: Id, props?: MountProps) => Promise): Promise, unmount(): Promise }>; } type ExcludeProps = { diff --git a/utils/generate_types/overrides-testReporter.d.ts b/utils/generate_types/overrides-testReporter.d.ts index 6ca13fdcf2157..78dd048a9e554 100644 --- a/utils/generate_types/overrides-testReporter.d.ts +++ b/utils/generate_types/overrides-testReporter.d.ts @@ -132,6 +132,7 @@ export interface JSONReportTestResult { export interface JSONReportTestStep { title: string; + subtitle?: string; duration: number; error: TestError | undefined; steps?: JSONReportTestStep[]; diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index a018af158bbb5..a329924e5ad0f 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -166,6 +166,10 @@ export interface JSHandle { asElement(): T extends Node ? ElementHandle : null; } +export interface APIResponse { + json(): Promise; +} + export interface ElementHandle extends JSHandle { $(selector: K, options?: { strict: boolean }): Promise | null>; $(selector: string, options?: { strict: boolean }): Promise | null>; @@ -252,11 +256,6 @@ export interface Screencast { height: number; }; quality?: number; - annotate?: { - duration?: number; - position?: 'top-left' | 'top' | 'top-right' | 'bottom-left' | 'bottom' | 'bottom-right'; - fontSize?: number; - }; }): Promise; } diff --git a/utils/generate_types/test/test.ts b/utils/generate_types/test/test.ts index 3e4d407f1c0c7..3d098fc0f4931 100644 --- a/utils/generate_types/test/test.ts +++ b/utils/generate_types/test/test.ts @@ -19,6 +19,10 @@ import * as playwright from 'playwright'; type AssertType = S extends T ? AssertNotAny : false; type AssertNotAny = {notRealProperty: number} extends S ? false : true; +declare const page: playwright.Page; +// @ts-expect-error annotate is not a Screencast.start option. +page.screencast.start({ annotate: { position: 'top-left' } }); + // Examples taken from README (async () => { const browser = await playwright.chromium.launch(); @@ -428,10 +432,12 @@ playwright.chromium.launch().then(async browser => { { await locator.evaluateAll((sel: HTMLSelectElement[]) => {}) } + // Handles in callback results are unboxed, but the callback keeps its authored + // sync/async signature, even though the runtime always exposes it as async on the page side. { await locator.evaluate((e, cb) => { const value = cb(2); - const assertion: AssertType, typeof value> = true; + const assertion: AssertType = true; }, (x: number) => 2 * x); } { @@ -456,6 +462,54 @@ playwright.chromium.launch().then(async browser => { const assertion: AssertType, typeof value> = true; }, { cb: func }); } + { + // Promises nested in the argument are not awaited, only callback results are. + const result = await page.evaluate(arg => { + const assertion: AssertType, typeof arg.a> = true; + return arg; + }, { a: new Promise(() => {}), b: 42 }); + const assertion: AssertType<{ a: Promise, b: number }, typeof result> = true; + } + await browser.close(); +})(); + +// branded primitives in evaluate arguments — https://github.com/microsoft/playwright/issues/42000 +declare const __brand: unique symbol; +type Branded = T & { [__brand]: B }; +type IsoDate = Branded; +declare function takesIsoDate(date: IsoDate): void; + +(async () => { + const browser = await playwright.chromium.launch(); + const page = await browser.newPage(); + const date = '2026-01-15' as IsoDate; + { + const result = await page.evaluate((d: IsoDate) => d, date); + const assertion: AssertType = true; + } + { + await page.evaluate(d => takesIsoDate(d), date); + } + { + await page.evaluate(({ d }) => takesIsoDate(d), { d: date }); + } + { + const count = 42 as Branded; + const result = await page.evaluate(c => c, count); + const assertion: AssertType, typeof result> = true; + } + { + const result = await page.evaluate((arg: { d?: IsoDate }) => arg.d, { d: date } as { d?: IsoDate }); + const assertion: AssertType = true; + } + { + const result = await page.evaluate((dates: readonly IsoDate[]) => dates[0], [date] as readonly IsoDate[]); + const assertion: AssertType = true; + } + { + const result = await page.evaluate((pair: [IsoDate, number]) => pair[0], [date, 42] as [IsoDate, number]); + const assertion: AssertType = true; + } await browser.close(); })(); @@ -966,7 +1020,24 @@ playwright.chromium.launch().then(async browser => { const browserType = {} as playwright.BrowserType; const browser = await browserType.launch(); await browser.close(); -}) +})(); + +// APIRequestContext / APIResponse generics +(async () => { + const request = {} as playwright.APIRequestContext; + interface User { id: string; name: string } + + const typed = await request.get('/api/users/42'); + const user = await typed.json(); + const name: string = user.name; + + const posted = await request.post('/api/users', { data: { name: 'x' } }); + const created: User = await posted.json(); + + const untyped = await request.get('/api/users/42'); + const body = await untyped.json(); + console.log(body, name, created); +})(); // exported types import { diff --git a/utils/roll_browser.js b/utils/roll_browser.js index 9000a69e79ea5..f04030c4a7f6c 100755 --- a/utils/roll_browser.js +++ b/utils/roll_browser.js @@ -32,9 +32,9 @@ usage: ${SCRIPT_NAME} [version] Roll the to a specific and generate new protocol. Version is required for chromium-based browsers. -Supported browsers: chromium, firefox, webkit, ffmpeg, firefox-beta. +Supported browsers: chromium, firefox, webkit, ffmpeg. -Rolling firefox or firefox-beta requires a playwright-browsers checkout +Rolling firefox requires a playwright-browsers checkout next to the playwright checkout, to roll browser patches from upstream. Set PW_BROWSERS_CHECKOUT to point to a checkout in a custom location. @@ -63,7 +63,6 @@ Example: const browserName = { 'cr': 'chromium', 'ff': 'firefox', - 'ff-beta': 'firefox-beta', 'wk': 'webkit', }[args[0].toLowerCase()] ?? args[0].toLowerCase(); const browserTypeName = browserName.split('-')[0]; @@ -149,11 +148,7 @@ Example: // 8. Update docs. console.log('\nUpdating documentation...'); - try { - execSync('npm run doc', { stdio: 'inherit' }); - } catch (e) { - console.log('npm run doc failed with non-zero exit code. This might have updated generated files.'); - } + execSync('npm run doc -- --allow-dirty', { stdio: 'inherit' }); } console.log(`\nRolled ${browserName} to ${revision}`); })().catch(err => { diff --git a/utils/workspace.js b/utils/workspace.js index 2f98fc2f3ecf5..bc25a191d0b18 100755 --- a/utils/workspace.js +++ b/utils/workspace.js @@ -194,26 +194,6 @@ const workspace = new Workspace(ROOT_PATH, [ path: path.join(ROOT_PATH, 'packages', 'playwright-browser-chromium'), files: LICENCE_FILES, }), - new PWPackage({ - name: '@playwright/experimental-ct-core', - path: path.join(ROOT_PATH, 'packages', 'playwright-ct-core'), - files: ['LICENSE'], - }), - new PWPackage({ - name: '@playwright/experimental-ct-react', - path: path.join(ROOT_PATH, 'packages', 'playwright-ct-react'), - files: ['LICENSE'], - }), - new PWPackage({ - name: '@playwright/experimental-ct-react17', - path: path.join(ROOT_PATH, 'packages', 'playwright-ct-react17'), - files: ['LICENSE'], - }), - new PWPackage({ - name: '@playwright/experimental-ct-vue', - path: path.join(ROOT_PATH, 'packages', 'playwright-ct-vue'), - files: ['LICENSE'], - }), ]); if (require.main === module) {