Merge remote-tracking branch 'origin/main' into hello-angular-ct
This commit is contained in:
commit
10ee5da3ea
2
.github/actions/download-artifact/action.yml
vendored
2
.github/actions/download-artifact/action.yml
vendored
|
|
@ -4,12 +4,10 @@ inputs:
|
|||
namePrefix:
|
||||
description: 'Name prefix of the artifacts to download'
|
||||
required: true
|
||||
type: string
|
||||
default: 'blob-report'
|
||||
path:
|
||||
description: 'Directory with downloaded artifacts'
|
||||
required: true
|
||||
type: string
|
||||
default: '.'
|
||||
runs:
|
||||
using: "composite"
|
||||
|
|
|
|||
|
|
@ -4,16 +4,13 @@ inputs:
|
|||
blob_prefix:
|
||||
description: 'Name of the Azure blob storage directory containing blob report'
|
||||
required: true
|
||||
type: string
|
||||
output_dir:
|
||||
description: 'Output directory where downloaded blobs will be stored'
|
||||
required: true
|
||||
type: string
|
||||
default: 'blob-report'
|
||||
connection_string:
|
||||
description: 'Azure connection string'
|
||||
required: true
|
||||
type: string
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
|
|
|
|||
11
.github/actions/upload-blob-report/action.yml
vendored
11
.github/actions/upload-blob-report/action.yml
vendored
|
|
@ -4,29 +4,30 @@ inputs:
|
|||
report_dir:
|
||||
description: 'Directory containing blob report'
|
||||
required: true
|
||||
type: string
|
||||
default: 'test-results/blob-report'
|
||||
job_name:
|
||||
description: 'Unique job name'
|
||||
required: true
|
||||
type: string
|
||||
default: ''
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Integrity check
|
||||
shell: bash
|
||||
run: find "${{ inputs.report_dir }}" -name "*.zip" -exec unzip -t {} \;
|
||||
- name: Upload blob report to GitHub
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: blob-report-${{ inputs.job_name }}
|
||||
path: ${{ inputs.report_dir }}/**
|
||||
retention-days: 7
|
||||
- name: Write triggering pull request number in a file
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
|
||||
shell: bash
|
||||
run: echo '${{ github.event.number }}' > pull_request_number.txt;
|
||||
- name: Upload artifact with the pull request number
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pull-request-${{ inputs.job_name }}
|
||||
|
|
|
|||
4
.github/workflows/publish_release_npm.yml
vendored
4
.github/workflows/publish_release_npm.yml
vendored
|
|
@ -25,10 +25,10 @@ jobs:
|
|||
- run: npm run build
|
||||
- run: npx playwright install-deps
|
||||
- run: utils/publish_all_packages.sh --release-candidate
|
||||
if: "github.event.release.prerelease"
|
||||
if: ${{ github.event.release.prerelease }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- run: utils/publish_all_packages.sh --release
|
||||
if: "!github.event.release.prerelease"
|
||||
if: ${{ !github.event.release.prerelease }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
|
|
|||
4
.github/workflows/tests_electron.yml
vendored
4
.github/workflows/tests_electron.yml
vendored
|
|
@ -42,7 +42,7 @@ jobs:
|
|||
- run: npm run etest
|
||||
if: matrix.os != 'ubuntu-latest'
|
||||
- run: node tests/config/checkCoverage.js electron
|
||||
if: always() && matrix.os == 'ubuntu-latest'
|
||||
if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }}
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
|
|
|
|||
24
.github/workflows/tests_primary.yml
vendored
24
.github/workflows/tests_primary.yml
vendored
|
|
@ -58,10 +58,10 @@ jobs:
|
|||
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}-*
|
||||
- run: node tests/config/checkCoverage.js ${{ matrix.browser }}
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -92,10 +92,10 @@ jobs:
|
|||
PWTEST_CHANNEL: chromium-tip-of-tree
|
||||
PWTEST_BOT_NAME: "${{ matrix.os }}-chromium-tip-of-tree"
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -145,10 +145,10 @@ jobs:
|
|||
- run: xvfb-run npm run ttest -- --shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -172,18 +172,18 @@ jobs:
|
|||
env:
|
||||
PWTEST_BOT_NAME: "web-components-html-reporter"
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: packages/html-reporter/blob-report
|
||||
job_name: "web-components-html-reporter"
|
||||
|
||||
- run: npm run test-web
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
env:
|
||||
PWTEST_BOT_NAME: "web-components-web"
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: packages/web/blob-report
|
||||
|
|
@ -219,7 +219,7 @@ jobs:
|
|||
run: npm run test -- --workers=1
|
||||
working-directory: ./playwright-vscode
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: playwright-vscode/blob-report
|
||||
|
|
@ -255,10 +255,10 @@ jobs:
|
|||
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run itest
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
|
|||
114
.github/workflows/tests_secondary.yml
vendored
114
.github/workflows/tests_secondary.yml
vendored
|
|
@ -45,10 +45,10 @@ jobs:
|
|||
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}-*
|
||||
- run: node tests/config/checkCoverage.js ${{ matrix.browser }}
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -77,10 +77,10 @@ jobs:
|
|||
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium
|
||||
- run: npm run test -- --project=${{ matrix.browser }}-*
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -113,10 +113,10 @@ jobs:
|
|||
if: matrix.browser != 'firefox'
|
||||
shell: bash
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -151,7 +151,7 @@ jobs:
|
|||
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run itest
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
|
||||
headed_tests:
|
||||
|
|
@ -176,14 +176,14 @@ jobs:
|
|||
- run: npm run build
|
||||
- run: npx playwright install --with-deps ${{ matrix.browser }} chromium
|
||||
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test -- --project=${{ matrix.browser }}-* --headed
|
||||
if: always() && startsWith(matrix.os, 'ubuntu-')
|
||||
if: ${{ !cancelled() && startsWith(matrix.os, 'ubuntu-') }}
|
||||
- run: npm run test -- --project=${{ matrix.browser }}-* --headed
|
||||
if: always() && !startsWith(matrix.os, 'ubuntu-')
|
||||
if: ${{ !cancelled() && !startsWith(matrix.os, 'ubuntu-') }}
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -213,10 +213,10 @@ jobs:
|
|||
env:
|
||||
PWTEST_MODE: ${{ matrix.mode }}
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -252,10 +252,10 @@ jobs:
|
|||
PWTEST_TRACE: 1
|
||||
PWTEST_CHANNEL: ${{ matrix.channel }}
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -280,10 +280,10 @@ jobs:
|
|||
env:
|
||||
PWTEST_CHANNEL: chrome
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -309,10 +309,10 @@ jobs:
|
|||
env:
|
||||
PWTEST_CHANNEL: chrome
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -337,10 +337,10 @@ jobs:
|
|||
env:
|
||||
PWTEST_CHANNEL: chrome
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -371,10 +371,10 @@ jobs:
|
|||
- run: npm run ctest
|
||||
if: matrix.os != 'ubuntu-20.04'
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -405,10 +405,10 @@ jobs:
|
|||
- run: npm run ctest -- --headed
|
||||
if: matrix.os != 'ubuntu-latest'
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -433,10 +433,10 @@ jobs:
|
|||
env:
|
||||
PWTEST_CHANNEL: firefox-beta
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -462,10 +462,10 @@ jobs:
|
|||
env:
|
||||
PWTEST_CHANNEL: firefox-beta
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -490,10 +490,10 @@ jobs:
|
|||
env:
|
||||
PWTEST_CHANNEL: firefox-beta
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -518,10 +518,10 @@ jobs:
|
|||
env:
|
||||
PWTEST_CHANNEL: msedge
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -547,10 +547,10 @@ jobs:
|
|||
env:
|
||||
PWTEST_CHANNEL: msedge
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -575,10 +575,10 @@ jobs:
|
|||
env:
|
||||
PWTEST_CHANNEL: msedge
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -603,10 +603,10 @@ jobs:
|
|||
env:
|
||||
PWTEST_CHANNEL: msedge-beta
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -632,10 +632,10 @@ jobs:
|
|||
env:
|
||||
PWTEST_CHANNEL: msedge-beta
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -660,10 +660,10 @@ jobs:
|
|||
env:
|
||||
PWTEST_CHANNEL: msedge-beta
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -688,10 +688,10 @@ jobs:
|
|||
env:
|
||||
PWTEST_CHANNEL: msedge-dev
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -717,10 +717,10 @@ jobs:
|
|||
env:
|
||||
PWTEST_CHANNEL: msedge-dev
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -745,10 +745,10 @@ jobs:
|
|||
env:
|
||||
PWTEST_CHANNEL: msedge-dev
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -773,10 +773,10 @@ jobs:
|
|||
env:
|
||||
PWTEST_CHANNEL: chrome-beta
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -802,10 +802,10 @@ jobs:
|
|||
env:
|
||||
PWTEST_CHANNEL: chrome-beta
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -830,10 +830,10 @@ jobs:
|
|||
env:
|
||||
PWTEST_CHANNEL: chrome-beta
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
@ -873,10 +873,10 @@ jobs:
|
|||
PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW: 1
|
||||
- run: node tests/config/checkCoverage.js chromium
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
- name: Upload blob report
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: ./.github/actions/upload-blob-report
|
||||
with:
|
||||
report_dir: blob-report
|
||||
|
|
|
|||
4
.github/workflows/tests_service.yml
vendored
4
.github/workflows/tests_service.yml
vendored
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
PLAYWRIGHT_SERVICE_OS: ${{ matrix.service-os }}
|
||||
PLAYWRIGHT_SERVICE_RUN_ID: ${{ github.run_id }}-${{ github.run_attempt }}-${{ github.sha }}
|
||||
- name: Upload blob report to GitHub
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: all-blob-reports
|
||||
|
|
@ -43,7 +43,7 @@ jobs:
|
|||
merge_reports:
|
||||
name: "Merge reports"
|
||||
needs: [test]
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
|
|||
20
.github/workflows/tests_stress.yml
vendored
20
.github/workflows/tests_stress.yml
vendored
|
|
@ -37,22 +37,22 @@ jobs:
|
|||
- run: npx playwright install firefox-asan
|
||||
if: matrix.os != 'windows-latest'
|
||||
- run: npm run stest contexts -- --project=chromium
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
- run: npm run stest browsers -- --project=chromium
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
- run: npm run stest frames -- --project=chromium
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
- run: npm run stest contexts -- --project=webkit
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
- run: npm run stest browsers -- --project=webkit
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
- run: npm run stest frames -- --project=webkit
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
- run: npm run stest contexts -- --project=firefox
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
- run: npm run stest browsers -- --project=firefox
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
- run: npm run stest frames -- --project=firefox
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
- run: npm run stest heap -- --project=chromium
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
|
|
|
|||
2
.github/workflows/tests_video.yml
vendored
2
.github/workflows/tests_video.yml
vendored
|
|
@ -36,5 +36,5 @@ jobs:
|
|||
env:
|
||||
PWTEST_VIDEO: 1
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
|
|
|
|||
2
.github/workflows/tests_webview2.yml
vendored
2
.github/workflows/tests_webview2.yml
vendored
|
|
@ -46,5 +46,5 @@ jobs:
|
|||
Start-Process -FilePath setup.exe -Verb RunAs -Wait
|
||||
- run: npm run webview2test
|
||||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 🎭 Playwright
|
||||
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop -->
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop -->
|
||||
|
||||
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||
|
||||
|
|
@ -10,7 +10,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
|||
| :--- | :---: | :---: | :---: |
|
||||
| Chromium <!-- GEN:chromium-version -->124.0.6367.8<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| WebKit <!-- GEN:webkit-version -->17.4<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Firefox <!-- GEN:firefox-version -->123.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Firefox <!-- GEN:firefox-version -->124.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
|
||||
Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details.
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ A few examples of problems this can catch include:
|
|||
|
||||
The following examples rely on the [`@axe-core/playwright`](https://npmjs.org/@axe-core/playwright) package which adds support for running the [axe accessibility testing engine](https://www.deque.com/axe/) as part of your Playwright tests.
|
||||
|
||||
:::note Disclaimer
|
||||
:::note[Disclaimer]
|
||||
Automated accessibility tests can detect some common accessibility problems such as missing or invalid properties. But many accessibility problems can only be discovered through manual testing. We recommend using a combination of automated testing, manual accessibility assessments, and inclusive user testing.
|
||||
|
||||
For manual assessments, we recommend [Accessibility Insights for Web](https://accessibilityinsights.io/docs/web/overview/?referrer=playwright-accessibility-testing-js), a free and open source dev tool that walks you through assessing a website for [WCAG 2.1 AA](https://www.w3.org/WAI/WCAG21/quickref/?currentsidebar=%23col_customize&levels=aaa) coverage.
|
||||
|
|
|
|||
|
|
@ -566,7 +566,7 @@ api_request_context.post("https://example.com/api/createBook", data=data)
|
|||
|
||||
```csharp
|
||||
var data = new Dictionary<string, object>() {
|
||||
{ "firstNam", "John" },
|
||||
{ "firstName", "John" },
|
||||
{ "lastName", "Doe" }
|
||||
};
|
||||
await request.PostAsync("https://example.com/api/createBook", new() { DataObject = data });
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
ElementHandle represents an in-page DOM element. ElementHandles can be created with the [`method: Page.querySelector`] method.
|
||||
|
||||
:::caution Discouraged
|
||||
:::warning[Discouraged]
|
||||
The use of ElementHandle is discouraged, use [Locator] objects and web-first assertions instead.
|
||||
:::
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,72 @@ FormData form = FormData.create()
|
|||
page.request().post("http://localhost/submit", RequestOptions.create().setForm(form));
|
||||
```
|
||||
|
||||
## method: FormData.add
|
||||
* since: v1.43
|
||||
- returns: <[FormData]>
|
||||
|
||||
Adds a field to the form. File values can be passed either as `Path` or as `FilePayload`.
|
||||
Multiple fields with the same name can be added.
|
||||
|
||||
```java
|
||||
import com.microsoft.playwright.options.FormData;
|
||||
...
|
||||
FormData form = FormData.create()
|
||||
// Only name and value are set.
|
||||
.add("firstName", "John")
|
||||
// Name and value are set, filename and Content-Type are inferred from the file path.
|
||||
.add("attachment", Paths.get("pic.jpg"))
|
||||
// Name, value, filename and Content-Type are set.
|
||||
.add("attachment", new FilePayload("table.csv", "text/csv", Files.readAllBytes(Paths.get("my-tble.csv"))));
|
||||
page.request().post("http://localhost/submit", RequestOptions.create().setForm(form));
|
||||
```
|
||||
|
||||
```csharp
|
||||
var multipart = Context.APIRequest.CreateFormData();
|
||||
// Only name and value are set.
|
||||
multipart.Add("firstName", "John");
|
||||
// Name, value, filename and Content-Type are set.
|
||||
multipart.Add("attachment", new FilePayload()
|
||||
{
|
||||
Name = "pic.jpg",
|
||||
MimeType = "image/jpeg",
|
||||
Buffer = File.ReadAllBytes("john.jpg")
|
||||
});
|
||||
// Name, value, filename and Content-Type are set.
|
||||
multipart.Add("attachment", new FilePayload()
|
||||
{
|
||||
Name = "table.csv",
|
||||
MimeType = "text/csv",
|
||||
Buffer = File.ReadAllBytes("my-tble.csv")
|
||||
});
|
||||
await Page.APIRequest.PostAsync("https://localhost/submit", new() { Multipart = multipart });
|
||||
```
|
||||
|
||||
### param: FormData.add.name
|
||||
* since: v1.43
|
||||
- `name` <[string]>
|
||||
|
||||
Field name.
|
||||
|
||||
### param: FormData.add.value
|
||||
* since: v1.43
|
||||
- `value` <[string]|[boolean]|[int]|[Path]|[Object]>
|
||||
- `name` <[string]> File name
|
||||
- `mimeType` <[string]> File type
|
||||
- `buffer` <[Buffer]> File content
|
||||
|
||||
Field value.
|
||||
|
||||
### param: FormData.add.value
|
||||
* since: v1.43
|
||||
* langs: csharp
|
||||
- `value` <[string]|[boolean]|[int]|[Object]>
|
||||
- `name` <[string]> File name
|
||||
- `mimeType` <[string]> File type
|
||||
- `buffer` <[Buffer]> File content
|
||||
|
||||
Field value.
|
||||
|
||||
## method: FormData.create
|
||||
* since: v1.18
|
||||
* langs: java
|
||||
|
|
@ -36,7 +102,7 @@ FormData form = FormData.create()
|
|||
// Name and value are set, filename and Content-Type are inferred from the file path.
|
||||
.set("profilePicture1", Paths.get("john.jpg"))
|
||||
// Name, value, filename and Content-Type are set.
|
||||
.set("profilePicture2", new FilePayload("john.jpg", "image/jpeg", Files.readAllBytes(Paths.get("john.jpg"))));
|
||||
.set("profilePicture2", new FilePayload("john.jpg", "image/jpeg", Files.readAllBytes(Paths.get("john.jpg"))))
|
||||
.set("age", 30);
|
||||
page.request().post("http://localhost/submit", RequestOptions.create().setForm(form));
|
||||
```
|
||||
|
|
@ -52,6 +118,7 @@ multipart.Set("profilePicture", new FilePayload()
|
|||
MimeType = "image/jpeg",
|
||||
Buffer = File.ReadAllBytes("john.jpg")
|
||||
});
|
||||
multipart.Set("age", 30);
|
||||
await Page.APIRequest.PostAsync("https://localhost/submit", new() { Multipart = multipart });
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -74,57 +74,11 @@ await page.FrameLocator(".result-frame").First.getByRole(AriaRole.Button).ClickA
|
|||
|
||||
**Converting Locator to FrameLocator**
|
||||
|
||||
If you have a [Locator] object pointing to an `iframe` it can be converted to [FrameLocator] using [`method: Locator.enterFrame`].
|
||||
If you have a [Locator] object pointing to an `iframe` it can be converted to [FrameLocator] using [`method: Locator.contentFrame`].
|
||||
|
||||
**Converting FrameLocator to Locator**
|
||||
|
||||
If you have a [FrameLocator] object it can be converted to [Locator] pointing to the same `iframe` using [`method: FrameLocator.exitFrame`].
|
||||
|
||||
|
||||
## method: FrameLocator.exitFrame
|
||||
* since: v1.43
|
||||
- returns: <[Locator]>
|
||||
|
||||
Returns a [Locator] object pointing to the same `iframe` as this frame locator.
|
||||
|
||||
Useful when you have a [FrameLocator] object obtained somewhere, and later on would like to interact with the `iframe` element.
|
||||
|
||||
**Usage**
|
||||
|
||||
```js
|
||||
const frameLocator = page.frameLocator('iframe[name="embedded"]');
|
||||
// ...
|
||||
const locator = frameLocator.exitFrame();
|
||||
await expect(locator).toBeVisible();
|
||||
```
|
||||
|
||||
```java
|
||||
FrameLocator frameLocator = page.frameLocator("iframe[name=\"embedded\"]");
|
||||
// ...
|
||||
Locator locator = frameLocator.exitFrame();
|
||||
assertThat(locator).isVisible();
|
||||
```
|
||||
|
||||
```python async
|
||||
frame_locator = page.frame_locator("iframe[name=\"embedded\"]")
|
||||
# ...
|
||||
locator = frame_locator.exit_frame
|
||||
await expect(locator).to_be_visible()
|
||||
```
|
||||
|
||||
```python sync
|
||||
frame_locator = page.frame_locator("iframe[name=\"embedded\"]")
|
||||
# ...
|
||||
locator = frame_locator.exit_frame
|
||||
expect(locator).to_be_visible()
|
||||
```
|
||||
|
||||
```csharp
|
||||
var frameLocator = Page.FrameLocator("iframe[name=\"embedded\"]");
|
||||
// ...
|
||||
var locator = frameLocator.ExitFrame;
|
||||
await Expect(locator).ToBeVisibleAsync();
|
||||
```
|
||||
If you have a [FrameLocator] object it can be converted to [Locator] pointing to the same `iframe` using [`method: FrameLocator.owner`].
|
||||
|
||||
|
||||
## method: FrameLocator.first
|
||||
|
|
@ -248,3 +202,51 @@ Returns locator to the n-th matching frame. It's zero based, `nth(0)` selects th
|
|||
### param: FrameLocator.nth.index
|
||||
* since: v1.17
|
||||
- `index` <[int]>
|
||||
|
||||
## method: FrameLocator.owner
|
||||
* since: v1.43
|
||||
- returns: <[Locator]>
|
||||
|
||||
Returns a [Locator] object pointing to the same `iframe` as this frame locator.
|
||||
|
||||
Useful when you have a [FrameLocator] object obtained somewhere, and later on would like to interact with the `iframe` element.
|
||||
|
||||
For a reverse operation, use [`method: Locator.contentFrame`].
|
||||
|
||||
**Usage**
|
||||
|
||||
```js
|
||||
const frameLocator = page.frameLocator('iframe[name="embedded"]');
|
||||
// ...
|
||||
const locator = frameLocator.owner();
|
||||
await expect(locator).toBeVisible();
|
||||
```
|
||||
|
||||
```java
|
||||
FrameLocator frameLocator = page.frameLocator("iframe[name=\"embedded\"]");
|
||||
// ...
|
||||
Locator locator = frameLocator.owner();
|
||||
assertThat(locator).isVisible();
|
||||
```
|
||||
|
||||
```python async
|
||||
frame_locator = page.frame_locator("iframe[name=\"embedded\"]")
|
||||
# ...
|
||||
locator = frame_locator.owner
|
||||
await expect(locator).to_be_visible()
|
||||
```
|
||||
|
||||
```python sync
|
||||
frame_locator = page.frame_locator("iframe[name=\"embedded\"]")
|
||||
# ...
|
||||
locator = frame_locator.owner
|
||||
expect(locator).to_be_visible()
|
||||
```
|
||||
|
||||
```csharp
|
||||
var frameLocator = Page.FrameLocator("iframe[name=\"embedded\"]");
|
||||
// ...
|
||||
var locator = frameLocator.Owner;
|
||||
await Expect(locator).ToBeVisibleAsync();
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ foreach (var li in await page.GetByRole("listitem").AllAsync())
|
|||
|
||||
Returns an array of `node.innerText` values for all matching nodes.
|
||||
|
||||
:::caution Asserting text
|
||||
:::warning[Asserting text]
|
||||
If you need to assert text on the page, prefer [`method: LocatorAssertions.toHaveText`] with [`option: useInnerText`] option to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
|
||||
:::
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ var texts = await page.GetByRole(AriaRole.Link).AllInnerTextsAsync();
|
|||
|
||||
Returns an array of `node.textContent` values for all matching nodes.
|
||||
|
||||
:::caution Asserting text
|
||||
:::warning[Asserting text]
|
||||
If you need to assert text on the page, prefer [`method: LocatorAssertions.toHaveText`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
|
||||
:::
|
||||
|
||||
|
|
@ -443,7 +443,7 @@ await page.Locator("canvas").ClickAsync(new() {
|
|||
|
||||
Returns the number of elements matching the locator.
|
||||
|
||||
:::caution Asserting count
|
||||
:::warning[Asserting count]
|
||||
If you need to assert the number of elements on the page, prefer [`method: LocatorAssertions.toHaveCount`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
|
||||
:::
|
||||
|
||||
|
|
@ -747,7 +747,7 @@ Resolves given locator to the first matching DOM element. If there are no matchi
|
|||
|
||||
Resolves given locator to all matching DOM elements. If there are no matching elements, returns an empty list.
|
||||
|
||||
## method: Locator.enterFrame
|
||||
## method: Locator.contentFrame
|
||||
* since: v1.43
|
||||
- returns: <[FrameLocator]>
|
||||
|
||||
|
|
@ -755,40 +755,42 @@ Returns a [FrameLocator] object pointing to the same `iframe` as this locator.
|
|||
|
||||
Useful when you have a [Locator] object obtained somewhere, and later on would like to interact with the content inside the frame.
|
||||
|
||||
For a reverse operation, use [`method: FrameLocator.owner`].
|
||||
|
||||
**Usage**
|
||||
|
||||
```js
|
||||
const locator = page.locator('iframe[name="embedded"]');
|
||||
// ...
|
||||
const frameLocator = locator.enterFrame();
|
||||
const frameLocator = locator.contentFrame();
|
||||
await frameLocator.getByRole('button').click();
|
||||
```
|
||||
|
||||
```java
|
||||
Locator locator = page.locator("iframe[name=\"embedded\"]");
|
||||
// ...
|
||||
FrameLocator frameLocator = locator.enterFrame();
|
||||
FrameLocator frameLocator = locator.contentFrame();
|
||||
frameLocator.getByRole(AriaRole.BUTTON).click();
|
||||
```
|
||||
|
||||
```python async
|
||||
locator = page.locator("iframe[name=\"embedded\"]")
|
||||
# ...
|
||||
frame_locator = locator.enter_frame
|
||||
frame_locator = locator.content_frame
|
||||
await frame_locator.get_by_role("button").click()
|
||||
```
|
||||
|
||||
```python sync
|
||||
locator = page.locator("iframe[name=\"embedded\"]")
|
||||
# ...
|
||||
frame_locator = locator.enter_frame
|
||||
frame_locator = locator.content_frame
|
||||
frame_locator.get_by_role("button").click()
|
||||
```
|
||||
|
||||
```csharp
|
||||
var locator = Page.Locator("iframe[name=\"embedded\"]");
|
||||
// ...
|
||||
var frameLocator = locator.EnterFrame;
|
||||
var frameLocator = locator.ContentFrame;
|
||||
await frameLocator.GetByRole(AriaRole.Button).ClickAsync();
|
||||
```
|
||||
|
||||
|
|
@ -1119,7 +1121,7 @@ await locator.ClickAsync();
|
|||
|
||||
Returns the matching element's attribute value.
|
||||
|
||||
:::caution Asserting attributes
|
||||
:::warning[Asserting attributes]
|
||||
If you need to assert an element's attribute, prefer [`method: LocatorAssertions.toHaveAttribute`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
|
||||
:::
|
||||
|
||||
|
|
@ -1291,7 +1293,7 @@ Returns the [`element.innerHTML`](https://developer.mozilla.org/en-US/docs/Web/A
|
|||
|
||||
Returns the [`element.innerText`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/innerText).
|
||||
|
||||
:::caution Asserting text
|
||||
:::warning[Asserting text]
|
||||
If you need to assert text on the page, prefer [`method: LocatorAssertions.toHaveText`] with [`option: useInnerText`] option to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
|
||||
:::
|
||||
|
||||
|
|
@ -1307,7 +1309,7 @@ If you need to assert text on the page, prefer [`method: LocatorAssertions.toHav
|
|||
|
||||
Returns the value for the matching `<input>` or `<textarea>` or `<select>` element.
|
||||
|
||||
:::caution Asserting value
|
||||
:::warning[Asserting value]
|
||||
If you need to assert input value, prefer [`method: LocatorAssertions.toHaveValue`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
|
||||
:::
|
||||
|
||||
|
|
@ -1349,7 +1351,7 @@ Throws elements that are not an input, textarea or a select. However, if the ele
|
|||
|
||||
Returns whether the element is checked. Throws if the element is not a checkbox or radio input.
|
||||
|
||||
:::caution Asserting checked state
|
||||
:::warning[Asserting checked state]
|
||||
If you need to assert that checkbox is checked, prefer [`method: LocatorAssertions.toBeChecked`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
|
||||
:::
|
||||
|
||||
|
|
@ -1387,7 +1389,7 @@ var isChecked = await page.GetByRole(AriaRole.Checkbox).IsCheckedAsync();
|
|||
|
||||
Returns whether the element is disabled, the opposite of [enabled](../actionability.md#enabled).
|
||||
|
||||
:::caution Asserting disabled state
|
||||
:::warning[Asserting disabled state]
|
||||
If you need to assert that an element is disabled, prefer [`method: LocatorAssertions.toBeDisabled`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
|
||||
:::
|
||||
|
||||
|
|
@ -1425,7 +1427,7 @@ Boolean disabled = await page.GetByRole(AriaRole.Button).IsDisabledAsync();
|
|||
|
||||
Returns whether the element is [editable](../actionability.md#editable).
|
||||
|
||||
:::caution Asserting editable state
|
||||
:::warning[Asserting editable state]
|
||||
If you need to assert that an element is editable, prefer [`method: LocatorAssertions.toBeEditable`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
|
||||
:::
|
||||
|
||||
|
|
@ -1463,7 +1465,7 @@ Boolean editable = await page.GetByRole(AriaRole.Textbox).IsEditableAsync();
|
|||
|
||||
Returns whether the element is [enabled](../actionability.md#enabled).
|
||||
|
||||
:::caution Asserting enabled state
|
||||
:::warning[Asserting enabled state]
|
||||
If you need to assert that an element is enabled, prefer [`method: LocatorAssertions.toBeEnabled`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
|
||||
:::
|
||||
|
||||
|
|
@ -1501,7 +1503,7 @@ Boolean enabled = await page.GetByRole(AriaRole.Button).IsEnabledAsync();
|
|||
|
||||
Returns whether the element is hidden, the opposite of [visible](../actionability.md#visible).
|
||||
|
||||
:::caution Asserting visibility
|
||||
:::warning[Asserting visibility]
|
||||
If you need to assert that element is hidden, prefer [`method: LocatorAssertions.toBeHidden`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
|
||||
:::
|
||||
|
||||
|
|
@ -1538,7 +1540,7 @@ Boolean hidden = await page.GetByRole(AriaRole.Button).IsHiddenAsync();
|
|||
|
||||
Returns whether the element is [visible](../actionability.md#visible).
|
||||
|
||||
:::caution Asserting visibility
|
||||
:::warning[Asserting visibility]
|
||||
If you need to assert that element is visible, prefer [`method: LocatorAssertions.toBeVisible`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
|
||||
:::
|
||||
|
||||
|
|
@ -2322,7 +2324,7 @@ When all steps combined have not finished during the specified [`option: timeout
|
|||
|
||||
Returns the [`node.textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent).
|
||||
|
||||
:::caution Asserting text
|
||||
:::warning[Asserting text]
|
||||
If you need to assert text on the page, prefer [`method: LocatorAssertions.toHaveText`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
|
||||
:::
|
||||
|
||||
|
|
|
|||
|
|
@ -3146,7 +3146,7 @@ return value resolves to `[]`.
|
|||
## async method: Page.addLocatorHandler
|
||||
* since: v1.42
|
||||
|
||||
:::warning Experimental
|
||||
:::warning[Experimental]
|
||||
This method is experimental and its behavior may change in the upcoming releases.
|
||||
:::
|
||||
|
||||
|
|
|
|||
|
|
@ -405,7 +405,7 @@ An instance of [FormData] can be created via [`method: APIRequestContext.createF
|
|||
|
||||
## js-python-fetch-option-multipart
|
||||
* langs: js, python
|
||||
- `multipart` <[Object]<[string], [string]|[float]|[boolean]|[ReadStream]|[Object]>>
|
||||
- `multipart` <[Object]<[string], [string]|[float]|[boolean]|[ReadStream]|[Object]|Array<[string]|[float]|[boolean]|[ReadStream]|[Object]>>>
|
||||
- `name` <[string]> File name
|
||||
- `mimeType` <[string]> File type
|
||||
- `buffer` <[Buffer]> File content
|
||||
|
|
@ -413,7 +413,8 @@ An instance of [FormData] can be created via [`method: APIRequestContext.createF
|
|||
Provides an object that will be serialized as html form using `multipart/form-data` encoding and sent as
|
||||
this request body. If this parameter is specified `content-type` header will be set to `multipart/form-data`
|
||||
unless explicitly provided. File values can be passed either as [`fs.ReadStream`](https://nodejs.org/api/fs.html#fs_class_fs_readstream)
|
||||
or as file-like object containing file name, mime-type and its content.
|
||||
or as file-like object containing file name, mime-type and its content. If the value is an array, each element
|
||||
will be sent as a separate field with the same name.
|
||||
|
||||
## csharp-fetch-option-multipart
|
||||
* langs: csharp
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ jobs:
|
|||
- name: Run Playwright tests
|
||||
run: npx playwright test
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
|
|
@ -103,7 +103,7 @@ jobs:
|
|||
- name: Run your tests
|
||||
run: pytest --tracing=retain-on-failure
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-traces
|
||||
path: test-results/
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ var jsHandle = await page.EvaluateHandleAsync("window");
|
|||
|
||||
## Element Handles
|
||||
|
||||
:::caution Discouraged
|
||||
:::warning[Discouraged]
|
||||
The use of [ElementHandle] is discouraged, use [Locator] objects and web-first assertions instead.
|
||||
:::
|
||||
|
||||
|
|
|
|||
|
|
@ -264,7 +264,7 @@ Role locators include [buttons, checkboxes, headings, links, lists, tables, and
|
|||
|
||||
Note that role locators **do not replace** accessibility audits and conformance tests, but rather give early feedback about the ARIA guidelines.
|
||||
|
||||
:::note When to use role locators
|
||||
:::note[When to use role locators]
|
||||
We recommend prioritizing role locators to locate elements, as it is the closest way to how users and assistive technology perceive the page.
|
||||
:::
|
||||
|
||||
|
|
@ -301,7 +301,7 @@ page.get_by_label("Password").fill("secret")
|
|||
await page.GetByLabel("Password").FillAsync("secret");
|
||||
```
|
||||
|
||||
:::note When to use label locators
|
||||
:::note[When to use label locators]
|
||||
Use this locator when locating form fields.
|
||||
:::
|
||||
### Locate by placeholder
|
||||
|
|
@ -340,7 +340,7 @@ await page
|
|||
.FillAsync("playwright@microsoft.com");
|
||||
```
|
||||
|
||||
:::note When to use placeholder locators
|
||||
:::note[When to use placeholder locators]
|
||||
Use this locator when locating form elements that do not have labels but do have placeholder texts.
|
||||
:::
|
||||
|
||||
|
|
@ -433,7 +433,7 @@ await Expect(Page
|
|||
Matching by text always normalizes whitespace, even with exact match. For example, it turns multiple spaces into one, turns line breaks into spaces and ignores leading and trailing whitespace.
|
||||
:::
|
||||
|
||||
:::note When to use text locators
|
||||
:::note[When to use text locators]
|
||||
We recommend using text locators to find non interactive elements like `div`, `span`, `p`, etc. For interactive elements like `button`, `a`, `input`, etc. use [role locators](#locate-by-role).
|
||||
:::
|
||||
|
||||
|
|
@ -471,7 +471,7 @@ page.get_by_alt_text("playwright logo").click()
|
|||
await page.GetByAltText("playwright logo").ClickAsync();
|
||||
```
|
||||
|
||||
:::note When to use alt locators
|
||||
:::note[When to use alt locators]
|
||||
Use this locator when your element supports alt text such as `img` and `area` elements.
|
||||
:::
|
||||
|
||||
|
|
@ -507,7 +507,7 @@ expect(page.get_by_title("Issues count")).to_have_text("25 issues")
|
|||
await Expect(Page.GetByTitle("Issues count")).toHaveText("25 issues");
|
||||
```
|
||||
|
||||
:::note When to use title locators
|
||||
:::note[When to use title locators]
|
||||
Use this locator when your element has the `title` attribute.
|
||||
:::
|
||||
|
||||
|
|
@ -543,7 +543,7 @@ page.get_by_test_id("directions").click()
|
|||
await page.GetByTestId("directions").ClickAsync();
|
||||
```
|
||||
|
||||
:::note When to use testid locators
|
||||
:::note[When to use testid locators]
|
||||
You can also use test ids when you choose to use the test id methodology or when you can't locate by [role](#locate-by-role) or [text](#locate-by-text).
|
||||
:::
|
||||
|
||||
|
|
@ -693,7 +693,7 @@ await page.Locator("#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > di
|
|||
await page.Locator("//*[@id='tsf']/div[2]/div[1]/div[1]/div/div[2]/input").ClickAsync();
|
||||
```
|
||||
|
||||
:::note When to use this
|
||||
:::note[When to use this]
|
||||
CSS and XPath are not recommended as the DOM can often change leading to non resilient tests. Instead, try to come up with a locator that is close to how the user perceives the page such as [role locators](#locate-by-role) or [define an explicit testing contract](#locate-by-test-id) using test ids.
|
||||
:::
|
||||
|
||||
|
|
|
|||
|
|
@ -556,10 +556,10 @@ export default defineConfig({
|
|||
Whether to record trace for each test. Defaults to `'off'`.
|
||||
* `'off'`: Do not record trace.
|
||||
* `'on'`: Record trace for each test.
|
||||
* `'retain-on-failure'`: Record trace for each test, but remove all traces from successful test runs.
|
||||
* `'on-first-retry'`: Record trace only when retrying a test for the first time.
|
||||
* `'on-all-retries'`: Record traces only when retrying for all retries.
|
||||
* `'retain-on-first-failure'`: Record traces only when the test fails for the first time.
|
||||
* `'on-all-retries'`: Record trace only when retrying a test.
|
||||
* `'retain-on-failure'`: Record trace for each test. When test run passes, remove the recorded trace.
|
||||
* `'retain-on-first-failure'`: Record trace for the first run of each test, but not for retires. When test run passes, remove the recorded trace.
|
||||
|
||||
For more control, pass an object that specifies `mode` and trace features to enable.
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ jobs:
|
|||
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
|
||||
- name: Upload blob report to GitHub Actions Artifacts
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: blob-report-${{ matrix.shardIndex }}
|
||||
|
|
@ -104,7 +104,7 @@ jobs:
|
|||
...
|
||||
merge-reports:
|
||||
# Merge reports after playwright-tests, even if some shards have failed
|
||||
if: always()
|
||||
if: ${{ !cancelled() }}
|
||||
needs: [playwright-tests]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export function msToString(ms: number): string {
|
|||
return '-';
|
||||
|
||||
if (ms === 0)
|
||||
return '0';
|
||||
return '0ms';
|
||||
|
||||
if (ms < 1000)
|
||||
return ms.toFixed(0) + 'ms';
|
||||
|
|
|
|||
|
|
@ -15,25 +15,25 @@
|
|||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
"revision": "1445",
|
||||
"revision": "1447",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "123.0"
|
||||
"browserVersion": "124.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox-asan",
|
||||
"revision": "1445",
|
||||
"revision": "1447",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "123.0"
|
||||
"browserVersion": "124.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox-beta",
|
||||
"revision": "1444",
|
||||
"revision": "1447",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "124.0b3"
|
||||
"browserVersion": "125.0b3"
|
||||
},
|
||||
{
|
||||
"name": "webkit",
|
||||
"revision": "1990",
|
||||
"revision": "1992",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"mac10.14": "1446",
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export type FetchOptions = {
|
|||
headers?: Headers,
|
||||
data?: string | Buffer | Serializable,
|
||||
form?: { [key: string]: string|number|boolean; };
|
||||
multipart?: { [key: string]: string|number|boolean|fs.ReadStream|FilePayload; };
|
||||
multipart?: { [key: string]: string|number|boolean|fs.ReadStream|FilePayload|Array<string|number|boolean|fs.ReadStream|FilePayload>; };
|
||||
timeout?: number,
|
||||
failOnStatusCode?: boolean,
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
|
|
@ -188,15 +188,11 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||
multipartData = [];
|
||||
// Convert file-like values to ServerFilePayload structs.
|
||||
for (const [name, value] of Object.entries(options.multipart)) {
|
||||
if (isFilePayload(value)) {
|
||||
const payload = value as FilePayload;
|
||||
if (!Buffer.isBuffer(payload.buffer))
|
||||
throw new Error(`Unexpected buffer type of 'data.${name}'`);
|
||||
multipartData.push({ name, file: filePayloadToJson(payload) });
|
||||
} else if (value instanceof fs.ReadStream) {
|
||||
multipartData.push({ name, file: await readStreamToJson(value as fs.ReadStream) });
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value)
|
||||
multipartData.push(await toFormField(name, item));
|
||||
} else {
|
||||
multipartData.push({ name, value: String(value) });
|
||||
multipartData.push(await toFormField(name, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -234,6 +230,19 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||
}
|
||||
}
|
||||
|
||||
async function toFormField(name: string, value: string|number|boolean|fs.ReadStream|FilePayload): Promise<channels.FormField> {
|
||||
if (isFilePayload(value)) {
|
||||
const payload = value as FilePayload;
|
||||
if (!Buffer.isBuffer(payload.buffer))
|
||||
throw new Error(`Unexpected buffer type of 'data.${name}'`);
|
||||
return { name, file: filePayloadToJson(payload) };
|
||||
} else if (value instanceof fs.ReadStream) {
|
||||
return { name, file: await readStreamToJson(value as fs.ReadStream) };
|
||||
} else {
|
||||
return { name, value: String(value) };
|
||||
}
|
||||
}
|
||||
|
||||
function isJsonParsable(value: any) {
|
||||
if (typeof value !== 'string')
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ export class Locator implements api.Locator {
|
|||
return await this._frame.$$(this._selector);
|
||||
}
|
||||
|
||||
enterFrame() {
|
||||
contentFrame() {
|
||||
return new FrameLocator(this._frame, this._selector);
|
||||
}
|
||||
|
||||
|
|
@ -408,7 +408,7 @@ export class FrameLocator implements api.FrameLocator {
|
|||
return this.locator(getByRoleSelector(role, options));
|
||||
}
|
||||
|
||||
exitFrame() {
|
||||
owner() {
|
||||
return new Locator(this._frame, this._frameSelector);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1472,7 +1472,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Firefox HiDPI": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0",
|
||||
"screen": {
|
||||
"width": 1792,
|
||||
"height": 1120
|
||||
|
|
@ -1532,7 +1532,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Firefox": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0",
|
||||
"screen": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
|
|
|
|||
|
|
@ -147,6 +147,11 @@ export module Protocol {
|
|||
enabled: boolean;
|
||||
};
|
||||
export type setRequestInterceptionReturnValue = void;
|
||||
export type setCacheDisabledParameters = {
|
||||
browserContextId?: string;
|
||||
cacheDisabled: boolean;
|
||||
};
|
||||
export type setCacheDisabledReturnValue = void;
|
||||
export type setGeolocationOverrideParameters = {
|
||||
browserContextId?: string;
|
||||
geolocation: {
|
||||
|
|
@ -1099,6 +1104,7 @@ export module Protocol {
|
|||
"Browser.setContextProxy": Browser.setContextProxyParameters;
|
||||
"Browser.setHTTPCredentials": Browser.setHTTPCredentialsParameters;
|
||||
"Browser.setRequestInterception": Browser.setRequestInterceptionParameters;
|
||||
"Browser.setCacheDisabled": Browser.setCacheDisabledParameters;
|
||||
"Browser.setGeolocationOverride": Browser.setGeolocationOverrideParameters;
|
||||
"Browser.setUserAgentOverride": Browser.setUserAgentOverrideParameters;
|
||||
"Browser.setPlatformOverride": Browser.setPlatformOverrideParameters;
|
||||
|
|
@ -1179,6 +1185,7 @@ export module Protocol {
|
|||
"Browser.setContextProxy": Browser.setContextProxyReturnValue;
|
||||
"Browser.setHTTPCredentials": Browser.setHTTPCredentialsReturnValue;
|
||||
"Browser.setRequestInterception": Browser.setRequestInterceptionReturnValue;
|
||||
"Browser.setCacheDisabled": Browser.setCacheDisabledReturnValue;
|
||||
"Browser.setGeolocationOverride": Browser.setGeolocationOverrideReturnValue;
|
||||
"Browser.setUserAgentOverride": Browser.setUserAgentOverrideReturnValue;
|
||||
"Browser.setPlatformOverride": Browser.setPlatformOverrideReturnValue;
|
||||
|
|
|
|||
|
|
@ -231,12 +231,6 @@ x-pw-tool-item.value > x-div {
|
|||
mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path fill-rule='evenodd' clip-rule='evenodd' d='M4 6h8v1H4V6zm8 3H4v1h8V9z'/><path fill-rule='evenodd' clip-rule='evenodd' d='M1 4l1-1h12l1 1v8l-1 1H2l-1-1V4zm1 0v8h12V4H2z'/></svg>");
|
||||
}
|
||||
|
||||
x-pw-tool-item.screenshot > x-div {
|
||||
/* codicon: device-camera */
|
||||
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path fill-rule='evenodd' clip-rule='evenodd' d='M10.707 3H14.5l.5.5v9l-.5.5h-13l-.5-.5v-9l.5-.5h3.793l.853-.854L6.5 2h3l.354.146.853.854zM2 12h12V4h-3.5l-.354-.146L9.293 3H6.707l-.853.854L5.5 4H2v8zm1.5-7a.5.5 0 1 0 0 1 .5.5 0 0 0 0-1zM8 6a2 2 0 1 1 0 4 2 2 0 0 1 0-4zm0-1a3 3 0 1 0 0 6 3 3 0 0 0 0-6z'/></svg>");
|
||||
mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path fill-rule='evenodd' clip-rule='evenodd' d='M10.707 3H14.5l.5.5v9l-.5.5h-13l-.5-.5v-9l.5-.5h3.793l.853-.854L6.5 2h3l.354.146.853.854zM2 12h12V4h-3.5l-.354-.146L9.293 3H6.707l-.853.854L5.5 4H2v8zm1.5-7a.5.5 0 1 0 0 1 .5.5 0 0 0 0-1zM8 6a2 2 0 1 1 0 4 2 2 0 0 1 0-4zm0-1a3 3 0 1 0 0 6 3 3 0 0 0 0-6z'/></svg>");
|
||||
}
|
||||
|
||||
x-pw-tool-item.accept > x-div {
|
||||
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path d='M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/></svg>");
|
||||
mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path d='M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/></svg>");
|
||||
|
|
|
|||
|
|
@ -761,7 +761,6 @@ class Overlay {
|
|||
private _assertVisibilityToggle: HTMLElement;
|
||||
private _assertTextToggle: HTMLElement;
|
||||
private _assertValuesToggle: HTMLElement;
|
||||
private _assertScreenshotButton: HTMLElement;
|
||||
private _offsetX = 0;
|
||||
private _dragState: { offsetX: number, dragStart: { x: number, y: number } } | undefined;
|
||||
private _measure: { width: number, height: number } = { width: 0, height: 0 };
|
||||
|
|
@ -808,12 +807,6 @@ class Overlay {
|
|||
this._assertValuesToggle.appendChild(this._recorder.injectedScript.document.createElement('x-div'));
|
||||
toolsListElement.appendChild(this._assertValuesToggle);
|
||||
|
||||
this._assertScreenshotButton = this._recorder.injectedScript.document.createElement('x-pw-tool-item');
|
||||
this._assertScreenshotButton.title = 'Assert screenshot';
|
||||
this._assertScreenshotButton.classList.add('screenshot');
|
||||
this._assertScreenshotButton.appendChild(this._recorder.injectedScript.document.createElement('x-div'));
|
||||
toolsListElement.appendChild(this._assertScreenshotButton);
|
||||
|
||||
this._updateVisualPosition();
|
||||
this._refreshListeners();
|
||||
}
|
||||
|
|
@ -852,15 +845,6 @@ class Overlay {
|
|||
if (!this._assertValuesToggle.classList.contains('disabled'))
|
||||
this._recorder.delegate.setMode?.(this._recorder.state.mode === 'assertingValue' ? 'recording' : 'assertingValue');
|
||||
}),
|
||||
addEventListener(this._assertScreenshotButton, 'click', () => {
|
||||
if (!this._assertScreenshotButton.classList.contains('disabled')) {
|
||||
this._recorder.delegate.recordAction?.({
|
||||
name: 'assertScreenshot',
|
||||
signals: [],
|
||||
});
|
||||
this.flashToolSucceeded('assertScreenshot');
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -883,7 +867,6 @@ class Overlay {
|
|||
this._assertTextToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting');
|
||||
this._assertValuesToggle.classList.toggle('active', state.mode === 'assertingValue');
|
||||
this._assertValuesToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting');
|
||||
this._assertScreenshotButton.classList.toggle('disabled', state.mode !== 'recording');
|
||||
if (this._offsetX !== state.overlay.offsetX) {
|
||||
this._offsetX = state.overlay.offsetX;
|
||||
this._updateVisualPosition();
|
||||
|
|
@ -894,12 +877,8 @@ class Overlay {
|
|||
this._showOverlay();
|
||||
}
|
||||
|
||||
flashToolSucceeded(tool: 'assertingVisibility' | 'assertingValue' | 'assertScreenshot') {
|
||||
const element = {
|
||||
'assertingVisibility': this._assertVisibilityToggle,
|
||||
'assertingValue': this._assertValuesToggle,
|
||||
'assertScreenshot': this._assertScreenshotButton,
|
||||
}[tool];
|
||||
flashToolSucceeded(tool: 'assertingVisibility' | 'assertingValue') {
|
||||
const element = tool === 'assertingVisibility' ? this._assertVisibilityToggle : this._assertValuesToggle;
|
||||
element.classList.add('succeeded');
|
||||
setTimeout(() => element.classList.remove('succeeded'), 2000);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,8 +164,6 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
|||
const assertion = action.value ? `ToHaveValueAsync(${quote(action.value)})` : `ToBeEmptyAsync()`;
|
||||
return `await Expect(${subject}.${this._asLocator(action.selector)}).${assertion};`;
|
||||
}
|
||||
case 'assertScreenshot':
|
||||
return `// AssertScreenshot(await ${subject}.ScreenshotAsync());`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -152,8 +152,6 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
|||
const assertion = action.value ? `hasValue(${quote(action.value)})` : `isEmpty()`;
|
||||
return `assertThat(${subject}.${this._asLocator(action.selector, inFrameLocator)}).${assertion};`;
|
||||
}
|
||||
case 'assertScreenshot':
|
||||
return `// assertScreenshot(${subject}.screenshot());`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -135,8 +135,6 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
|||
const assertion = action.value ? `toHaveValue(${quote(action.value)})` : `toBeEmpty()`;
|
||||
return `${this._isTest ? '' : '// '}await expect(${subject}.${this._asLocator(action.selector)}).${assertion};`;
|
||||
}
|
||||
case 'assertScreenshot':
|
||||
return `${this._isTest ? '' : '// '}await expect(${subject}).toHaveScreenshot();`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
|||
if (signals.dialog)
|
||||
formatter.add(` ${pageAlias}.once("dialog", lambda dialog: dialog.dismiss())`);
|
||||
|
||||
let code = this._generateActionCall(subject, action);
|
||||
let code = `${this._awaitPrefix}${this._generateActionCall(subject, action)}`;
|
||||
|
||||
if (signals.popup) {
|
||||
code = `${this._asyncPrefix}with ${pageAlias}.expect_popup() as ${signals.popup.popupAlias}_info {
|
||||
|
|
@ -99,7 +99,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
|||
case 'openPage':
|
||||
throw Error('Not reached');
|
||||
case 'closePage':
|
||||
return `${this._awaitPrefix}${subject}.close()`;
|
||||
return `${subject}.close()`;
|
||||
case 'click': {
|
||||
let method = 'click';
|
||||
if (action.clickCount === 2)
|
||||
|
|
@ -115,37 +115,35 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
|||
if (action.position)
|
||||
options.position = action.position;
|
||||
const optionsString = formatOptions(options, false);
|
||||
return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.${method}(${optionsString})`;
|
||||
return `${subject}.${this._asLocator(action.selector)}.${method}(${optionsString})`;
|
||||
}
|
||||
case 'check':
|
||||
return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.check()`;
|
||||
return `${subject}.${this._asLocator(action.selector)}.check()`;
|
||||
case 'uncheck':
|
||||
return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.uncheck()`;
|
||||
return `${subject}.${this._asLocator(action.selector)}.uncheck()`;
|
||||
case 'fill':
|
||||
return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.fill(${quote(action.text)})`;
|
||||
return `${subject}.${this._asLocator(action.selector)}.fill(${quote(action.text)})`;
|
||||
case 'setInputFiles':
|
||||
return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.set_input_files(${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`;
|
||||
return `${subject}.${this._asLocator(action.selector)}.set_input_files(${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`;
|
||||
case 'press': {
|
||||
const modifiers = toModifiers(action.modifiers);
|
||||
const shortcut = [...modifiers, action.key].join('+');
|
||||
return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)})`;
|
||||
return `${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)})`;
|
||||
}
|
||||
case 'navigate':
|
||||
return `${this._awaitPrefix}${subject}.goto(${quote(action.url)})`;
|
||||
return `${subject}.goto(${quote(action.url)})`;
|
||||
case 'select':
|
||||
return `${this._awaitPrefix}${subject}.${this._asLocator(action.selector)}.select_option(${formatValue(action.options.length === 1 ? action.options[0] : action.options)})`;
|
||||
return `${subject}.${this._asLocator(action.selector)}.select_option(${formatValue(action.options.length === 1 ? action.options[0] : action.options)})`;
|
||||
case 'assertText':
|
||||
return `${this._awaitPrefix}expect(${subject}.${this._asLocator(action.selector)}).${action.substring ? 'to_contain_text' : 'to_have_text'}(${quote(action.text)})`;
|
||||
return `expect(${subject}.${this._asLocator(action.selector)}).${action.substring ? 'to_contain_text' : 'to_have_text'}(${quote(action.text)})`;
|
||||
case 'assertChecked':
|
||||
return `${this._awaitPrefix}expect(${subject}.${this._asLocator(action.selector)}).${action.checked ? 'to_be_checked()' : 'not_to_be_checked()'}`;
|
||||
return `expect(${subject}.${this._asLocator(action.selector)}).${action.checked ? 'to_be_checked()' : 'not_to_be_checked()'}`;
|
||||
case 'assertVisible':
|
||||
return `${this._awaitPrefix}expect(${subject}.${this._asLocator(action.selector)}).to_be_visible()`;
|
||||
return `expect(${subject}.${this._asLocator(action.selector)}).to_be_visible()`;
|
||||
case 'assertValue': {
|
||||
const assertion = action.value ? `to_have_value(${quote(action.value)})` : `to_be_empty()`;
|
||||
return `${this._awaitPrefix}expect(${subject}.${this._asLocator(action.selector)}).${assertion}`;
|
||||
return `expect(${subject}.${this._asLocator(action.selector)}).${assertion};`;
|
||||
}
|
||||
case 'assertScreenshot':
|
||||
return `# assert_screenshot(${this._awaitPrefix}${subject}.screenshot())`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ export type ActionName =
|
|||
'assertText' |
|
||||
'assertValue' |
|
||||
'assertChecked' |
|
||||
'assertScreenshot' |
|
||||
'assertVisible';
|
||||
|
||||
export type ActionBase = {
|
||||
|
|
@ -120,11 +119,7 @@ export type AssertVisibleAction = ActionBase & {
|
|||
selector: string,
|
||||
};
|
||||
|
||||
export type AssertScreenshotAction = ActionBase & {
|
||||
name: 'assertScreenshot',
|
||||
};
|
||||
|
||||
export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction | AssertScreenshotAction;
|
||||
export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction;
|
||||
export type AssertAction = AssertCheckedAction | AssertValueAction | AssertTextAction | AssertVisibleAction;
|
||||
|
||||
// Signals.
|
||||
|
|
|
|||
|
|
@ -36,6 +36,14 @@ export type TraceViewerServerOptions = {
|
|||
};
|
||||
|
||||
export type TraceViewerRedirectOptions = {
|
||||
args?: string[];
|
||||
grep?: string;
|
||||
grepInvert?: string;
|
||||
project?: string[];
|
||||
workers?: number | string;
|
||||
headed?: boolean;
|
||||
timeout?: number;
|
||||
reporter?: string[];
|
||||
webApp?: string;
|
||||
isServer?: boolean;
|
||||
};
|
||||
|
|
@ -102,19 +110,36 @@ export async function startTraceViewerServer(options?: TraceViewerServerOptions)
|
|||
}
|
||||
|
||||
export async function installRootRedirect(server: HttpServer, traceUrls: string[], options: TraceViewerRedirectOptions) {
|
||||
const params = (traceUrls || []).map(t => `trace=${encodeURIComponent(t)}`);
|
||||
const params = new URLSearchParams();
|
||||
for (const traceUrl of traceUrls)
|
||||
params.append('trace', traceUrl);
|
||||
if (server.wsGuid())
|
||||
params.push('ws=' + server.wsGuid());
|
||||
params.append('ws', server.wsGuid()!);
|
||||
if (options?.isServer)
|
||||
params.push('isServer');
|
||||
params.append('isServer', '');
|
||||
if (isUnderTest())
|
||||
params.push('isUnderTest=true');
|
||||
const searchQuery = params.length ? '?' + params.join('&') : '';
|
||||
const urlPath = `/trace/${options.webApp || 'index.html'}${searchQuery}`;
|
||||
params.append('isUnderTest', 'true');
|
||||
for (const arg of options.args || [])
|
||||
params.append('arg', arg);
|
||||
if (options.grep)
|
||||
params.append('grep', options.grep);
|
||||
if (options.grepInvert)
|
||||
params.append('grepInvert', options.grepInvert);
|
||||
for (const project of options.project || [])
|
||||
params.append('project', project);
|
||||
if (options.workers)
|
||||
params.append('workers', String(options.workers));
|
||||
if (options.timeout)
|
||||
params.append('timeout', String(options.timeout));
|
||||
if (options.headed)
|
||||
params.append('headed', '');
|
||||
for (const reporter of options.reporter || [])
|
||||
params.append('reporter', reporter);
|
||||
|
||||
server.routePath('/', (request, response) => {
|
||||
const urlPath = `/trace/${options.webApp || 'index.html'}?${params.toString()}`;
|
||||
server.routePath('/', (_, response) => {
|
||||
response.statusCode = 302;
|
||||
response.setHeader('Location', urlPath + request.url!.substring(1));
|
||||
response.setHeader('Location', urlPath);
|
||||
response.end();
|
||||
return true;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1569,7 +1569,7 @@ export module Protocol {
|
|||
/**
|
||||
* The reason the console is being cleared.
|
||||
*/
|
||||
export type ClearReason = "console-api"|"main-frame-navigation";
|
||||
export type ClearReason = "console-api"|"frontend"|"main-frame-navigation";
|
||||
/**
|
||||
* Logging channel.
|
||||
*/
|
||||
|
|
@ -1738,6 +1738,14 @@ export module Protocol {
|
|||
}
|
||||
export type clearMessagesReturnValue = {
|
||||
}
|
||||
/**
|
||||
* Control whether calling <code>console.clear()</code> has an effect in Web Inspector. Defaults to true.
|
||||
*/
|
||||
export type setConsoleClearAPIEnabledParameters = {
|
||||
enable: boolean;
|
||||
}
|
||||
export type setConsoleClearAPIEnabledReturnValue = {
|
||||
}
|
||||
/**
|
||||
* List of the different message sources that are non-default logging channels.
|
||||
*/
|
||||
|
|
@ -9388,6 +9396,7 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
|
|||
"Console.enable": Console.enableParameters;
|
||||
"Console.disable": Console.disableParameters;
|
||||
"Console.clearMessages": Console.clearMessagesParameters;
|
||||
"Console.setConsoleClearAPIEnabled": Console.setConsoleClearAPIEnabledParameters;
|
||||
"Console.getLoggingChannels": Console.getLoggingChannelsParameters;
|
||||
"Console.setLoggingChannelLevel": Console.setLoggingChannelLevelParameters;
|
||||
"DOM.getDocument": DOM.getDocumentParameters;
|
||||
|
|
@ -9698,6 +9707,7 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
|
|||
"Console.enable": Console.enableReturnValue;
|
||||
"Console.disable": Console.disableReturnValue;
|
||||
"Console.clearMessages": Console.clearMessagesReturnValue;
|
||||
"Console.setConsoleClearAPIEnabled": Console.setConsoleClearAPIEnabledReturnValue;
|
||||
"Console.getLoggingChannels": Console.getLoggingChannelsReturnValue;
|
||||
"Console.setLoggingChannelLevel": Console.setLoggingChannelLevelReturnValue;
|
||||
"DOM.getDocument": DOM.getDocumentReturnValue;
|
||||
|
|
|
|||
222
packages/playwright-core/types/types.d.ts
vendored
222
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -11238,6 +11238,27 @@ export interface Locator {
|
|||
trial?: boolean;
|
||||
}): Promise<void>;
|
||||
|
||||
/**
|
||||
* Returns a {@link FrameLocator} object pointing to the same `iframe` as this locator.
|
||||
*
|
||||
* Useful when you have a {@link Locator} object obtained somewhere, and later on would like to interact with the
|
||||
* content inside the frame.
|
||||
*
|
||||
* For a reverse operation, use
|
||||
* [frameLocator.owner()](https://playwright.dev/docs/api/class-framelocator#frame-locator-owner).
|
||||
*
|
||||
* **Usage**
|
||||
*
|
||||
* ```js
|
||||
* const locator = page.locator('iframe[name="embedded"]');
|
||||
* // ...
|
||||
* const frameLocator = locator.contentFrame();
|
||||
* await frameLocator.getByRole('button').click();
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
contentFrame(): FrameLocator;
|
||||
|
||||
/**
|
||||
* Returns the number of elements matching the locator.
|
||||
*
|
||||
|
|
@ -11462,24 +11483,6 @@ export interface Locator {
|
|||
*/
|
||||
elementHandles(): Promise<Array<ElementHandle>>;
|
||||
|
||||
/**
|
||||
* Returns a {@link FrameLocator} object pointing to the same `iframe` as this locator.
|
||||
*
|
||||
* Useful when you have a {@link Locator} object obtained somewhere, and later on would like to interact with the
|
||||
* content inside the frame.
|
||||
*
|
||||
* **Usage**
|
||||
*
|
||||
* ```js
|
||||
* const locator = page.locator('iframe[name="embedded"]');
|
||||
* // ...
|
||||
* const frameLocator = locator.enterFrame();
|
||||
* await frameLocator.getByRole('button').click();
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
enterFrame(): FrameLocator;
|
||||
|
||||
/**
|
||||
* Set a value to the input field.
|
||||
*
|
||||
|
|
@ -15680,7 +15683,8 @@ export interface APIRequestContext {
|
|||
* request body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless
|
||||
* explicitly provided. File values can be passed either as
|
||||
* [`fs.ReadStream`](https://nodejs.org/api/fs.html#fs_class_fs_readstream) or as file-like object containing file
|
||||
* name, mime-type and its content.
|
||||
* name, mime-type and its content. If the value is an array, each element will be sent as a separate field with the
|
||||
* same name.
|
||||
*/
|
||||
multipart?: { [key: string]: string|number|boolean|ReadStream|{
|
||||
/**
|
||||
|
|
@ -15697,7 +15701,22 @@ export interface APIRequestContext {
|
|||
* File content
|
||||
*/
|
||||
buffer: Buffer;
|
||||
}; };
|
||||
}|Array<string|number|boolean|ReadStream|{
|
||||
/**
|
||||
* File name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* File type
|
||||
*/
|
||||
mimeType: string;
|
||||
|
||||
/**
|
||||
* File content
|
||||
*/
|
||||
buffer: Buffer;
|
||||
}>; };
|
||||
|
||||
/**
|
||||
* Query parameters to be sent with the URL.
|
||||
|
|
@ -15814,7 +15833,8 @@ export interface APIRequestContext {
|
|||
* request body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless
|
||||
* explicitly provided. File values can be passed either as
|
||||
* [`fs.ReadStream`](https://nodejs.org/api/fs.html#fs_class_fs_readstream) or as file-like object containing file
|
||||
* name, mime-type and its content.
|
||||
* name, mime-type and its content. If the value is an array, each element will be sent as a separate field with the
|
||||
* same name.
|
||||
*/
|
||||
multipart?: { [key: string]: string|number|boolean|ReadStream|{
|
||||
/**
|
||||
|
|
@ -15831,7 +15851,22 @@ export interface APIRequestContext {
|
|||
* File content
|
||||
*/
|
||||
buffer: Buffer;
|
||||
}; };
|
||||
}|Array<string|number|boolean|ReadStream|{
|
||||
/**
|
||||
* File name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* File type
|
||||
*/
|
||||
mimeType: string;
|
||||
|
||||
/**
|
||||
* File content
|
||||
*/
|
||||
buffer: Buffer;
|
||||
}>; };
|
||||
|
||||
/**
|
||||
* Query parameters to be sent with the URL.
|
||||
|
|
@ -15908,7 +15943,8 @@ export interface APIRequestContext {
|
|||
* request body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless
|
||||
* explicitly provided. File values can be passed either as
|
||||
* [`fs.ReadStream`](https://nodejs.org/api/fs.html#fs_class_fs_readstream) or as file-like object containing file
|
||||
* name, mime-type and its content.
|
||||
* name, mime-type and its content. If the value is an array, each element will be sent as a separate field with the
|
||||
* same name.
|
||||
*/
|
||||
multipart?: { [key: string]: string|number|boolean|ReadStream|{
|
||||
/**
|
||||
|
|
@ -15925,7 +15961,22 @@ export interface APIRequestContext {
|
|||
* File content
|
||||
*/
|
||||
buffer: Buffer;
|
||||
}; };
|
||||
}|Array<string|number|boolean|ReadStream|{
|
||||
/**
|
||||
* File name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* File type
|
||||
*/
|
||||
mimeType: string;
|
||||
|
||||
/**
|
||||
* File content
|
||||
*/
|
||||
buffer: Buffer;
|
||||
}>; };
|
||||
|
||||
/**
|
||||
* Query parameters to be sent with the URL.
|
||||
|
|
@ -15988,7 +16039,8 @@ export interface APIRequestContext {
|
|||
* request body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless
|
||||
* explicitly provided. File values can be passed either as
|
||||
* [`fs.ReadStream`](https://nodejs.org/api/fs.html#fs_class_fs_readstream) or as file-like object containing file
|
||||
* name, mime-type and its content.
|
||||
* name, mime-type and its content. If the value is an array, each element will be sent as a separate field with the
|
||||
* same name.
|
||||
*/
|
||||
multipart?: { [key: string]: string|number|boolean|ReadStream|{
|
||||
/**
|
||||
|
|
@ -16005,7 +16057,22 @@ export interface APIRequestContext {
|
|||
* File content
|
||||
*/
|
||||
buffer: Buffer;
|
||||
}; };
|
||||
}|Array<string|number|boolean|ReadStream|{
|
||||
/**
|
||||
* File name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* File type
|
||||
*/
|
||||
mimeType: string;
|
||||
|
||||
/**
|
||||
* File content
|
||||
*/
|
||||
buffer: Buffer;
|
||||
}>; };
|
||||
|
||||
/**
|
||||
* Query parameters to be sent with the URL.
|
||||
|
|
@ -16068,7 +16135,8 @@ export interface APIRequestContext {
|
|||
* request body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless
|
||||
* explicitly provided. File values can be passed either as
|
||||
* [`fs.ReadStream`](https://nodejs.org/api/fs.html#fs_class_fs_readstream) or as file-like object containing file
|
||||
* name, mime-type and its content.
|
||||
* name, mime-type and its content. If the value is an array, each element will be sent as a separate field with the
|
||||
* same name.
|
||||
*/
|
||||
multipart?: { [key: string]: string|number|boolean|ReadStream|{
|
||||
/**
|
||||
|
|
@ -16085,7 +16153,22 @@ export interface APIRequestContext {
|
|||
* File content
|
||||
*/
|
||||
buffer: Buffer;
|
||||
}; };
|
||||
}|Array<string|number|boolean|ReadStream|{
|
||||
/**
|
||||
* File name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* File type
|
||||
*/
|
||||
mimeType: string;
|
||||
|
||||
/**
|
||||
* File content
|
||||
*/
|
||||
buffer: Buffer;
|
||||
}>; };
|
||||
|
||||
/**
|
||||
* Query parameters to be sent with the URL.
|
||||
|
|
@ -16199,7 +16282,8 @@ export interface APIRequestContext {
|
|||
* request body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless
|
||||
* explicitly provided. File values can be passed either as
|
||||
* [`fs.ReadStream`](https://nodejs.org/api/fs.html#fs_class_fs_readstream) or as file-like object containing file
|
||||
* name, mime-type and its content.
|
||||
* name, mime-type and its content. If the value is an array, each element will be sent as a separate field with the
|
||||
* same name.
|
||||
*/
|
||||
multipart?: { [key: string]: string|number|boolean|ReadStream|{
|
||||
/**
|
||||
|
|
@ -16216,7 +16300,22 @@ export interface APIRequestContext {
|
|||
* File content
|
||||
*/
|
||||
buffer: Buffer;
|
||||
}; };
|
||||
}|Array<string|number|boolean|ReadStream|{
|
||||
/**
|
||||
* File name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* File type
|
||||
*/
|
||||
mimeType: string;
|
||||
|
||||
/**
|
||||
* File content
|
||||
*/
|
||||
buffer: Buffer;
|
||||
}>; };
|
||||
|
||||
/**
|
||||
* Query parameters to be sent with the URL.
|
||||
|
|
@ -16279,7 +16378,8 @@ export interface APIRequestContext {
|
|||
* request body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless
|
||||
* explicitly provided. File values can be passed either as
|
||||
* [`fs.ReadStream`](https://nodejs.org/api/fs.html#fs_class_fs_readstream) or as file-like object containing file
|
||||
* name, mime-type and its content.
|
||||
* name, mime-type and its content. If the value is an array, each element will be sent as a separate field with the
|
||||
* same name.
|
||||
*/
|
||||
multipart?: { [key: string]: string|number|boolean|ReadStream|{
|
||||
/**
|
||||
|
|
@ -16296,7 +16396,22 @@ export interface APIRequestContext {
|
|||
* File content
|
||||
*/
|
||||
buffer: Buffer;
|
||||
}; };
|
||||
}|Array<string|number|boolean|ReadStream|{
|
||||
/**
|
||||
* File name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* File type
|
||||
*/
|
||||
mimeType: string;
|
||||
|
||||
/**
|
||||
* File content
|
||||
*/
|
||||
buffer: Buffer;
|
||||
}>; };
|
||||
|
||||
/**
|
||||
* Query parameters to be sent with the URL.
|
||||
|
|
@ -17783,32 +17898,14 @@ export interface FileChooser {
|
|||
* **Converting Locator to FrameLocator**
|
||||
*
|
||||
* If you have a {@link Locator} object pointing to an `iframe` it can be converted to {@link FrameLocator} using
|
||||
* [locator.enterFrame()](https://playwright.dev/docs/api/class-locator#locator-enter-frame).
|
||||
* [locator.contentFrame()](https://playwright.dev/docs/api/class-locator#locator-content-frame).
|
||||
*
|
||||
* **Converting FrameLocator to Locator**
|
||||
*
|
||||
* If you have a {@link FrameLocator} object it can be converted to {@link Locator} pointing to the same `iframe`
|
||||
* using [frameLocator.exitFrame()](https://playwright.dev/docs/api/class-framelocator#frame-locator-exit-frame).
|
||||
* using [frameLocator.owner()](https://playwright.dev/docs/api/class-framelocator#frame-locator-owner).
|
||||
*/
|
||||
export interface FrameLocator {
|
||||
/**
|
||||
* Returns a {@link Locator} object pointing to the same `iframe` as this frame locator.
|
||||
*
|
||||
* Useful when you have a {@link FrameLocator} object obtained somewhere, and later on would like to interact with the
|
||||
* `iframe` element.
|
||||
*
|
||||
* **Usage**
|
||||
*
|
||||
* ```js
|
||||
* const frameLocator = page.frameLocator('iframe[name="embedded"]');
|
||||
* // ...
|
||||
* const locator = frameLocator.exitFrame();
|
||||
* await expect(locator).toBeVisible();
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
exitFrame(): Locator;
|
||||
|
||||
/**
|
||||
* Returns locator to the first matching frame.
|
||||
*/
|
||||
|
|
@ -18190,6 +18287,27 @@ export interface FrameLocator {
|
|||
* @param index
|
||||
*/
|
||||
nth(index: number): FrameLocator;
|
||||
|
||||
/**
|
||||
* Returns a {@link Locator} object pointing to the same `iframe` as this frame locator.
|
||||
*
|
||||
* Useful when you have a {@link FrameLocator} object obtained somewhere, and later on would like to interact with the
|
||||
* `iframe` element.
|
||||
*
|
||||
* For a reverse operation, use
|
||||
* [locator.contentFrame()](https://playwright.dev/docs/api/class-locator#locator-content-frame).
|
||||
*
|
||||
* **Usage**
|
||||
*
|
||||
* ```js
|
||||
* const frameLocator = page.frameLocator('iframe[name="embedded"]');
|
||||
* // ...
|
||||
* const locator = frameLocator.owner();
|
||||
* await expect(locator).toBeVisible();
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
owner(): Locator;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ export type ImportInfo = {
|
|||
|
||||
export function importInfo(importNode: T.ImportDeclaration, specifier: T.ImportSpecifier | T.ImportDefaultSpecifier, filename: string): { localName: string, info: ImportInfo } {
|
||||
const importSource = importNode.source.value;
|
||||
const idPrefix = importSource.replace(/[^\w_\d]/g, '_');
|
||||
const idPrefix = path.join(filename, '..', importSource).replace(/[^\w_\d]/g, '_');
|
||||
|
||||
const result: ImportInfo = {
|
||||
id: idPrefix,
|
||||
|
|
@ -193,7 +193,4 @@ const artifactExtensions = new Set([
|
|||
'.ttf',
|
||||
'.otf',
|
||||
'.eot',
|
||||
|
||||
// Other assets
|
||||
'.json',
|
||||
]);
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -46,15 +46,7 @@ export type SerializedConfig = {
|
|||
compilationCache?: SerializedCompilationCache;
|
||||
};
|
||||
|
||||
export type TtyParams = {
|
||||
rows: number | undefined;
|
||||
columns: number | undefined;
|
||||
colorDepth: number;
|
||||
};
|
||||
|
||||
export type ProcessInitParams = {
|
||||
stdoutParams: TtyParams;
|
||||
stderrParams: TtyParams;
|
||||
processName: string;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { WriteStream } from 'tty';
|
||||
import type { EnvProducedPayload, ProcessInitParams, TtyParams } from './ipc';
|
||||
import type { EnvProducedPayload, ProcessInitParams } from './ipc';
|
||||
import { startProfiling, stopProfiling } from 'playwright-core/lib/utils';
|
||||
import type { TestInfoError } from '../../types/test';
|
||||
import { serializeError } from '../util';
|
||||
|
|
@ -67,8 +66,6 @@ const startingEnv = { ...process.env };
|
|||
process.on('message', async (message: any) => {
|
||||
if (message.method === '__init__') {
|
||||
const { processParams, runnerParams, runnerScript } = message.params as { processParams: ProcessInitParams, runnerParams: any, runnerScript: string };
|
||||
setTtyParams(process.stdout, processParams.stdoutParams);
|
||||
setTtyParams(process.stderr, processParams.stderrParams);
|
||||
void startProfiling();
|
||||
const { create } = require(runnerScript);
|
||||
processRunner = create(runnerParams) as ProcessRunner;
|
||||
|
|
@ -117,40 +114,3 @@ function sendMessageToParent(message: { method: string, params?: any }) {
|
|||
// Can throw when closing.
|
||||
}
|
||||
}
|
||||
|
||||
function setTtyParams(stream: WriteStream, params: TtyParams) {
|
||||
stream.isTTY = true;
|
||||
if (params.rows)
|
||||
stream.rows = params.rows;
|
||||
if (params.columns)
|
||||
stream.columns = params.columns;
|
||||
stream.getColorDepth = () => params.colorDepth;
|
||||
stream.hasColors = ((count = 16) => {
|
||||
// count is optional and the first argument may actually be env.
|
||||
if (typeof count !== 'number')
|
||||
count = 16;
|
||||
return count <= 2 ** params.colorDepth;
|
||||
})as any;
|
||||
|
||||
// Stubs for the rest of the methods to avoid exceptions in user code.
|
||||
stream.clearLine = (dir: any, callback?: () => void) => {
|
||||
callback?.();
|
||||
return true;
|
||||
};
|
||||
stream.clearScreenDown = (callback?: () => void) => {
|
||||
callback?.();
|
||||
return true;
|
||||
};
|
||||
(stream as any).cursorTo = (x: number, y?: number | (() => void), callback?: () => void) => {
|
||||
if (callback)
|
||||
callback();
|
||||
else if (y instanceof Function)
|
||||
y();
|
||||
return true;
|
||||
};
|
||||
stream.moveCursor = (dx: number, dy: number, callback?: () => void) => {
|
||||
callback?.();
|
||||
return true;
|
||||
};
|
||||
stream.getWindowSize = () => [stream.columns, stream.rows];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
*/
|
||||
|
||||
import type { TestServerInterface, TestServerInterfaceEvents } from '@testIsomorphic/testServerInterface';
|
||||
import type * as reporterTypes from 'playwright/types/testReporter';
|
||||
import * as events from './events';
|
||||
|
||||
export class TestServerConnection implements TestServerInterface, TestServerInterfaceEvents {
|
||||
|
|
@ -67,7 +66,7 @@ export class TestServerConnection implements TestServerInterface, TestServerInte
|
|||
this._connectedPromise = new Promise<void>((f, r) => {
|
||||
this._ws.addEventListener('open', () => {
|
||||
f();
|
||||
this._ws.send(JSON.stringify({ method: 'ready' }));
|
||||
this._ws.send(JSON.stringify({ id: -1, method: 'ready' }));
|
||||
});
|
||||
this._ws.addEventListener('error', r);
|
||||
});
|
||||
|
|
@ -77,6 +76,10 @@ export class TestServerConnection implements TestServerInterface, TestServerInte
|
|||
});
|
||||
}
|
||||
|
||||
connect() {
|
||||
return this._connectedPromise;
|
||||
}
|
||||
|
||||
private async _sendMessage(method: string, params?: any): Promise<any> {
|
||||
const logForTest = (globalThis as any).__logForTest;
|
||||
logForTest?.({ method, params });
|
||||
|
|
@ -103,81 +106,91 @@ export class TestServerConnection implements TestServerInterface, TestServerInte
|
|||
this._onListChangedEmitter.fire(params);
|
||||
else if (method === 'testFilesChanged')
|
||||
this._onTestFilesChangedEmitter.fire(params);
|
||||
else if (method === 'loadTraceRequested')
|
||||
this._onLoadTraceRequestedEmitter.fire(params);
|
||||
}
|
||||
|
||||
async ping(): Promise<void> {
|
||||
async setSerializer(params: { serializer: string; }): Promise<void> {
|
||||
await this._sendMessage('setSerializer', params);
|
||||
}
|
||||
|
||||
async ping(params: Parameters<TestServerInterface['ping']>[0]): ReturnType<TestServerInterface['ping']> {
|
||||
await this._sendMessage('ping');
|
||||
}
|
||||
|
||||
async pingNoReply() {
|
||||
async pingNoReply(params: Parameters<TestServerInterface['ping']>[0]) {
|
||||
this._sendMessageNoReply('ping');
|
||||
}
|
||||
|
||||
async watch(params: { fileNames: string[]; }): Promise<void> {
|
||||
async watch(params: Parameters<TestServerInterface['watch']>[0]): ReturnType<TestServerInterface['watch']> {
|
||||
await this._sendMessage('watch', params);
|
||||
}
|
||||
|
||||
watchNoReply(params: { fileNames: string[]; }) {
|
||||
watchNoReply(params: Parameters<TestServerInterface['watch']>[0]) {
|
||||
this._sendMessageNoReply('watch', params);
|
||||
}
|
||||
|
||||
async open(params: { location: reporterTypes.Location; }): Promise<void> {
|
||||
async watchTestDir(params: Parameters<TestServerInterface['watchTestDir']>[0]): ReturnType<TestServerInterface['watchTestDir']> {
|
||||
await this._sendMessage('watchTestDir', params);
|
||||
}
|
||||
|
||||
async open(params: Parameters<TestServerInterface['open']>[0]): ReturnType<TestServerInterface['open']> {
|
||||
await this._sendMessage('open', params);
|
||||
}
|
||||
|
||||
openNoReply(params: { location: reporterTypes.Location; }) {
|
||||
openNoReply(params: Parameters<TestServerInterface['open']>[0]) {
|
||||
this._sendMessageNoReply('open', params);
|
||||
}
|
||||
|
||||
async resizeTerminal(params: { cols: number; rows: number; }): Promise<void> {
|
||||
async resizeTerminal(params: Parameters<TestServerInterface['resizeTerminal']>[0]): ReturnType<TestServerInterface['resizeTerminal']> {
|
||||
await this._sendMessage('resizeTerminal', params);
|
||||
}
|
||||
|
||||
resizeTerminalNoReply(params: { cols: number; rows: number; }) {
|
||||
resizeTerminalNoReply(params: Parameters<TestServerInterface['resizeTerminal']>[0]) {
|
||||
this._sendMessageNoReply('resizeTerminal', params);
|
||||
}
|
||||
|
||||
async checkBrowsers(): Promise<{ hasBrowsers: boolean; }> {
|
||||
async checkBrowsers(params: Parameters<TestServerInterface['checkBrowsers']>[0]): ReturnType<TestServerInterface['checkBrowsers']> {
|
||||
return await this._sendMessage('checkBrowsers');
|
||||
}
|
||||
|
||||
async installBrowsers(): Promise<void> {
|
||||
async installBrowsers(params: Parameters<TestServerInterface['installBrowsers']>[0]): ReturnType<TestServerInterface['installBrowsers']> {
|
||||
await this._sendMessage('installBrowsers');
|
||||
}
|
||||
|
||||
async runGlobalSetup(): Promise<'passed' | 'failed' | 'timedout' | 'interrupted'> {
|
||||
async runGlobalSetup(params: Parameters<TestServerInterface['runGlobalSetup']>[0]): ReturnType<TestServerInterface['runGlobalSetup']> {
|
||||
return await this._sendMessage('runGlobalSetup');
|
||||
}
|
||||
|
||||
async runGlobalTeardown(): Promise<'passed' | 'failed' | 'timedout' | 'interrupted'> {
|
||||
async runGlobalTeardown(params: Parameters<TestServerInterface['runGlobalTeardown']>[0]): ReturnType<TestServerInterface['runGlobalTeardown']> {
|
||||
return await this._sendMessage('runGlobalTeardown');
|
||||
}
|
||||
|
||||
async listFiles(): Promise<{ projects: { name: string; testDir: string; use: { testIdAttribute?: string | undefined; }; files: string[]; }[]; cliEntryPoint?: string | undefined; error?: reporterTypes.TestError | undefined; }> {
|
||||
return await this._sendMessage('listFiles');
|
||||
async listFiles(params: Parameters<TestServerInterface['listFiles']>[0]): ReturnType<TestServerInterface['listFiles']> {
|
||||
return await this._sendMessage('listFiles', params);
|
||||
}
|
||||
|
||||
async listTests(params: { reporter?: string | undefined; fileNames?: string[] | undefined; }): Promise<{ report: any[] }> {
|
||||
async listTests(params: Parameters<TestServerInterface['listTests']>[0]): ReturnType<TestServerInterface['listTests']> {
|
||||
return await this._sendMessage('listTests', params);
|
||||
}
|
||||
|
||||
async runTests(params: { reporter?: string | undefined; locations?: string[] | undefined; grep?: string | undefined; testIds?: string[] | undefined; headed?: boolean | undefined; oneWorker?: boolean | undefined; trace?: 'off' | 'on' | undefined; projects?: string[] | undefined; reuseContext?: boolean | undefined; connectWsEndpoint?: string | undefined; }): Promise<{ status: reporterTypes.FullResult['status'] }> {
|
||||
async runTests(params: Parameters<TestServerInterface['runTests']>[0]): ReturnType<TestServerInterface['runTests']> {
|
||||
return await this._sendMessage('runTests', params);
|
||||
}
|
||||
|
||||
async findRelatedTestFiles(params: { files: string[]; }): Promise<{ testFiles: string[]; errors?: reporterTypes.TestError[] | undefined; }> {
|
||||
async findRelatedTestFiles(params: Parameters<TestServerInterface['findRelatedTestFiles']>[0]): ReturnType<TestServerInterface['findRelatedTestFiles']> {
|
||||
return await this._sendMessage('findRelatedTestFiles', params);
|
||||
}
|
||||
|
||||
async stopTests(): Promise<void> {
|
||||
async stopTests(params: Parameters<TestServerInterface['stopTests']>[0]): ReturnType<TestServerInterface['stopTests']> {
|
||||
await this._sendMessage('stopTests');
|
||||
}
|
||||
|
||||
stopTestsNoReply() {
|
||||
stopTestsNoReply(params: Parameters<TestServerInterface['stopTests']>[0]) {
|
||||
this._sendMessageNoReply('stopTests');
|
||||
}
|
||||
|
||||
async closeGracefully(): Promise<void> {
|
||||
async closeGracefully(params: Parameters<TestServerInterface['closeGracefully']>[0]): ReturnType<TestServerInterface['closeGracefully']> {
|
||||
await this._sendMessage('closeGracefully');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,69 +16,84 @@
|
|||
|
||||
import type * as reporterTypes from '../../types/testReporter';
|
||||
import type { Event } from './events';
|
||||
import type { JsonEvent } from './teleReceiver';
|
||||
|
||||
export type ReportEntry = JsonEvent;
|
||||
|
||||
export interface TestServerInterface {
|
||||
ping(): Promise<void>;
|
||||
setSerializer(params: { serializer: string }): Promise<void>;
|
||||
|
||||
ping(params: {}): Promise<void>;
|
||||
|
||||
watch(params: {
|
||||
fileNames: string[];
|
||||
}): Promise<void>;
|
||||
|
||||
watchTestDir(params: {}): Promise<void>;
|
||||
|
||||
open(params: { location: reporterTypes.Location }): Promise<void>;
|
||||
|
||||
resizeTerminal(params: { cols: number, rows: number }): Promise<void>;
|
||||
|
||||
checkBrowsers(): Promise<{ hasBrowsers: boolean }>;
|
||||
checkBrowsers(params: {}): Promise<{ hasBrowsers: boolean }>;
|
||||
|
||||
installBrowsers(): Promise<void>;
|
||||
installBrowsers(params: {}): Promise<void>;
|
||||
|
||||
runGlobalSetup(): Promise<reporterTypes.FullResult['status']>;
|
||||
runGlobalSetup(params: {}): Promise<{
|
||||
report: ReportEntry[],
|
||||
status: reporterTypes.FullResult['status']
|
||||
}>;
|
||||
|
||||
runGlobalTeardown(): Promise<reporterTypes.FullResult['status']>;
|
||||
runGlobalTeardown(params: {}): Promise<{
|
||||
report: ReportEntry[],
|
||||
status: reporterTypes.FullResult['status']
|
||||
}>;
|
||||
|
||||
listFiles(): Promise<{
|
||||
projects: {
|
||||
name: string;
|
||||
testDir: string;
|
||||
use: { testIdAttribute?: string };
|
||||
files: string[];
|
||||
}[];
|
||||
cliEntryPoint?: string;
|
||||
error?: reporterTypes.TestError;
|
||||
listFiles(params: {
|
||||
projects?: string[];
|
||||
}): Promise<{
|
||||
report: ReportEntry[];
|
||||
status: reporterTypes.FullResult['status']
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Returns list of teleReporter events.
|
||||
*/
|
||||
listTests(params: {
|
||||
reporter?: string;
|
||||
fileNames?: string[];
|
||||
}): Promise<{ report: any[] }>;
|
||||
projects?: string[];
|
||||
locations?: string[];
|
||||
}): Promise<{
|
||||
report: ReportEntry[],
|
||||
status: reporterTypes.FullResult['status']
|
||||
}>;
|
||||
|
||||
runTests(params: {
|
||||
reporter?: string;
|
||||
locations?: string[];
|
||||
grep?: string;
|
||||
grepInvert?: string;
|
||||
testIds?: string[];
|
||||
headed?: boolean;
|
||||
oneWorker?: boolean;
|
||||
workers?: number | string;
|
||||
timeout?: number,
|
||||
reporters?: string[],
|
||||
trace?: 'on' | 'off';
|
||||
projects?: string[];
|
||||
reuseContext?: boolean;
|
||||
connectWsEndpoint?: string;
|
||||
}): Promise<{ status: reporterTypes.FullResult['status'] }>;
|
||||
}): Promise<{
|
||||
status: reporterTypes.FullResult['status'];
|
||||
}>;
|
||||
|
||||
findRelatedTestFiles(params: {
|
||||
files: string[];
|
||||
}): Promise<{ testFiles: string[]; errors?: reporterTypes.TestError[]; }>;
|
||||
|
||||
stopTests(): Promise<void>;
|
||||
stopTests(params: {}): Promise<void>;
|
||||
|
||||
closeGracefully(): Promise<void>;
|
||||
closeGracefully(params: {}): Promise<void>;
|
||||
}
|
||||
|
||||
export interface TestServerInterfaceEvents {
|
||||
onClose: Event<void>;
|
||||
onReport: Event<any>;
|
||||
onStdio: Event<{ type: 'stdout' | 'stderr', text?: string, buffer?: string }>;
|
||||
onListChanged: Event<void>;
|
||||
|
|
@ -87,8 +102,7 @@ export interface TestServerInterfaceEvents {
|
|||
}
|
||||
|
||||
export interface TestServerInterfaceEventEmitters {
|
||||
dispatchEvent(event: 'close', params: {}): void;
|
||||
dispatchEvent(event: 'report', params: any): void;
|
||||
dispatchEvent(event: 'report', params: ReportEntry): void;
|
||||
dispatchEvent(event: 'stdio', params: { type: 'stdout' | 'stderr', text?: string, buffer?: string }): void;
|
||||
dispatchEvent(event: 'listChanged', params: {}): void;
|
||||
dispatchEvent(event: 'testFilesChanged', params: { testFiles: string[] }): void;
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export { program } from 'playwright-core/lib/cli/program';
|
|||
import type { ReporterDescription } from '../types/test';
|
||||
import { prepareErrorStack } from './reporters/base';
|
||||
import { cacheDir } from './transform/compilationCache';
|
||||
import * as testServer from './runner/testServer';
|
||||
|
||||
function addTestCommand(program: Command) {
|
||||
const command = program.command('test [test-filter...]');
|
||||
|
|
@ -107,6 +108,7 @@ function addFindRelatedTestFilesCommand(program: Command) {
|
|||
function addTestServerCommand(program: Command) {
|
||||
const command = program.command('test-server', { hidden: true });
|
||||
command.description('start test server');
|
||||
command.option('-c, --config <file>', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
|
||||
command.option('--host <host>', 'Host to start the server on', 'localhost');
|
||||
command.option('--port <port>', 'Port to start the server on', '0');
|
||||
command.action(opts => runTestServer(opts));
|
||||
|
|
@ -151,7 +153,28 @@ Examples:
|
|||
|
||||
async function runTests(args: string[], opts: { [key: string]: any }) {
|
||||
await startProfiling();
|
||||
const config = await loadConfigFromFileRestartIfNeeded(opts.config, overridesFromOptions(opts), opts.deps === false);
|
||||
const cliOverrides = overridesFromOptions(opts);
|
||||
|
||||
if (opts.ui || opts.uiHost || opts.uiPort) {
|
||||
const status = await testServer.runUIMode(opts.config, {
|
||||
host: opts.uiHost,
|
||||
port: opts.uiPort ? +opts.uiPort : undefined,
|
||||
args,
|
||||
grep: opts.grep as string | undefined,
|
||||
grepInvert: opts.grepInvert as string | undefined,
|
||||
project: opts.project || undefined,
|
||||
headed: opts.headed,
|
||||
reporter: Array.isArray(opts.reporter) ? opts.reporter : opts.reporter ? [opts.reporter] : undefined,
|
||||
workers: cliOverrides.workers,
|
||||
timeout: cliOverrides.timeout,
|
||||
});
|
||||
await stopProfiling('runner');
|
||||
const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1);
|
||||
gracefullyProcessExitDoNotHang(exitCode);
|
||||
return;
|
||||
}
|
||||
|
||||
const config = await loadConfigFromFileRestartIfNeeded(opts.config, cliOverrides, opts.deps === false);
|
||||
if (!config)
|
||||
return;
|
||||
|
||||
|
|
@ -164,9 +187,7 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
|
|||
|
||||
const runner = new Runner(config);
|
||||
let status: FullResult['status'];
|
||||
if (opts.ui || opts.uiHost || opts.uiPort)
|
||||
status = await runner.runUIMode({ host: opts.uiHost, port: opts.uiPort ? +opts.uiPort : undefined });
|
||||
else if (process.env.PWTEST_WATCH)
|
||||
if (process.env.PWTEST_WATCH)
|
||||
status = await runner.watchAllTests();
|
||||
else
|
||||
status = await runner.runAllTests();
|
||||
|
|
@ -176,14 +197,9 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
|
|||
}
|
||||
|
||||
async function runTestServer(opts: { [key: string]: any }) {
|
||||
const config = await loadConfigFromFileRestartIfNeeded(opts.config, overridesFromOptions(opts), opts.deps === false);
|
||||
if (!config)
|
||||
return;
|
||||
config.cliPassWithNoTests = true;
|
||||
const runner = new Runner(config);
|
||||
const host = opts.host || 'localhost';
|
||||
const port = opts.port ? +opts.port : 0;
|
||||
const status = await runner.runTestServer({ host, port });
|
||||
const status = await testServer.runTestServer(opts.config, { host, port });
|
||||
const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1);
|
||||
gracefullyProcessExitDoNotHang(exitCode);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,8 @@ type HtmlReporterOptions = {
|
|||
host?: string,
|
||||
port?: number,
|
||||
attachmentsBaseURL?: string,
|
||||
_mode?: string;
|
||||
_mode?: 'test' | 'list';
|
||||
_isTestServer?: boolean;
|
||||
};
|
||||
|
||||
class HtmlReporter extends EmptyReporter {
|
||||
|
|
@ -67,6 +68,8 @@ class HtmlReporter extends EmptyReporter {
|
|||
constructor(options: HtmlReporterOptions) {
|
||||
super();
|
||||
this._options = options;
|
||||
if (options._mode === 'test')
|
||||
process.env.PW_HTML_REPORT = '1';
|
||||
}
|
||||
|
||||
override printsToStdio() {
|
||||
|
|
@ -125,7 +128,7 @@ class HtmlReporter extends EmptyReporter {
|
|||
if (process.env.CI || !this._buildResult)
|
||||
return;
|
||||
const { ok, singleTestId } = this._buildResult;
|
||||
const shouldOpen = this._open === 'always' || (!ok && this._open === 'on-failure');
|
||||
const shouldOpen = !this._options._isTestServer && (this._open === 'always' || (!ok && this._open === 'on-failure'));
|
||||
if (shouldOpen) {
|
||||
await showHTMLReport(this._outputFolder, this._options.host, this._options.port, singleTestId);
|
||||
} else if (this._options._mode === 'test') {
|
||||
|
|
@ -213,6 +216,8 @@ class HtmlBuilder {
|
|||
private _dataZipFile: ZipFile;
|
||||
private _hasTraces = false;
|
||||
private _attachmentsBaseURL: string;
|
||||
private _projectToId: Map<Suite, number> = new Map();
|
||||
private _lastProjectId = 0;
|
||||
|
||||
constructor(config: FullConfig, outputDir: string, attachmentsBaseURL: string) {
|
||||
this._config = config;
|
||||
|
|
@ -353,7 +358,7 @@ class HtmlBuilder {
|
|||
path = path.slice(1);
|
||||
|
||||
const [file, ...titles] = test.titlePath();
|
||||
const testIdExpression = `[project=${projectName}]${toPosixPath(file)}\x1e${titles.join('\x1e')} (repeat:${test.repeatEachIndex})`;
|
||||
const testIdExpression = `[project=${this._projectId(test.parent)}]${toPosixPath(file)}\x1e${titles.join('\x1e')} (repeat:${test.repeatEachIndex})`;
|
||||
const testId = fileId + '-' + calculateSha1(testIdExpression).slice(0, 20);
|
||||
|
||||
const results = test.results.map(r => this._createTestResult(test, r));
|
||||
|
|
@ -392,6 +397,16 @@ class HtmlBuilder {
|
|||
};
|
||||
}
|
||||
|
||||
private _projectId(suite: Suite): number {
|
||||
const project = projectSuite(suite);
|
||||
let id = this._projectToId.get(project);
|
||||
if (!id) {
|
||||
id = ++this._lastProjectId;
|
||||
this._projectToId.set(project, id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
private _serializeAttachments(attachments: JsonAttachment[]) {
|
||||
let lastAttachment: TestAttachment | undefined;
|
||||
return attachments.map(a => {
|
||||
|
|
@ -629,4 +644,10 @@ function createSnippets(stepsInFile: MultiMap<string, TestStep>) {
|
|||
}
|
||||
}
|
||||
|
||||
function projectSuite(suite: Suite): Suite {
|
||||
while (suite.parent?.parent)
|
||||
suite = suite.parent;
|
||||
return suite;
|
||||
}
|
||||
|
||||
export default HtmlReporter;
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ type ReportData = {
|
|||
};
|
||||
|
||||
export async function createMergedReport(config: FullConfigInternal, dir: string, reporterDescriptions: ReporterDescription[], rootDirOverride: string | undefined) {
|
||||
const reporters = await createReporters(config, 'merge', reporterDescriptions);
|
||||
const reporters = await createReporters(config, 'merge', false, reporterDescriptions);
|
||||
const multiplexer = new Multiplexer(reporters);
|
||||
const stringPool = new StringInternPool();
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import { dependenciesForTestFile } from '../transform/compilationCache';
|
|||
import { sourceMapSupport } from '../utilsBundle';
|
||||
import type { RawSourceMap } from 'source-map';
|
||||
|
||||
export async function collectProjectsAndTestFiles(testRun: TestRun, doNotRunTestsOutsideProjectFilter: boolean, additionalFileMatcher: Matcher | undefined) {
|
||||
export async function collectProjectsAndTestFiles(testRun: TestRun, doNotRunTestsOutsideProjectFilter: boolean, additionalFileMatcher?: Matcher) {
|
||||
const config = testRun.config;
|
||||
const fsCache = new Map();
|
||||
const sourceMapCache = new Map();
|
||||
|
|
@ -60,8 +60,7 @@ export async function collectProjectsAndTestFiles(testRun: TestRun, doNotRunTest
|
|||
return hasMatchingSources;
|
||||
});
|
||||
const filteredFiles = matchedFiles.filter(Boolean) as string[];
|
||||
if (filteredFiles.length)
|
||||
filesToRunByProject.set(project, filteredFiles);
|
||||
filesToRunByProject.set(project, filteredFiles);
|
||||
}
|
||||
|
||||
// (Re-)add all files for dependent projects, disregard filters.
|
||||
|
|
@ -316,7 +315,7 @@ export function loadGlobalHook(config: FullConfigInternal, file: string): Promis
|
|||
return requireOrImportDefaultFunction(path.resolve(config.config.rootDir, file), false);
|
||||
}
|
||||
|
||||
export function loadReporter(config: FullConfigInternal, file: string): Promise<new (arg?: any) => Reporter> {
|
||||
export function loadReporter(config: FullConfigInternal | null, file: string): Promise<new (arg?: any) => Reporter> {
|
||||
return requireOrImportDefaultFunction(config ? path.resolve(config.config.rootDir, file) : file, true);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -112,16 +112,6 @@ export class ProcessHost extends EventEmitter {
|
|||
return error;
|
||||
|
||||
const processParams: ProcessInitParams = {
|
||||
stdoutParams: {
|
||||
rows: process.stdout.rows,
|
||||
columns: process.stdout.columns,
|
||||
colorDepth: process.stdout.getColorDepth?.() || 8
|
||||
},
|
||||
stderrParams: {
|
||||
rows: process.stderr.rows,
|
||||
columns: process.stderr.columns,
|
||||
colorDepth: process.stderr.getColorDepth?.() || 8
|
||||
},
|
||||
processName: this._processName
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import { BlobReporter } from '../reporters/blob';
|
|||
import type { ReporterDescription } from '../../types/test';
|
||||
import { type ReporterV2, wrapReporterAsV2 } from '../reporters/reporterV2';
|
||||
|
||||
export async function createReporters(config: FullConfigInternal, mode: 'list' | 'test' | 'ui' | 'merge', descriptions?: ReporterDescription[]): Promise<ReporterV2[]> {
|
||||
export async function createReporters(config: FullConfigInternal, mode: 'list' | 'test' | 'merge', isTestServer: boolean, descriptions?: ReporterDescription[]): Promise<ReporterV2[]> {
|
||||
const defaultReporters: { [key in BuiltInReporter]: new(arg: any) => ReporterV2 } = {
|
||||
blob: BlobReporter,
|
||||
dot: mode === 'list' ? ListModeReporter : DotReporter,
|
||||
|
|
@ -43,14 +43,14 @@ export async function createReporters(config: FullConfigInternal, mode: 'list' |
|
|||
json: JSONReporter,
|
||||
junit: JUnitReporter,
|
||||
null: EmptyReporter,
|
||||
html: mode === 'ui' ? LineReporter : HtmlReporter,
|
||||
html: HtmlReporter,
|
||||
markdown: MarkdownReporter,
|
||||
};
|
||||
const reporters: ReporterV2[] = [];
|
||||
descriptions ??= config.config.reporter;
|
||||
if (config.configCLIOverrides.additionalReporters)
|
||||
descriptions = [...descriptions, ...config.configCLIOverrides.additionalReporters];
|
||||
const runOptions = reporterOptions(config, mode);
|
||||
const runOptions = reporterOptions(config, mode, isTestServer);
|
||||
for (const r of descriptions) {
|
||||
const [name, arg] = r;
|
||||
const options = { ...runOptions, ...arg };
|
||||
|
|
@ -78,17 +78,18 @@ export async function createReporters(config: FullConfigInternal, mode: 'list' |
|
|||
return reporters;
|
||||
}
|
||||
|
||||
export async function createReporterForTestServer(config: FullConfigInternal, file: string, mode: 'test' | 'list', messageSink: (message: any) => void): Promise<ReporterV2> {
|
||||
const reporterConstructor = await loadReporter(config, file);
|
||||
const runOptions = reporterOptions(config, mode, messageSink);
|
||||
const instance = new reporterConstructor(runOptions);
|
||||
return wrapReporterAsV2(instance);
|
||||
export async function createReporterForTestServer(file: string, messageSink: (message: any) => void): Promise<ReporterV2> {
|
||||
const reporterConstructor = await loadReporter(null, file);
|
||||
return wrapReporterAsV2(new reporterConstructor({
|
||||
_send: messageSink,
|
||||
}));
|
||||
}
|
||||
|
||||
function reporterOptions(config: FullConfigInternal, mode: 'list' | 'test' | 'ui' | 'merge', send?: (message: any) => void) {
|
||||
function reporterOptions(config: FullConfigInternal, mode: 'list' | 'test' | 'merge', isTestServer: boolean) {
|
||||
return {
|
||||
configDir: config.configDir,
|
||||
_send: send,
|
||||
_mode: mode,
|
||||
_isTestServer: isTestServer,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@
|
|||
*/
|
||||
|
||||
import path from 'path';
|
||||
import type { HttpServer, ManualPromise } from 'playwright-core/lib/utils';
|
||||
import { isUnderTest, monotonicTime } from 'playwright-core/lib/utils';
|
||||
import { monotonicTime } from 'playwright-core/lib/utils';
|
||||
import type { FullResult, TestError } from '../../types/testReporter';
|
||||
import { webServerPluginsForConfig } from '../plugins/webServerPlugin';
|
||||
import { collectFilesForProject, filterProjects } from './projectUtils';
|
||||
|
|
@ -26,13 +25,11 @@ import { TestRun, createTaskRunner, createTaskRunnerForList } from './tasks';
|
|||
import type { FullConfigInternal } from '../common/config';
|
||||
import { colors } from 'playwright-core/lib/utilsBundle';
|
||||
import { runWatchModeLoop } from './watchMode';
|
||||
import { runTestServer } from './testServer';
|
||||
import { InternalReporter } from '../reporters/internalReporter';
|
||||
import { Multiplexer } from '../reporters/multiplexer';
|
||||
import type { Suite } from '../common/test';
|
||||
import { wrapReporterAsV2 } from '../reporters/reporterV2';
|
||||
import { affectedTestFiles } from '../transform/compilationCache';
|
||||
import { installRootRedirect, openTraceInBrowser, openTraceViewerApp } from 'playwright-core/lib/server';
|
||||
|
||||
type ProjectConfigWithFiles = {
|
||||
name: string;
|
||||
|
|
@ -43,7 +40,6 @@ type ProjectConfigWithFiles = {
|
|||
|
||||
type ConfigListFilesReport = {
|
||||
projects: ProjectConfigWithFiles[];
|
||||
cliEntryPoint?: string;
|
||||
error?: TestError;
|
||||
};
|
||||
|
||||
|
|
@ -59,12 +55,10 @@ export class Runner {
|
|||
this._config = config;
|
||||
}
|
||||
|
||||
async listTestFiles(): Promise<ConfigListFilesReport> {
|
||||
const frameworkPackage = (this._config.config as any)['@playwright/test']?.['packageJSON'];
|
||||
const projects = filterProjects(this._config.projects);
|
||||
async listTestFiles(projectNames?: string[]): Promise<ConfigListFilesReport> {
|
||||
const projects = filterProjects(this._config.projects, projectNames);
|
||||
const report: ConfigListFilesReport = {
|
||||
projects: [],
|
||||
cliEntryPoint: frameworkPackage ? path.join(path.dirname(frameworkPackage), 'cli.js') : undefined,
|
||||
};
|
||||
for (const project of projects) {
|
||||
report.projects.push({
|
||||
|
|
@ -85,7 +79,7 @@ export class Runner {
|
|||
// Legacy webServer support.
|
||||
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
|
||||
|
||||
const reporter = new InternalReporter(new Multiplexer(await createReporters(config, listOnly ? 'list' : 'test')));
|
||||
const reporter = new InternalReporter(new Multiplexer(await createReporters(config, listOnly ? 'list' : 'test', false)));
|
||||
const taskRunner = listOnly ? createTaskRunnerForList(config, reporter, 'in-process', { failOnLoadErrors: true })
|
||||
: createTaskRunner(config, reporter);
|
||||
|
||||
|
|
@ -148,34 +142,6 @@ export class Runner {
|
|||
return await runWatchModeLoop(config);
|
||||
}
|
||||
|
||||
async runUIMode(options: { host?: string, port?: number }): Promise<FullResult['status']> {
|
||||
const config = this._config;
|
||||
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
|
||||
return await runTestServer(config, options, async (server: HttpServer, cancelPromise: ManualPromise<void>) => {
|
||||
await installRootRedirect(server, [], { webApp: 'uiMode.html' });
|
||||
if (options.host !== undefined || options.port !== undefined) {
|
||||
await openTraceInBrowser(server.urlPrefix());
|
||||
} else {
|
||||
const page = await openTraceViewerApp(server.urlPrefix(), 'chromium', {
|
||||
headless: isUnderTest() && process.env.PWTEST_HEADED_FOR_TEST !== '1',
|
||||
persistentContextOptions: {
|
||||
handleSIGINT: false,
|
||||
},
|
||||
});
|
||||
page.on('close', () => cancelPromise.resolve());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async runTestServer(options: { host?: string, port?: number }): Promise<FullResult['status']> {
|
||||
const config = this._config;
|
||||
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
|
||||
return await runTestServer(config, options, async server => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Listening on ' + server.urlPrefix().replace('http:', 'ws:') + '/' + server.wsGuid());
|
||||
});
|
||||
}
|
||||
|
||||
async findRelatedTestFiles(mode: 'in-process' | 'out-of-process', files: string[]): Promise<FindRelatedTestFilesReport> {
|
||||
const result = await this.loadAllTests(mode);
|
||||
if (result.status !== 'passed' || !result.suite)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { TaskRunner } from './taskRunner';
|
|||
import type { FullConfigInternal, FullProjectInternal } from '../common/config';
|
||||
import { collectProjectsAndTestFiles, createRootSuite, loadFileSuites, loadGlobalHook } from './loadUtils';
|
||||
import type { Matcher } from '../util';
|
||||
import type { Suite } from '../common/test';
|
||||
import { Suite } from '../common/test';
|
||||
import { buildDependentProjects, buildTeardownToSetupsMap, filterProjects } from './projectUtils';
|
||||
import { FailureTracker } from './failureTracker';
|
||||
|
||||
|
|
@ -116,6 +116,13 @@ export function createTaskRunnerForList(config: FullConfigInternal, reporter: Re
|
|||
return taskRunner;
|
||||
}
|
||||
|
||||
export function createTaskRunnerForListFiles(config: FullConfigInternal, reporter: ReporterV2): TaskRunner<TestRun> {
|
||||
const taskRunner = new TaskRunner<TestRun>(reporter, config.config.globalTimeout);
|
||||
taskRunner.addTask('load tests', createListFilesTask());
|
||||
taskRunner.addTask('report begin', createReportBeginTask());
|
||||
return taskRunner;
|
||||
}
|
||||
|
||||
function createReportBeginTask(): Task<TestRun> {
|
||||
return {
|
||||
setup: async ({ reporter, rootSuite }) => {
|
||||
|
|
@ -195,6 +202,29 @@ function createRemoveOutputDirsTask(): Task<TestRun> {
|
|||
};
|
||||
}
|
||||
|
||||
function createListFilesTask(): Task<TestRun> {
|
||||
return {
|
||||
setup: async (testRun, errors) => {
|
||||
testRun.rootSuite = await createRootSuite(testRun, errors, false);
|
||||
testRun.failureTracker.onRootSuite(testRun.rootSuite);
|
||||
await collectProjectsAndTestFiles(testRun, false);
|
||||
for (const [project, files] of testRun.projectFiles) {
|
||||
const projectSuite = new Suite(project.project.name, 'project');
|
||||
projectSuite._fullProject = project;
|
||||
testRun.rootSuite._addSuite(projectSuite);
|
||||
const suites = files.map(file => {
|
||||
const title = path.relative(testRun.config.config.rootDir, file);
|
||||
const suite = new Suite(title, 'file');
|
||||
suite.location = { file, line: 0, column: 0 };
|
||||
projectSuite._addSuite(suite);
|
||||
return suite;
|
||||
});
|
||||
testRun.projectSuites.set(project, suites);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, failOnLoadErrors: boolean, doNotRunDepsOutsideProjectFilter?: boolean, additionalFileMatcher?: Matcher }): Task<TestRun> {
|
||||
return {
|
||||
setup: async (testRun, errors, softErrors) => {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { registry, startTraceViewerServer } from 'playwright-core/lib/server';
|
||||
import { installRootRedirect, openTraceInBrowser, openTraceViewerApp, registry, startTraceViewerServer } from 'playwright-core/lib/server';
|
||||
import { ManualPromise, gracefullyProcessExitDoNotHang, isUnderTest } from 'playwright-core/lib/utils';
|
||||
import type { Transport, HttpServer } from 'playwright-core/lib/utils';
|
||||
import type * as reporterTypes from '../../types/testReporter';
|
||||
|
|
@ -24,43 +24,35 @@ import { collectAffectedTestFiles, dependenciesForTestFile } from '../transform/
|
|||
import type { FullConfigInternal } from '../common/config';
|
||||
import { InternalReporter } from '../reporters/internalReporter';
|
||||
import { createReporterForTestServer, createReporters } from './reporters';
|
||||
import { TestRun, createTaskRunnerForList, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
|
||||
import { TestRun, createTaskRunnerForList, createTaskRunnerForTestServer, createTaskRunnerForWatchSetup, createTaskRunnerForListFiles } from './tasks';
|
||||
import { open } from 'playwright-core/lib/utilsBundle';
|
||||
import ListReporter from '../reporters/list';
|
||||
import { Multiplexer } from '../reporters/multiplexer';
|
||||
import { SigIntWatcher } from './sigIntWatcher';
|
||||
import { Watcher } from '../fsWatcher';
|
||||
import type { TestServerInterface, TestServerInterfaceEventEmitters } from '../isomorphic/testServerInterface';
|
||||
import type { ReportEntry, TestServerInterface, TestServerInterfaceEventEmitters } from '../isomorphic/testServerInterface';
|
||||
import { Runner } from './runner';
|
||||
import type { ConfigCLIOverrides } from '../common/ipc';
|
||||
import { loadConfig, resolveConfigFile, restartWithExperimentalTsEsm } from '../common/configLoader';
|
||||
import { webServerPluginsForConfig } from '../plugins/webServerPlugin';
|
||||
import type { TraceViewerRedirectOptions, TraceViewerServerOptions } from 'playwright-core/lib/server/trace/viewer/traceViewer';
|
||||
import type { TestRunnerPluginRegistration } from '../plugins';
|
||||
import { serializeError } from '../util';
|
||||
import { prepareErrorStack } from '../reporters/base';
|
||||
|
||||
class TestServer {
|
||||
private _config: FullConfigInternal;
|
||||
private _configFile: string | undefined;
|
||||
private _dispatcher: TestServerDispatcher | undefined;
|
||||
private _originalStdoutWrite: NodeJS.WriteStream['write'];
|
||||
private _originalStderrWrite: NodeJS.WriteStream['write'];
|
||||
|
||||
constructor(config: FullConfigInternal) {
|
||||
this._config = config;
|
||||
process.env.PW_LIVE_TRACE_STACKS = '1';
|
||||
config.cliListOnly = false;
|
||||
config.cliPassWithNoTests = true;
|
||||
config.config.preserveOutput = 'always';
|
||||
|
||||
for (const p of config.projects) {
|
||||
p.project.retries = 0;
|
||||
p.project.repeatEach = 1;
|
||||
}
|
||||
config.configCLIOverrides.use = config.configCLIOverrides.use || {};
|
||||
config.configCLIOverrides.use.trace = { mode: 'on', sources: false, _live: true };
|
||||
|
||||
constructor(configFile: string | undefined) {
|
||||
this._configFile = configFile;
|
||||
this._originalStdoutWrite = process.stdout.write;
|
||||
this._originalStderrWrite = process.stderr.write;
|
||||
}
|
||||
|
||||
async start(options: { host?: string, port?: number }): Promise<HttpServer> {
|
||||
this._dispatcher = new TestServerDispatcher(this._config);
|
||||
this._dispatcher = new TestServerDispatcher(this._configFile);
|
||||
return await startTraceViewerServer({ ...options, transport: this._dispatcher.transport });
|
||||
}
|
||||
|
||||
|
|
@ -90,20 +82,23 @@ class TestServer {
|
|||
}
|
||||
|
||||
class TestServerDispatcher implements TestServerInterface {
|
||||
private _config: FullConfigInternal;
|
||||
private _configFile: string | undefined;
|
||||
private _globalWatcher: Watcher;
|
||||
private _testWatcher: Watcher;
|
||||
private _testRun: { run: Promise<reporterTypes.FullResult['status']>, stop: ManualPromise<void> } | undefined;
|
||||
readonly transport: Transport;
|
||||
private _queue = Promise.resolve();
|
||||
private _globalCleanup: (() => Promise<reporterTypes.FullResult['status']>) | undefined;
|
||||
private _globalSetup: { cleanup: () => Promise<any>, report: ReportEntry[] } | undefined;
|
||||
readonly _dispatchEvent: TestServerInterfaceEventEmitters['dispatchEvent'];
|
||||
private _plugins: TestRunnerPluginRegistration[] | undefined;
|
||||
private _serializer = require.resolve('./uiModeReporter');
|
||||
private _watchTestDir = false;
|
||||
|
||||
constructor(config: FullConfigInternal) {
|
||||
this._config = config;
|
||||
constructor(configFile: string | undefined) {
|
||||
this._configFile = configFile;
|
||||
this.transport = {
|
||||
dispatch: (method, params) => (this as any)[method](params),
|
||||
onclose: () => {},
|
||||
onclose: () => gracefullyProcessExitDoNotHang(0),
|
||||
};
|
||||
this._globalWatcher = new Watcher('deep', () => this._dispatchEvent('listChanged', {}));
|
||||
this._testWatcher = new Watcher('flat', events => {
|
||||
|
|
@ -114,16 +109,33 @@ class TestServerDispatcher implements TestServerInterface {
|
|||
this._dispatchEvent = (method, params) => this.transport.sendEvent?.(method, params);
|
||||
}
|
||||
|
||||
async setSerializer(params: { serializer: string; }): Promise<void> {
|
||||
this._serializer = params.serializer;
|
||||
}
|
||||
|
||||
private async _wireReporter(messageSink: (message: any) => void) {
|
||||
return await createReporterForTestServer(this._serializer, messageSink);
|
||||
}
|
||||
|
||||
private async _collectingReporter() {
|
||||
const report: ReportEntry[] = [];
|
||||
const wireReporter = await createReporterForTestServer(this._serializer, e => report.push(e));
|
||||
const reporter = new InternalReporter(wireReporter);
|
||||
return { reporter, report };
|
||||
}
|
||||
|
||||
async ready() {}
|
||||
|
||||
async ping() {}
|
||||
|
||||
async open(params: { location: reporterTypes.Location }) {
|
||||
async open(params: Parameters<TestServerInterface['open']>[0]): ReturnType<TestServerInterface['open']> {
|
||||
if (isUnderTest())
|
||||
return;
|
||||
// eslint-disable-next-line no-console
|
||||
open('vscode://file/' + params.location.file + ':' + params.location.line).catch(e => console.error(e));
|
||||
}
|
||||
|
||||
async resizeTerminal(params: { cols: number; rows: number; }) {
|
||||
async resizeTerminal(params: Parameters<TestServerInterface['resizeTerminal']>[0]): ReturnType<TestServerInterface['resizeTerminal']> {
|
||||
process.stdout.columns = params.cols;
|
||||
process.stdout.rows = params.rows;
|
||||
process.stderr.columns = params.cols;
|
||||
|
|
@ -138,117 +150,173 @@ class TestServerDispatcher implements TestServerInterface {
|
|||
await installBrowsers();
|
||||
}
|
||||
|
||||
async runGlobalSetup(): Promise<reporterTypes.FullResult['status']> {
|
||||
async runGlobalSetup(params: Parameters<TestServerInterface['runGlobalSetup']>[0]): ReturnType<TestServerInterface['runGlobalSetup']> {
|
||||
await this.runGlobalTeardown();
|
||||
|
||||
const reporter = new InternalReporter(new ListReporter());
|
||||
const taskRunner = createTaskRunnerForWatchSetup(this._config, reporter);
|
||||
reporter.onConfigure(this._config.config);
|
||||
const testRun = new TestRun(this._config, reporter);
|
||||
const { reporter, report } = await this._collectingReporter();
|
||||
const { config, error } = await this._loadConfig(this._configFile);
|
||||
if (!config) {
|
||||
reporter.onError(error!);
|
||||
return { status: 'failed', report };
|
||||
}
|
||||
|
||||
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
|
||||
const listReporter = new InternalReporter(new ListReporter());
|
||||
const taskRunner = createTaskRunnerForWatchSetup(config, new Multiplexer([reporter, listReporter]));
|
||||
reporter.onConfigure(config.config);
|
||||
const testRun = new TestRun(config, reporter);
|
||||
const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(testRun, 0);
|
||||
await reporter.onEnd({ status });
|
||||
await reporter.onExit();
|
||||
if (status !== 'passed') {
|
||||
await globalCleanup();
|
||||
return status;
|
||||
return { report, status };
|
||||
}
|
||||
this._globalCleanup = globalCleanup;
|
||||
return status;
|
||||
this._globalSetup = { cleanup: globalCleanup, report };
|
||||
return { report, status };
|
||||
}
|
||||
|
||||
async runGlobalTeardown() {
|
||||
const result = (await this._globalCleanup?.()) || 'passed';
|
||||
this._globalCleanup = undefined;
|
||||
return result;
|
||||
const globalSetup = this._globalSetup;
|
||||
const status = await globalSetup?.cleanup();
|
||||
this._globalSetup = undefined;
|
||||
return { status, report: globalSetup?.report || [] };
|
||||
}
|
||||
|
||||
async listFiles() {
|
||||
try {
|
||||
const runner = new Runner(this._config);
|
||||
return runner.listTestFiles();
|
||||
} catch (e) {
|
||||
const error: reporterTypes.TestError = serializeError(e);
|
||||
error.location = prepareErrorStack(e.stack).location;
|
||||
return { projects: [], error };
|
||||
async listFiles(params: Parameters<TestServerInterface['listFiles']>[0]): ReturnType<TestServerInterface['listFiles']> {
|
||||
const { reporter, report } = await this._collectingReporter();
|
||||
const { config, error } = await this._loadConfig(this._configFile);
|
||||
if (!config) {
|
||||
reporter.onError(error!);
|
||||
return { status: 'failed', report };
|
||||
}
|
||||
|
||||
config.cliProjectFilter = params.projects?.length ? params.projects : undefined;
|
||||
const taskRunner = createTaskRunnerForListFiles(config, reporter);
|
||||
reporter.onConfigure(config.config);
|
||||
const testRun = new TestRun(config, reporter);
|
||||
const status = await taskRunner.run(testRun, 0);
|
||||
await reporter.onEnd({ status });
|
||||
await reporter.onExit();
|
||||
return { report, status };
|
||||
}
|
||||
|
||||
async listTests(params: { reporter?: string; fileNames: string[]; }) {
|
||||
let report: any[] = [];
|
||||
async listTests(params: Parameters<TestServerInterface['listTests']>[0]): ReturnType<TestServerInterface['listTests']> {
|
||||
let result: Awaited<ReturnType<TestServerInterface['listTests']>>;
|
||||
this._queue = this._queue.then(async () => {
|
||||
report = await this._innerListTests(params);
|
||||
result = await this._innerListTests(params);
|
||||
}).catch(printInternalError);
|
||||
await this._queue;
|
||||
return { report };
|
||||
return result!;
|
||||
}
|
||||
|
||||
private async _innerListTests(params: { reporter?: string; fileNames?: string[]; }) {
|
||||
const report: any[] = [];
|
||||
const wireReporter = await createReporterForTestServer(this._config, params.reporter || require.resolve('./uiModeReporter'), 'list', e => report.push(e));
|
||||
const reporter = new InternalReporter(wireReporter);
|
||||
this._config.cliArgs = params.fileNames || [];
|
||||
this._config.cliListOnly = true;
|
||||
this._config.testIdMatcher = undefined;
|
||||
const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process', { failOnLoadErrors: false });
|
||||
const testRun = new TestRun(this._config, reporter);
|
||||
reporter.onConfigure(this._config.config);
|
||||
private async _innerListTests(params: Parameters<TestServerInterface['listTests']>[0]): ReturnType<TestServerInterface['listTests']> {
|
||||
const overrides: ConfigCLIOverrides = {
|
||||
repeatEach: 1,
|
||||
retries: 0,
|
||||
};
|
||||
const { reporter, report } = await this._collectingReporter();
|
||||
const { config, error } = await this._loadConfig(this._configFile, overrides);
|
||||
if (!config) {
|
||||
reporter.onError(error!);
|
||||
return { report: [], status: 'failed' };
|
||||
}
|
||||
|
||||
config.cliArgs = params.locations || [];
|
||||
config.cliProjectFilter = params.projects?.length ? params.projects : undefined;
|
||||
config.cliListOnly = true;
|
||||
|
||||
const taskRunner = createTaskRunnerForList(config, reporter, 'out-of-process', { failOnLoadErrors: false });
|
||||
const testRun = new TestRun(config, reporter);
|
||||
reporter.onConfigure(config.config);
|
||||
const status = await taskRunner.run(testRun, 0);
|
||||
await reporter.onEnd({ status });
|
||||
await reporter.onExit();
|
||||
|
||||
const projectDirs = new Set<string>();
|
||||
const projectOutputs = new Set<string>();
|
||||
for (const p of this._config.projects) {
|
||||
for (const p of config.projects) {
|
||||
projectDirs.add(p.project.testDir);
|
||||
projectOutputs.add(p.project.outputDir);
|
||||
}
|
||||
|
||||
const result = await resolveCtDirs(this._config);
|
||||
const result = await resolveCtDirs(config);
|
||||
if (result) {
|
||||
projectDirs.add(result.templateDir);
|
||||
projectOutputs.add(result.outDir);
|
||||
}
|
||||
|
||||
this._globalWatcher.update([...projectDirs], [...projectOutputs], false);
|
||||
return report;
|
||||
if (this._watchTestDir)
|
||||
this._globalWatcher.update([...projectDirs], [...projectOutputs], false);
|
||||
return { report, status };
|
||||
}
|
||||
|
||||
async runTests(params: { reporter?: string; locations?: string[] | undefined; grep?: string | undefined; testIds?: string[] | undefined; headed?: boolean | undefined; oneWorker?: boolean | undefined; trace?: 'off' | 'on' | undefined; projects?: string[] | undefined; reuseContext?: boolean | undefined; connectWsEndpoint?: string | undefined; }) {
|
||||
let status: reporterTypes.FullResult['status'];
|
||||
async runTests(params: Parameters<TestServerInterface['runTests']>[0]): ReturnType<TestServerInterface['runTests']> {
|
||||
let result: Awaited<ReturnType<TestServerInterface['runTests']>> = { status: 'passed' };
|
||||
this._queue = this._queue.then(async () => {
|
||||
status = await this._innerRunTests(params).catch(printInternalError) || 'failed';
|
||||
result = await this._innerRunTests(params).catch(e => { printInternalError(e); return { status: 'failed' }; });
|
||||
});
|
||||
await this._queue;
|
||||
return { status: status! };
|
||||
return result;
|
||||
}
|
||||
|
||||
private async _innerRunTests(params: { reporter?: string; locations?: string[] | undefined; grep?: string | undefined; testIds?: string[] | undefined; headed?: boolean | undefined; oneWorker?: boolean | undefined; trace?: 'off' | 'on' | undefined; projects?: string[] | undefined; reuseContext?: boolean | undefined; connectWsEndpoint?: string | undefined; }): Promise<reporterTypes.FullResult['status']> {
|
||||
private async _innerRunTests(params: Parameters<TestServerInterface['runTests']>[0]): ReturnType<TestServerInterface['runTests']> {
|
||||
await this.stopTests();
|
||||
const { testIds, projects, locations, grep } = params;
|
||||
const overrides: ConfigCLIOverrides = {
|
||||
repeatEach: 1,
|
||||
retries: 0,
|
||||
preserveOutputDir: true,
|
||||
timeout: params.timeout,
|
||||
reporter: params.reporters ? params.reporters.map(r => [r]) : undefined,
|
||||
use: {
|
||||
trace: params.trace === 'on' ? { mode: 'on', sources: false, _live: true } : (params.trace === 'off' ? 'off' : undefined),
|
||||
headless: params.headed ? false : undefined,
|
||||
_optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined,
|
||||
_optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined,
|
||||
},
|
||||
workers: params.workers,
|
||||
};
|
||||
if (params.trace === 'on')
|
||||
process.env.PW_LIVE_TRACE_STACKS = '1';
|
||||
else
|
||||
process.env.PW_LIVE_TRACE_STACKS = undefined;
|
||||
|
||||
const testIdSet = testIds ? new Set<string>(testIds) : null;
|
||||
this._config.cliArgs = locations ? locations : [];
|
||||
this._config.cliGrep = grep;
|
||||
this._config.cliListOnly = false;
|
||||
this._config.cliProjectFilter = projects?.length ? projects : undefined;
|
||||
this._config.testIdMatcher = id => !testIdSet || testIdSet.has(id);
|
||||
const { config, error } = await this._loadConfig(this._configFile, overrides);
|
||||
if (!config) {
|
||||
const wireReporter = await this._wireReporter(e => this._dispatchEvent('report', e));
|
||||
wireReporter.onError(error!);
|
||||
return { status: 'failed' };
|
||||
}
|
||||
|
||||
const reporters = await createReporters(this._config, 'ui');
|
||||
reporters.push(await createReporterForTestServer(this._config, params.reporter || require.resolve('./uiModeReporter'), 'list', e => this._dispatchEvent('report', e)));
|
||||
const testIdSet = params.testIds ? new Set<string>(params.testIds) : null;
|
||||
config.cliListOnly = false;
|
||||
config.cliPassWithNoTests = true;
|
||||
config.cliArgs = params.locations || [];
|
||||
config.cliGrep = params.grep;
|
||||
config.cliGrepInvert = params.grepInvert;
|
||||
config.cliProjectFilter = params.projects?.length ? params.projects : undefined;
|
||||
config.testIdMatcher = testIdSet ? id => testIdSet.has(id) : undefined;
|
||||
|
||||
const reporters = await createReporters(config, 'test', true);
|
||||
const wireReporter = await this._wireReporter(e => this._dispatchEvent('report', e));
|
||||
reporters.push(wireReporter);
|
||||
const reporter = new InternalReporter(new Multiplexer(reporters));
|
||||
const taskRunner = createTaskRunnerForWatch(this._config, reporter);
|
||||
const testRun = new TestRun(this._config, reporter);
|
||||
reporter.onConfigure(this._config.config);
|
||||
const taskRunner = createTaskRunnerForTestServer(config, reporter);
|
||||
const testRun = new TestRun(config, reporter);
|
||||
reporter.onConfigure(config.config);
|
||||
const stop = new ManualPromise();
|
||||
const run = taskRunner.run(testRun, 0, stop).then(async status => {
|
||||
await reporter.onEnd({ status });
|
||||
await reporter.onExit();
|
||||
this._testRun = undefined;
|
||||
this._config.testIdMatcher = undefined;
|
||||
return status;
|
||||
});
|
||||
this._testRun = { run, stop };
|
||||
return await run;
|
||||
return { status: await run };
|
||||
}
|
||||
|
||||
async watchTestDir() {
|
||||
this._watchTestDir = true;
|
||||
}
|
||||
|
||||
async watch(params: { fileNames: string[]; }) {
|
||||
|
|
@ -260,8 +328,11 @@ class TestServerDispatcher implements TestServerInterface {
|
|||
this._testWatcher.update([...files], [], true);
|
||||
}
|
||||
|
||||
findRelatedTestFiles(params: { files: string[]; }): Promise<{ testFiles: string[]; errors?: reporterTypes.TestError[] | undefined; }> {
|
||||
const runner = new Runner(this._config);
|
||||
async findRelatedTestFiles(params: Parameters<TestServerInterface['findRelatedTestFiles']>[0]): ReturnType<TestServerInterface['findRelatedTestFiles']> {
|
||||
const { config, error } = await this._loadConfig(this._configFile);
|
||||
if (error)
|
||||
return { testFiles: [], errors: [error] };
|
||||
const runner = new Runner(config!);
|
||||
return runner.findRelatedTestFiles('out-of-process', params.files);
|
||||
}
|
||||
|
||||
|
|
@ -273,10 +344,52 @@ class TestServerDispatcher implements TestServerInterface {
|
|||
async closeGracefully() {
|
||||
gracefullyProcessExitDoNotHang(0);
|
||||
}
|
||||
|
||||
private async _loadConfig(configFile: string | undefined, overrides?: ConfigCLIOverrides): Promise<{ config: FullConfigInternal | null, error?: reporterTypes.TestError }> {
|
||||
const configFileOrDirectory = configFile ? path.resolve(process.cwd(), configFile) : process.cwd();
|
||||
const resolvedConfigFile = resolveConfigFile(configFileOrDirectory);
|
||||
try {
|
||||
const config = await loadConfig({ resolvedConfigFile, configDir: resolvedConfigFile === configFileOrDirectory ? path.dirname(resolvedConfigFile) : configFileOrDirectory }, overrides);
|
||||
// Preserve plugin instances between setup and build.
|
||||
if (!this._plugins)
|
||||
this._plugins = config.plugins || [];
|
||||
else
|
||||
config.plugins.splice(0, config.plugins.length, ...this._plugins);
|
||||
return { config };
|
||||
} catch (e) {
|
||||
return { config: null, error: serializeError(e) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function runTestServer(config: FullConfigInternal, options: { host?: string, port?: number }, openUI: (server: HttpServer, cancelPromise: ManualPromise<void>) => Promise<void>): Promise<reporterTypes.FullResult['status']> {
|
||||
const testServer = new TestServer(config);
|
||||
export async function runUIMode(configFile: string | undefined, options: TraceViewerServerOptions & TraceViewerRedirectOptions): Promise<reporterTypes.FullResult['status']> {
|
||||
return await innerRunTestServer(configFile, options, async (server: HttpServer, cancelPromise: ManualPromise<void>) => {
|
||||
await installRootRedirect(server, [], { ...options, webApp: 'uiMode.html' });
|
||||
if (options.host !== undefined || options.port !== undefined) {
|
||||
await openTraceInBrowser(server.urlPrefix());
|
||||
} else {
|
||||
const page = await openTraceViewerApp(server.urlPrefix(), 'chromium', {
|
||||
headless: isUnderTest() && process.env.PWTEST_HEADED_FOR_TEST !== '1',
|
||||
persistentContextOptions: {
|
||||
handleSIGINT: false,
|
||||
},
|
||||
});
|
||||
page.on('close', () => cancelPromise.resolve());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function runTestServer(configFile: string | undefined, options: { host?: string, port?: number }): Promise<reporterTypes.FullResult['status']> {
|
||||
return await innerRunTestServer(configFile, options, async server => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Listening on ' + server.urlPrefix().replace('http:', 'ws:') + '/' + server.wsGuid());
|
||||
});
|
||||
}
|
||||
|
||||
async function innerRunTestServer(configFile: string | undefined, options: { host?: string, port?: number }, openUI: (server: HttpServer, cancelPromise: ManualPromise<void>) => Promise<void>): Promise<reporterTypes.FullResult['status']> {
|
||||
if (restartWithExperimentalTsEsm(undefined, true))
|
||||
return 'passed';
|
||||
const testServer = new TestServer(configFile);
|
||||
const cancelPromise = new ManualPromise<void>();
|
||||
const sigintWatcher = new SigIntWatcher();
|
||||
void sigintWatcher.promise().then(() => cancelPromise.resolve());
|
||||
|
|
|
|||
|
|
@ -137,14 +137,10 @@ export class TimeoutManager {
|
|||
switch (runnable.type) {
|
||||
case 'test': {
|
||||
if (runnable.fixture) {
|
||||
if (runnable.fixture.phase === 'setup') {
|
||||
if (runnable.fixture.phase === 'setup')
|
||||
message = `Test timeout of ${timeout}ms exceeded while setting up "${runnable.fixture.title}".`;
|
||||
} else {
|
||||
message = [
|
||||
`Test finished within timeout of ${timeout}ms, but tearing down "${runnable.fixture.title}" ran out of time.`,
|
||||
`Please allow more time for the test, since teardown is attributed towards the test timeout budget.`,
|
||||
].join('\n');
|
||||
}
|
||||
else
|
||||
message = `Tearing down "${runnable.fixture.title}" exceeded the test timeout of ${timeout}ms.`;
|
||||
} else {
|
||||
message = `Test timeout of ${timeout}ms exceeded.`;
|
||||
}
|
||||
|
|
|
|||
7
packages/playwright/types/test.d.ts
vendored
7
packages/playwright/types/test.d.ts
vendored
|
|
@ -5590,10 +5590,11 @@ export interface PlaywrightWorkerOptions {
|
|||
* Whether to record trace for each test. Defaults to `'off'`.
|
||||
* - `'off'`: Do not record trace.
|
||||
* - `'on'`: Record trace for each test.
|
||||
* - `'retain-on-failure'`: Record trace for each test, but remove all traces from successful test runs.
|
||||
* - `'on-first-retry'`: Record trace only when retrying a test for the first time.
|
||||
* - `'on-all-retries'`: Record traces only when retrying for all retries.
|
||||
* - `'retain-on-first-failure'`: Record traces only when the test fails for the first time.
|
||||
* - `'on-all-retries'`: Record trace only when retrying a test.
|
||||
* - `'retain-on-failure'`: Record trace for each test. When test run passes, remove the recorded trace.
|
||||
* - `'retain-on-first-failure'`: Record trace for the first run of each test, but not for retires. When test run
|
||||
* passes, remove the recorded trace.
|
||||
*
|
||||
* For more control, pass an object that specifies `mode` and trace features to enable.
|
||||
*
|
||||
|
|
|
|||
92
packages/trace-viewer/src/ui/tag.css
Normal file
92
packages/trace-viewer/src/ui/tag.css
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 2em;
|
||||
background-color: #8c959f;
|
||||
color: white;
|
||||
margin: 0 10px;
|
||||
flex: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.light-mode .tag-color-0 {
|
||||
background-color: #ddf4ff;
|
||||
color: #0550ae;
|
||||
border: 1px solid #218bff;
|
||||
}
|
||||
.light-mode .tag-color-1 {
|
||||
background-color: #fff8c5;
|
||||
color: #7d4e00;
|
||||
border: 1px solid #bf8700;
|
||||
}
|
||||
.light-mode .tag-color-2 {
|
||||
background-color: #fbefff;
|
||||
color: #6e40c9;
|
||||
border: 1px solid #a475f9;
|
||||
}
|
||||
.light-mode .tag-color-3 {
|
||||
background-color: #ffeff7;
|
||||
color: #99286e;
|
||||
border: 1px solid #e85aad;
|
||||
}
|
||||
.light-mode .tag-color-4 {
|
||||
background-color: #FFF0EB;
|
||||
color: #9E2F1C;
|
||||
border: 1px solid #EA6045;
|
||||
}
|
||||
.light-mode .tag-color-5 {
|
||||
background-color: #fff1e5;
|
||||
color: #9b4215;
|
||||
border: 1px solid #e16f24;
|
||||
}
|
||||
|
||||
.dark-mode .tag-color-0 {
|
||||
background-color: #051d4d;
|
||||
color: #80ccff;
|
||||
border: 1px solid #218bff;
|
||||
}
|
||||
.dark-mode .tag-color-1 {
|
||||
background-color: #3b2300;
|
||||
color: #eac54f;
|
||||
border: 1px solid #bf8700;
|
||||
}
|
||||
.dark-mode .tag-color-2 {
|
||||
background-color: #271052;
|
||||
color: #d2a8ff;
|
||||
border: 1px solid #a475f9;
|
||||
}
|
||||
.dark-mode .tag-color-3 {
|
||||
background-color: #42062a;
|
||||
color: #ff9bce;
|
||||
border: 1px solid #e85aad;
|
||||
}
|
||||
.dark-mode .tag-color-4 {
|
||||
background-color: #460701;
|
||||
color: #FFA28B;
|
||||
border: 1px solid #EC6547;
|
||||
}
|
||||
.dark-mode .tag-color-5 {
|
||||
background-color: #471700;
|
||||
color: #ffa657;
|
||||
border: 1px solid #e16f24;
|
||||
}
|
||||
36
packages/trace-viewer/src/ui/tag.tsx
Normal file
36
packages/trace-viewer/src/ui/tag.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* 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 './tag.css';
|
||||
|
||||
export const TagView: React.FC<{ tag: string, style?: React.CSSProperties, onClick?: (e: React.MouseEvent) => void }> = ({ tag, style, onClick }) => {
|
||||
return <span
|
||||
className={`tag tag-color-${tagNameToColor(tag)}`}
|
||||
onClick={onClick}
|
||||
style={{ margin: '6px 0 0 6px', ...style }}
|
||||
title={`Click to filter by tag: ${tag}`}
|
||||
>
|
||||
{tag}
|
||||
</span>;
|
||||
};
|
||||
|
||||
// hash string to integer in range [0, 6] for color index, to get same color for same tag
|
||||
function tagNameToColor(str: string) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++)
|
||||
hash = str.charCodeAt(i) + ((hash << 8) - hash);
|
||||
return Math.abs(hash % 6);
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ import { testStatusIcon } from './testUtils';
|
|||
import type { TestModel } from './uiModeModel';
|
||||
import './uiModeTestListView.css';
|
||||
import type { TestServerConnection } from '@testIsomorphic/testServerConnection';
|
||||
import { TagView } from './tag';
|
||||
|
||||
const TestTreeView = TreeView<TreeItem>;
|
||||
|
||||
|
|
@ -46,7 +47,8 @@ export const TestListView: React.FC<{
|
|||
isLoading?: boolean,
|
||||
onItemSelected: (item: { treeItem?: TreeItem, testCase?: reporterTypes.TestCase, testFile?: SourceLocation }) => void,
|
||||
requestedCollapseAllCount: number,
|
||||
}> = ({ filterText, testModel, testServerConnection, testTree, runTests, runningState, watchAll, watchedTreeIds, setWatchedTreeIds, isLoading, onItemSelected, requestedCollapseAllCount }) => {
|
||||
setFilterText: (text: string) => void;
|
||||
}> = ({ filterText, testModel, testServerConnection, testTree, runTests, runningState, watchAll, watchedTreeIds, setWatchedTreeIds, isLoading, onItemSelected, requestedCollapseAllCount, setFilterText }) => {
|
||||
const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() });
|
||||
const [selectedTreeItemId, setSelectedTreeItemId] = React.useState<string | undefined>();
|
||||
const [collapseAllCount, setCollapseAllCount] = React.useState(requestedCollapseAllCount);
|
||||
|
|
@ -132,6 +134,21 @@ export const TestListView: React.FC<{
|
|||
runTests('bounce-if-busy', testTree.collectTestIds(treeItem));
|
||||
};
|
||||
|
||||
const handleTagClick = (e: React.MouseEvent, tag: string) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
const parts = filterText.split(' ');
|
||||
if (parts.includes(tag))
|
||||
setFilterText(parts.filter(t => t !== tag).join(' ').trim());
|
||||
else
|
||||
setFilterText((filterText + ' ' + tag).trim());
|
||||
} else {
|
||||
// Replace all existing tags with this tag.
|
||||
setFilterText((filterText.split(' ').filter(t => !t.startsWith('@')).join(' ') + ' ' + tag).trim());
|
||||
}
|
||||
};
|
||||
|
||||
return <TestTreeView
|
||||
name='tests'
|
||||
treeState={treeState}
|
||||
|
|
@ -140,7 +157,10 @@ export const TestListView: React.FC<{
|
|||
dataTestId='test-tree'
|
||||
render={treeItem => {
|
||||
return <div className='hbox ui-mode-list-item'>
|
||||
<div className='ui-mode-list-item-title' title={treeItem.title}>{treeItem.title}</div>
|
||||
<div className='ui-mode-list-item-title'>
|
||||
<span title={treeItem.title}>{treeItem.title}</span>
|
||||
{treeItem.kind === 'case' ? treeItem.tags.map(tag => <TagView key={tag} tag={tag.slice(1)} onClick={e => handleTagClick(e, tag)} />) : null}
|
||||
</div>
|
||||
{!!treeItem.duration && treeItem.status !== 'skipped' && <div className='ui-mode-list-item-time'>{msToString(treeItem.duration)}</div>}
|
||||
<Toolbar noMinHeight={true} noShadow={true}>
|
||||
<ToolbarButton icon='play' title='Run' onClick={() => runTreeItem(treeItem)} disabled={!!runningState}></ToolbarButton>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,23 @@ const xtermDataSource: XtermDataSource = {
|
|||
resize: () => {},
|
||||
};
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const guid = searchParams.get('ws');
|
||||
const wsURL = new URL(`../${guid}`, window.location.toString());
|
||||
wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:');
|
||||
const queryParams = {
|
||||
args: searchParams.getAll('arg'),
|
||||
grep: searchParams.get('grep') || undefined,
|
||||
grepInvert: searchParams.get('grepInvert') || undefined,
|
||||
projects: searchParams.getAll('project'),
|
||||
workers: searchParams.get('workers') || undefined,
|
||||
timeout: searchParams.has('timeout') ? +searchParams.get('timeout')! : undefined,
|
||||
headed: searchParams.has('headed'),
|
||||
reporters: searchParams.has('reporter') ? searchParams.getAll('reporter') : undefined,
|
||||
};
|
||||
|
||||
const isMac = navigator.platform === 'MacIntel';
|
||||
|
||||
export const UIModeView: React.FC<{}> = ({
|
||||
}) => {
|
||||
const [filterText, setFilterText] = React.useState<string>('');
|
||||
|
|
@ -76,9 +93,6 @@ export const UIModeView: React.FC<{}> = ({
|
|||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const reloadTests = React.useCallback(() => {
|
||||
const guid = new URLSearchParams(window.location.search).get('ws');
|
||||
const wsURL = new URL(`../${guid}`, window.location.toString());
|
||||
wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:');
|
||||
setTestServerConnection(new TestServerConnection(wsURL.toString()));
|
||||
}, []);
|
||||
|
||||
|
|
@ -143,7 +157,7 @@ export const UIModeView: React.FC<{}> = ({
|
|||
commandQueue.current = commandQueue.current.then(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await testServerConnection.listTests({});
|
||||
const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args });
|
||||
teleSuiteUpdater.processListReport(result.report);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
|
@ -158,10 +172,11 @@ export const UIModeView: React.FC<{}> = ({
|
|||
setIsLoading(true);
|
||||
setWatchedTreeIds({ value: new Set() });
|
||||
(async () => {
|
||||
const status = await testServerConnection.runGlobalSetup();
|
||||
await testServerConnection.watchTestDir({});
|
||||
const { status } = await testServerConnection.runGlobalSetup({});
|
||||
if (status !== 'passed')
|
||||
return;
|
||||
const result = await testServerConnection.listTests({});
|
||||
const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args });
|
||||
teleSuiteUpdater.processListReport(result.report);
|
||||
|
||||
testServerConnection.onListChanged(updateList);
|
||||
|
|
@ -170,7 +185,7 @@ export const UIModeView: React.FC<{}> = ({
|
|||
});
|
||||
setIsLoading(false);
|
||||
|
||||
const { hasBrowsers } = await testServerConnection.checkBrowsers();
|
||||
const { hasBrowsers } = await testServerConnection.checkBrowsers({});
|
||||
setHasBrowsers(hasBrowsers);
|
||||
})();
|
||||
return () => {
|
||||
|
|
@ -251,7 +266,18 @@ export const UIModeView: React.FC<{}> = ({
|
|||
setProgress({ total: 0, passed: 0, failed: 0, skipped: 0 });
|
||||
setRunningState({ testIds });
|
||||
|
||||
await testServerConnection.runTests({ testIds: [...testIds], projects: [...projectFilters].filter(([_, v]) => v).map(([p]) => p) });
|
||||
await testServerConnection.runTests({
|
||||
locations: queryParams.args,
|
||||
grep: queryParams.grep,
|
||||
grepInvert: queryParams.grepInvert,
|
||||
testIds: [...testIds],
|
||||
projects: [...projectFilters].filter(([_, v]) => v).map(([p]) => p),
|
||||
workers: queryParams.workers,
|
||||
timeout: queryParams.timeout,
|
||||
headed: queryParams.headed,
|
||||
reporters: queryParams.reporters,
|
||||
trace: 'on',
|
||||
});
|
||||
// Clear pending tests in case of interrupt.
|
||||
for (const test of testModel.rootSuite?.allTests() || []) {
|
||||
if (test.results[0]?.duration === -1)
|
||||
|
|
@ -296,19 +322,22 @@ export const UIModeView: React.FC<{}> = ({
|
|||
if (!testServerConnection)
|
||||
return;
|
||||
const onShortcutEvent = (e: KeyboardEvent) => {
|
||||
if (e.code === 'F6') {
|
||||
if (e.code === 'Backquote' && e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
testServerConnection?.stopTestsNoReply();
|
||||
setIsShowingOutput(!isShowingOutput);
|
||||
} else if (e.code === 'F5' && e.shiftKey) {
|
||||
e.preventDefault();
|
||||
testServerConnection?.stopTestsNoReply({});
|
||||
} else if (e.code === 'F5') {
|
||||
e.preventDefault();
|
||||
reloadTests();
|
||||
runTests('bounce-if-busy', visibleTestIds);
|
||||
}
|
||||
};
|
||||
addEventListener('keydown', onShortcutEvent);
|
||||
return () => {
|
||||
removeEventListener('keydown', onShortcutEvent);
|
||||
};
|
||||
}, [runTests, reloadTests, testServerConnection]);
|
||||
}, [runTests, reloadTests, testServerConnection, visibleTestIds, isShowingOutput]);
|
||||
|
||||
const isRunningTest = !!runningState;
|
||||
const dialogRef = React.useRef<HTMLDialogElement>(null);
|
||||
|
|
@ -325,9 +354,9 @@ export const UIModeView: React.FC<{}> = ({
|
|||
const installBrowsers = React.useCallback((e: React.MouseEvent) => {
|
||||
closeInstallDialog(e);
|
||||
setIsShowingOutput(true);
|
||||
testServerConnection?.installBrowsers().then(async () => {
|
||||
testServerConnection?.installBrowsers({}).then(async () => {
|
||||
setIsShowingOutput(false);
|
||||
const { hasBrowsers } = await testServerConnection?.checkBrowsers();
|
||||
const { hasBrowsers } = await testServerConnection?.checkBrowsers({});
|
||||
setHasBrowsers(hasBrowsers);
|
||||
});
|
||||
}, [closeInstallDialog, testServerConnection]);
|
||||
|
|
@ -369,7 +398,7 @@ export const UIModeView: React.FC<{}> = ({
|
|||
<div className='section-title'>Playwright</div>
|
||||
<ToolbarButton icon='color-mode' title='Toggle color mode' onClick={() => toggleTheme()} />
|
||||
<ToolbarButton icon='refresh' title='Reload' onClick={() => reloadTests()} disabled={isRunningTest || isLoading}></ToolbarButton>
|
||||
<ToolbarButton icon='terminal' title='Toggle output' toggled={isShowingOutput} onClick={() => { setIsShowingOutput(!isShowingOutput); }} />
|
||||
<ToolbarButton icon='terminal' title={'Toggle output — ' + (isMac ? '⌃`' : 'Ctrl + `')} toggled={isShowingOutput} onClick={() => { setIsShowingOutput(!isShowingOutput); }} />
|
||||
{!hasBrowsers && <ToolbarButton icon='lightbulb-autofix' style={{ color: 'var(--vscode-list-warningForeground)' }} title='Playwright browsers are missing' onClick={openInstallDialog} />}
|
||||
</Toolbar>
|
||||
<FiltersView
|
||||
|
|
@ -389,8 +418,8 @@ export const UIModeView: React.FC<{}> = ({
|
|||
{isRunningTest && progress && <div data-testid='status-line' className='status-line'>
|
||||
<div>Running {progress.passed}/{runningState.testIds.size} passed ({(progress.passed / runningState.testIds.size) * 100 | 0}%)</div>
|
||||
</div>}
|
||||
<ToolbarButton icon='play' title='Run all' onClick={() => runTests('bounce-if-busy', visibleTestIds)} disabled={isRunningTest || isLoading}></ToolbarButton>
|
||||
<ToolbarButton icon='debug-stop' title='Stop' onClick={() => testServerConnection?.stopTests()} disabled={!isRunningTest || isLoading}></ToolbarButton>
|
||||
<ToolbarButton icon='play' title='Run all — F5' onClick={() => runTests('bounce-if-busy', visibleTestIds)} disabled={isRunningTest || isLoading}></ToolbarButton>
|
||||
<ToolbarButton icon='debug-stop' title={'Stop — ' + (isMac ? '⇧F5' : 'Shift + F5')} onClick={() => testServerConnection?.stopTests({})} disabled={!isRunningTest || isLoading}></ToolbarButton>
|
||||
<ToolbarButton icon='eye' title='Watch all' toggled={watchAll} onClick={() => {
|
||||
setWatchedTreeIds({ value: new Set() });
|
||||
setWatchAll(!watchAll);
|
||||
|
|
@ -411,7 +440,9 @@ export const UIModeView: React.FC<{}> = ({
|
|||
watchedTreeIds={watchedTreeIds}
|
||||
setWatchedTreeIds={setWatchedTreeIds}
|
||||
isLoading={isLoading}
|
||||
requestedCollapseAllCount={collapseAllCount} />
|
||||
requestedCollapseAllCount={collapseAllCount}
|
||||
setFilterText={setFilterText}
|
||||
/>
|
||||
</div>
|
||||
</SplitView>
|
||||
</div>;
|
||||
|
|
|
|||
|
|
@ -983,6 +983,40 @@ it('should support multipart/form-data and keep the order', async function({ con
|
|||
expect(response.status()).toBe(200);
|
||||
});
|
||||
|
||||
it('should support repeating names in multipart/form-data', async function({ context, server }) {
|
||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/28070' });
|
||||
const postBodyPromise = new Promise<string>(resolve => {
|
||||
server.setRoute('/empty.html', async (req, res) => {
|
||||
resolve((await req.postBody).toString('utf-8'));
|
||||
res.writeHead(200, {
|
||||
'content-type': 'text/plain',
|
||||
});
|
||||
res.end('OK.');
|
||||
});
|
||||
});
|
||||
const [postBody, response] = await Promise.all([
|
||||
postBodyPromise,
|
||||
context.request.post(server.EMPTY_PAGE, {
|
||||
multipart: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
file: [{
|
||||
name: 'f1.js',
|
||||
mimeType: 'text/javascript',
|
||||
buffer: Buffer.from('var x = 10;\r\n;console.log(x);')
|
||||
}, {
|
||||
name: 'f2.txt',
|
||||
mimeType: 'text/plain',
|
||||
buffer: Buffer.from('hello')
|
||||
}]
|
||||
}
|
||||
})
|
||||
]);
|
||||
expect(postBody).toContain(`content-disposition: form-data; name="file"; filename="f1.js"\r\ncontent-type: text/javascript\r\n\r\nvar x = 10;\r\n;console.log(x);`);
|
||||
expect(postBody).toContain(`content-disposition: form-data; name="file"; filename="f2.txt"\r\ncontent-type: text/plain\r\n\r\nhello`);
|
||||
expect(response.status()).toBe(200);
|
||||
});
|
||||
|
||||
it('should serialize data to json regardless of content-type', async function({ context, server }) {
|
||||
const data = {
|
||||
firstName: 'John',
|
||||
|
|
|
|||
|
|
@ -648,18 +648,4 @@ await page.GetByLabel("Coun\\"try").ClickAsync();`);
|
|||
expect.soft(sources1.get('Java')!.text).toContain(`assertThat(page.getByRole(AriaRole.TEXTBOX)).isVisible()`);
|
||||
expect.soft(sources1.get('C#')!.text).toContain(`await Expect(page.GetByRole(AriaRole.Textbox)).ToBeVisibleAsync()`);
|
||||
});
|
||||
|
||||
test('should assert screenshot', async ({ openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
await recorder.setContentAndWait(`<div>Hello, world</div>`);
|
||||
const [sources] = await Promise.all([
|
||||
recorder.waitForOutput('JavaScript', 'toHaveScreenshot'),
|
||||
recorder.page.click('x-pw-tool-item.screenshot'),
|
||||
]);
|
||||
expect.soft(sources.get('JavaScript')!.text).toContain(`await expect(page).toHaveScreenshot()`);
|
||||
expect.soft(sources.get('Python')!.text).toContain(`# assert_screenshot(page.screenshot())`);
|
||||
expect.soft(sources.get('Python Async')!.text).toContain(`# assert_screenshot(await page.screenshot())`);
|
||||
expect.soft(sources.get('Java')!.text).toContain(`// assertScreenshot(page.screenshot());`);
|
||||
expect.soft(sources.get('C#')!.text).toContain(`// AssertScreenshot(await page.ScreenshotAsync())`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -299,22 +299,22 @@ it('should work with COEP/COOP/CORP isolated iframe', async ({ page, server, bro
|
|||
expect(await page.frames()[1].evaluate(() => window['__clicked'])).toBe(true);
|
||||
});
|
||||
|
||||
it('locator.enterFrame should work', async ({ page, server }) => {
|
||||
it('locator.contentFrame should work', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const locator = page.locator('iframe');
|
||||
const frameLocator = locator.enterFrame();
|
||||
const frameLocator = locator.contentFrame();
|
||||
const button = frameLocator.locator('button');
|
||||
expect(await button.innerText()).toBe('Hello iframe');
|
||||
await expect(button).toHaveText('Hello iframe');
|
||||
await button.click();
|
||||
});
|
||||
|
||||
it('frameLocator.exitFrame should work', async ({ page, server }) => {
|
||||
it('frameLocator.owner should work', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const frameLocator = page.frameLocator('iframe');
|
||||
const locator = frameLocator.exitFrame();
|
||||
const locator = frameLocator.owner();
|
||||
await expect(locator).toBeVisible();
|
||||
expect(await locator.getAttribute('name')).toBe('frame1');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -348,7 +348,7 @@ it('should return body for prefetch script', async ({ page, server, browserName
|
|||
expect(body.toString()).toBe('// Scripts will be pre-fetched');
|
||||
});
|
||||
|
||||
it('should bypass disk cache when interception is enabled', async ({ page, server, browserName }) => {
|
||||
it('should bypass disk cache when page interception is enabled', async ({ page, server, browserName }) => {
|
||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30000' });
|
||||
it.fixme(browserName === 'firefox', 'Returns cached response.');
|
||||
await page.goto(server.PREFIX + '/frames/one-frame.html');
|
||||
|
|
@ -399,3 +399,32 @@ it('should bypass disk cache when interception is enabled', async ({ page, serve
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should bypass disk cache when context interception is enabled', async ({ page, server, browserName }) => {
|
||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30000' });
|
||||
it.fixme(browserName === 'firefox', 'Returns cached response.');
|
||||
await page.context().route('**/api*', route => route.continue());
|
||||
await page.goto(server.PREFIX + '/frames/one-frame.html');
|
||||
{
|
||||
const requests = [];
|
||||
server.setRoute('/api', (req, res) => {
|
||||
requests.push(req);
|
||||
res.statusCode = 200;
|
||||
res.setHeader('content-type', 'text/plain');
|
||||
res.setHeader('cache-control', 'public, max-age=31536000');
|
||||
res.end('Hello');
|
||||
});
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await it.step(`main frame iteration ${i}`, async () => {
|
||||
const respPromise = page.waitForResponse('**/api');
|
||||
await page.evaluate(async () => {
|
||||
const response = await fetch('/api');
|
||||
return response.status;
|
||||
});
|
||||
const response = await respPromise;
|
||||
expect(response.status()).toBe(200);
|
||||
expect(requests.length).toBe(i + 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -438,3 +438,21 @@ it('should fulfill json', async ({ page, server }) => {
|
|||
expect(response.headers()['content-type']).toBe('application/json');
|
||||
expect(body).toBe(JSON.stringify({ bar: 'baz' }));
|
||||
});
|
||||
|
||||
it('should fulfill with gzip and readback', {
|
||||
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/29261' },
|
||||
}, async ({ page, server, isAndroid, isElectron }) => {
|
||||
it.skip(isAndroid, 'The internal Android localhost (10.0.0.2) != the localhost on the host');
|
||||
it.fixme(isElectron, 'error: Browser context management is not supported.');
|
||||
server.enableGzip('/one-style.html');
|
||||
await page.route('**/one-style.html', async route => {
|
||||
const response = await route.fetch();
|
||||
expect(response.headers()['content-encoding']).toBe('gzip');
|
||||
await route.fulfill({ response });
|
||||
});
|
||||
|
||||
const response = await page.goto(server.PREFIX + '/one-style.html');
|
||||
await expect(page.locator('div')).toHaveText('hello, world!');
|
||||
await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)');
|
||||
expect(await response.text()).toContain(`<div>hello, world!</div>`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ test('should handle fixture timeout', async ({ runInlineTest }) => {
|
|||
`
|
||||
}, { timeout: 500 });
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.output).toContain('Test finished within timeout of 500ms, but tearing down "timeout" ran out of time.');
|
||||
expect(result.output).toContain('Tearing down "timeout" exceeded the test timeout of 500ms.');
|
||||
expect(result.failed).toBe(2);
|
||||
});
|
||||
|
||||
|
|
@ -459,7 +459,7 @@ test('should not give enough time for second fixture teardown after timeout', as
|
|||
}, { timeout: 2000 });
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(result.output).toContain('Test finished within timeout of 3000ms, but tearing down "fixture" ran out of time.');
|
||||
expect(result.output).toContain('Tearing down "fixture" exceeded the test timeout of 3000ms.');
|
||||
expect(result.outputLines).toEqual([
|
||||
'teardown start',
|
||||
'teardown finished',
|
||||
|
|
@ -525,7 +525,7 @@ test('should not report fixture teardown timeout twice', async ({ runInlineTest
|
|||
}, { reporter: 'list', timeout: 1000 });
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(result.output).toContain('Test finished within timeout of 1000ms, but tearing down "fixture" ran out of time.');
|
||||
expect(result.output).toContain('Tearing down "fixture" exceeded the test timeout of 1000ms.');
|
||||
expect(result.output).not.toContain('base.extend'); // Should not point to the location.
|
||||
expect(result.output).not.toContain('Worker teardown timeout');
|
||||
});
|
||||
|
|
@ -730,7 +730,7 @@ test('should not continue with scope teardown after fixture teardown timeout', a
|
|||
}, { reporter: 'list', timeout: 1000 });
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(result.output).toContain('Test finished within timeout of 1000ms, but tearing down "fixture2" ran out of time.');
|
||||
expect(result.output).toContain('Tearing down "fixture2" exceeded the test timeout of 1000ms.');
|
||||
expect(result.output).not.toContain('in fixture teardown');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
import { test, expect, playwrightCtConfigText } from './playwright-test-fixtures';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
|
||||
|
|
@ -121,6 +122,28 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
|||
await expect(component).toHaveText('Clashing name 2');
|
||||
});
|
||||
`,
|
||||
'src/relative-import-different-folders/one/index.tsx': `
|
||||
export default () => <button>Button</button>;
|
||||
`,
|
||||
'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(<Button></Button>);
|
||||
await expect(component).toHaveText('Button');
|
||||
});
|
||||
`,
|
||||
'src/relative-import-different-folders/two/index.tsx': `
|
||||
export default () => <button>Button</button>;
|
||||
`,
|
||||
'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(<Button></Button>);
|
||||
await expect(component).toHaveText('Button');
|
||||
});
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
|
|
@ -158,6 +181,14 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
|||
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))
|
||||
|
|
@ -184,6 +215,14 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
|||
expect.stringContaining('jsx-runtime.js'),
|
||||
expect.stringContaining('button.tsx'),
|
||||
]],
|
||||
[expect.stringContaining(`one${path.sep}index.tsx`), [
|
||||
expect.stringContaining('jsx-runtime.js'),
|
||||
expect.stringContaining(`one${path.sep}index.tsx`),
|
||||
]],
|
||||
[expect.stringContaining(`two${path.sep}index.tsx`), [
|
||||
expect.stringContaining('jsx-runtime.js'),
|
||||
expect.stringContaining(`two${path.sep}index.tsx`),
|
||||
]],
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -497,6 +536,24 @@ test('should render component via re-export', async ({ runInlineTest }, testInfo
|
|||
expect(result.passed).toBe(1);
|
||||
});
|
||||
|
||||
test('should import json', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': playwrightCtConfigText,
|
||||
'playwright/index.html': `<script type="module" src="./index.ts"></script>`,
|
||||
'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,
|
||||
|
|
|
|||
|
|
@ -1236,6 +1236,39 @@ test('preserve reportName on projects', async ({ runInlineTest, mergeReports })
|
|||
expect(output).toContain(`botNames: first,second`);
|
||||
});
|
||||
|
||||
test('keep projects with same name different bot name separate', async ({ runInlineTest, mergeReports, showReport, page }) => {
|
||||
const files = (reportName: string) => ({
|
||||
'playwright.config.ts': `
|
||||
module.exports = {
|
||||
reporter: [['blob', { fileName: '${reportName}.zip' }]],
|
||||
projects: [
|
||||
{ name: 'foo' },
|
||||
]
|
||||
};
|
||||
`,
|
||||
'a.test.js': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test 1', async ({}) => { expect('${reportName}').toBe('second'); });
|
||||
`,
|
||||
});
|
||||
|
||||
await runInlineTest(files('first'), undefined, { PWTEST_BOT_NAME: 'first' });
|
||||
await runInlineTest(files('second'), undefined, { PWTEST_BOT_NAME: 'second', PWTEST_BLOB_DO_NOT_REMOVE: '1' });
|
||||
|
||||
const reportDir = test.info().outputPath('blob-report');
|
||||
const { exitCode } = await mergeReports(reportDir, { 'PW_TEST_HTML_REPORT_OPEN': 'never' }, { additionalArgs: ['--reporter', 'html'] });
|
||||
expect(exitCode).toBe(0);
|
||||
await showReport();
|
||||
await expect(page.locator('.subnav-item:has-text("Passed") .counter')).toHaveText('1');
|
||||
await expect(page.locator('.subnav-item:has-text("Failed") .counter')).toHaveText('1');
|
||||
|
||||
await page.getByText(/test 1.*first/).getByRole('link', { name: 'test' }).click();
|
||||
await expect(page.getByText('Errors')).toBeVisible();
|
||||
await page.goBack();
|
||||
await page.getByText(/test 1.*second/).getByRole('link', { name: 'test' }).click();
|
||||
await expect(page.getByText('Errors')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('no reports error', async ({ runInlineTest, mergeReports }) => {
|
||||
const reportDir = test.info().outputPath('blob-report');
|
||||
fs.mkdirSync(reportDir, { recursive: true });
|
||||
|
|
|
|||
|
|
@ -78,7 +78,14 @@ test('should ignore stdio when quiet', async ({ runInlineTest }) => {
|
|||
expect(result.output).not.toContain('%%');
|
||||
});
|
||||
|
||||
test('should support console colors', async ({ runInlineTest }) => {
|
||||
test('should support console colors but not tty', {
|
||||
annotation: [
|
||||
{ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/15366' },
|
||||
{ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/29839' },
|
||||
],
|
||||
}, async ({ runInlineTest, nodeVersion }) => {
|
||||
test.skip(nodeVersion.major < 18, 'Node16 does not respect FORCE_COLOR in onsole');
|
||||
|
||||
const result = await runInlineTest({
|
||||
'a.spec.js': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
|
@ -90,31 +97,13 @@ test('should support console colors', async ({ runInlineTest }) => {
|
|||
});
|
||||
`
|
||||
});
|
||||
expect(result.output).toContain(`process.stdout.isTTY = true`);
|
||||
expect(result.output).toContain(`process.stderr.isTTY = true`);
|
||||
expect(result.output).toContain(`process.stdout.isTTY = undefined`);
|
||||
expect(result.output).toContain(`process.stderr.isTTY = undefined`);
|
||||
// The output should have colors.
|
||||
expect(result.rawOutput).toContain(`{ b: \x1b[33mtrue\x1b[39m, n: \x1b[33m123\x1b[39m, s: \x1b[32m'abc'\x1b[39m }`);
|
||||
expect(result.rawOutput).toContain(`{ b: \x1b[33mfalse\x1b[39m, n: \x1b[33m123\x1b[39m, s: \x1b[32m'abc'\x1b[39m }`);
|
||||
});
|
||||
|
||||
test('should override hasColors and getColorDepth', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.js': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('console log', () => {
|
||||
console.log('process.stdout.hasColors(1) = ' + process.stdout.hasColors(1));
|
||||
console.log('process.stderr.hasColors(1) = ' + process.stderr.hasColors(1));
|
||||
console.log('process.stdout.getColorDepth() > 0 = ' + (process.stdout.getColorDepth() > 0));
|
||||
console.log('process.stderr.getColorDepth() > 0 = ' + (process.stderr.getColorDepth() > 0));
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.output).toContain(`process.stdout.hasColors(1) = true`);
|
||||
expect(result.output).toContain(`process.stderr.hasColors(1) = true`);
|
||||
expect(result.output).toContain(`process.stdout.getColorDepth() > 0 = true`);
|
||||
expect(result.output).toContain(`process.stderr.getColorDepth() > 0 = true`);
|
||||
});
|
||||
|
||||
test('should not throw type error when using assert', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.js': `
|
||||
|
|
@ -128,30 +117,3 @@ test('should not throw type error when using assert', async ({ runInlineTest })
|
|||
expect(result.output).not.toContain(`TypeError: process.stderr.hasColors is not a function`);
|
||||
expect(result.output).toContain(`AssertionError`);
|
||||
});
|
||||
|
||||
test('should provide stubs for tty.WriteStream methods on process.stdout/stderr', async ({ runInlineTest }) => {
|
||||
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/29839' });
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
import type * as tty from 'tty';
|
||||
async function checkMethods(stream: tty.WriteStream) {
|
||||
expect(stream.isTTY).toBe(true);
|
||||
await new Promise<void>(r => stream.clearLine(-1, r));;
|
||||
await new Promise<void>(r => stream.clearScreenDown(r));;
|
||||
await new Promise<void>(r => stream.cursorTo(0, 0, r));;
|
||||
await new Promise<void>(r => stream.cursorTo(0, r));;
|
||||
await new Promise<void>(r => stream.moveCursor(1, 1, r));;
|
||||
expect(stream.getWindowSize()).toEqual([stream.columns, stream.rows]);
|
||||
// getColorDepth() and hasColors() are covered in other tests.
|
||||
}
|
||||
test('process.stdout implementd tty.WriteStream methods', () => {
|
||||
checkMethods(process.stdout);
|
||||
});
|
||||
test('process.stderr implementd tty.WriteStream methods', () => {
|
||||
checkMethods(process.stderr);
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export function dumpTestTree(page: Page, options: { time?: boolean } = {}): () =
|
|||
const indent = listItem.querySelectorAll('.list-view-indent').length;
|
||||
const watch = listItem.querySelector('.toolbar-button.eye.toggled') ? ' 👁' : '';
|
||||
const selected = listItem.classList.contains('selected') ? ' <=' : '';
|
||||
const title = listItem.querySelector('.ui-mode-list-item-title').textContent;
|
||||
const title = listItem.querySelector('.ui-mode-list-item-title').childNodes[0].textContent;
|
||||
const timeElement = options.time ? listItem.querySelector('.ui-mode-list-item-time') : undefined;
|
||||
const time = timeElement ? ' ' + timeElement.textContent.replace(/[.\d]+m?s/, 'XXms') : '';
|
||||
result.push(' ' + ' '.repeat(indent) + treeIcon + ' ' + statusIcon + ' ' + title + time + watch + selected);
|
||||
|
|
|
|||
|
|
@ -56,6 +56,22 @@ test('should filter by explicit tags', async ({ runUITest }) => {
|
|||
`);
|
||||
});
|
||||
|
||||
test('should display native tags and filter by them on click', async ({ runUITest }) => {
|
||||
const { page } = await runUITest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('p', () => {});
|
||||
test('pwt', { tag: '@smoke' }, () => {});
|
||||
`,
|
||||
});
|
||||
await page.locator('.ui-mode-list-item-title').getByText('smoke').click();
|
||||
await expect(page.getByPlaceholder('Filter')).toHaveValue('@smoke');
|
||||
await expect.poll(dumpTestTree(page)).toBe(`
|
||||
▼ ◯ a.test.ts
|
||||
◯ pwt
|
||||
`);
|
||||
});
|
||||
|
||||
test('should filter by status', async ({ runUITest }) => {
|
||||
const { page } = await runUITest(basicTestTree);
|
||||
|
||||
|
|
|
|||
|
|
@ -50,8 +50,10 @@ test('should run global setup and teardown', async ({ runUITest }) => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('should teardown on sigint', async ({ runUITest }) => {
|
||||
test('should teardown on sigint', async ({ runUITest, nodeVersion }) => {
|
||||
test.skip(process.platform === 'win32', 'No sending SIGINT on Windows');
|
||||
test.skip(nodeVersion.major < 18);
|
||||
|
||||
const { page, testProcess } = await runUITest({
|
||||
'playwright.config.ts': `
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
|
@ -201,8 +203,9 @@ test('should run part of the setup only', async ({ runUITest }) => {
|
|||
|
||||
for (const useWeb of [true, false]) {
|
||||
test.describe(`web-mode: ${useWeb}`, () => {
|
||||
test('should run teardown with SIGINT', async ({ runUITest }) => {
|
||||
test('should run teardown with SIGINT', async ({ runUITest, nodeVersion }) => {
|
||||
test.skip(process.platform === 'win32', 'No sending SIGINT on Windows');
|
||||
test.skip(nodeVersion.major < 18);
|
||||
const { page, testProcess } = await runUITest({
|
||||
'playwright.config.ts': `
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
|
|
|||
|
|
@ -28,7 +28,29 @@ const basicTestTree = {
|
|||
`
|
||||
};
|
||||
|
||||
test('should stop on F6', async ({ runUITest }) => {
|
||||
test('should run tests', async ({ runUITest }) => {
|
||||
const { page } = await runUITest(basicTestTree);
|
||||
|
||||
await expect(page.getByTitle('Run all')).toBeEnabled();
|
||||
await expect(page.getByTitle('Stop')).toBeDisabled();
|
||||
|
||||
await page.getByPlaceholder('Filter (e.g. text, @tag)').fill('test 3');
|
||||
await page.keyboard.press('F5');
|
||||
|
||||
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
||||
await page.getByPlaceholder('Filter (e.g. text, @tag)').fill('');
|
||||
|
||||
// Only the filtered test was run.
|
||||
await expect.poll(dumpTestTree(page)).toBe(`
|
||||
▼ ◯ a.test.ts
|
||||
◯ test 0
|
||||
◯ test 1
|
||||
◯ test 2
|
||||
✅ test 3
|
||||
`);
|
||||
});
|
||||
|
||||
test('should stop tests', async ({ runUITest }) => {
|
||||
const { page } = await runUITest(basicTestTree);
|
||||
|
||||
await expect(page.getByTitle('Run all')).toBeEnabled();
|
||||
|
|
@ -47,7 +69,7 @@ test('should stop on F6', async ({ runUITest }) => {
|
|||
await expect(page.getByTitle('Run all')).toBeDisabled();
|
||||
await expect(page.getByTitle('Stop')).toBeEnabled();
|
||||
|
||||
await page.keyboard.press('F6');
|
||||
await page.keyboard.press('Shift+F5');
|
||||
|
||||
await expect.poll(dumpTestTree(page)).toBe(`
|
||||
▼ ◯ a.test.ts
|
||||
|
|
@ -58,12 +80,15 @@ test('should stop on F6', async ({ runUITest }) => {
|
|||
`);
|
||||
});
|
||||
|
||||
test('should reload on F5', async ({ runUITest }) => {
|
||||
test('should toggle Terminal', async ({ runUITest }) => {
|
||||
const { page } = await runUITest(basicTestTree);
|
||||
|
||||
await page.getByTitle('Run all').click();
|
||||
await expect(page.getByTestId('status-line')).toHaveText('Running 1/4 passed (25%)');
|
||||
await expect(page.getByTitle('Run all')).toBeEnabled();
|
||||
await expect(page.getByTitle('Stop')).toBeDisabled();
|
||||
|
||||
await page.keyboard.press('F5');
|
||||
await expect(page.getByTestId('status-line')).toBeHidden();
|
||||
});
|
||||
await expect(page.getByTestId('output')).toBeHidden();
|
||||
|
||||
await page.keyboard.press(process.platform === 'darwin' ? 'Control+Backquote' : 'Control+Shift+Backquote');
|
||||
|
||||
await expect(page.getByTestId('output')).toBeVisible();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue