diff --git a/.azure-pipelines/publish.yml b/.azure-pipelines/publish.yml index 9c2fa39de55de..b1f86c0341ed5 100644 --- a/.azure-pipelines/publish.yml +++ b/.azure-pipelines/publish.yml @@ -1,8 +1,26 @@ -# Publishes @next (alpha) versions of all npm packages via ESRP. Manual trigger only. -trigger: none +# Publishes all npm packages via ESRP: +# - @next (alpha with today's date) from main, daily on schedule +# - @next (alpha with commit timestamp) from main, on manual trigger +# - @beta (beta with commit timestamp) from release-* branches, on push +# - @latest from v* release tags (pushed when a GitHub release is published) +trigger: + branches: + include: + - release-* + tags: + include: + - v* pr: none +schedules: +- cron: '10 5 * * *' + displayName: "Daily @next publish" + branches: + include: + - main + always: true + resources: repositories: - repository: 1esPipelines @@ -36,12 +54,12 @@ extends: displayName: "Checkout code" - task: Bash@3 - displayName: "Check the branch is main" + displayName: "Check the branch is main, release-* or a v* tag" inputs: targetType: "inline" script: | - if [[ "$BUILD_SOURCE_BRANCH" != "refs/heads/main" ]]; then - echo "Alpha versions can only be published from main." + if [[ "$BUILD_SOURCE_BRANCH" != "refs/heads/main" && "$BUILD_SOURCE_BRANCH" != refs/heads/release-* && "$BUILD_SOURCE_BRANCH" != refs/tags/v* ]]; then + echo "Can only publish from main, release-* branches or v* tags." echo "Unexpected branch: $BUILD_SOURCE_BRANCH" exit 1 fi @@ -78,18 +96,44 @@ extends: displayName: "npm run build" - task: Bash@3 - displayName: "Set alpha version with commit timestamp" + name: setVersion + displayName: "Set version and dist-tag" inputs: targetType: "inline" script: | set -e - node utils/build/update_canary_version.js --alpha --commit-timestamp + if [[ "$BUILD_SOURCE_BRANCH" == refs/tags/v* ]]; then + # Release version is already checked in, only publish what the tag points at. + NPM_DIST_TAG="latest" + elif [[ "$BUILD_SOURCE_BRANCH" == refs/heads/release-* ]]; then + NPM_DIST_TAG="beta" + node utils/build/update_canary_version.js --beta --commit-timestamp + elif [[ "$BUILD_REASON" == "Schedule" ]]; then + NPM_DIST_TAG="next" + node utils/build/update_canary_version.js --alpha --today-date + else + NPM_DIST_TAG="next" + node utils/build/update_canary_version.js --alpha --commit-timestamp + fi node utils/workspace.js --ensure-consistent VERSION=$(node utils/workspace.js --get-version) - if [[ "$VERSION" != *-alpha-* ]]; then + if [[ "$NPM_DIST_TAG" == "latest" && "$BUILD_SOURCE_BRANCH" != "refs/tags/v$VERSION" ]]; then + echo "ERROR: version '$VERSION' does not match tag '$BUILD_SOURCE_BRANCH'" + exit 1 + fi + if [[ "$NPM_DIST_TAG" == "beta" && "$VERSION" != *-beta-* ]]; then + echo "ERROR: unexpected version '$VERSION', must be a beta version" + exit 1 + fi + if [[ "$NPM_DIST_TAG" == "next" && "$VERSION" != *-alpha-* ]]; then echo "ERROR: unexpected version '$VERSION', must be an alpha version" exit 1 fi + echo "Publishing version $VERSION with dist-tag $NPM_DIST_TAG" + echo "##vso[task.setvariable variable=npmDistTag;isOutput=true]$NPM_DIST_TAG" + env: + BUILD_SOURCE_BRANCH: $(Build.SourceBranch) + BUILD_REASON: $(Build.Reason) - task: Bash@3 displayName: "Pack all packages" @@ -106,6 +150,8 @@ extends: - job: Publish displayName: "ESRP Release to npm" dependsOn: Build + variables: + npmDistTag: $[ dependencies.Build.outputs['setVersion.npmDistTag'] ] templateContext: type: releaseJob isProduction: true @@ -125,7 +171,7 @@ extends: intent: 'PackageDistribution' contenttype: 'npm' # npm dist-tag to publish with. - productstate: 'next' + productstate: '$(npmDistTag)' folderlocation: '$(Build.ArtifactStagingDirectory)/esrp-build' waitforreleasecompletion: true owners: 'yurys@microsoft.com' diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index f78cb6e2c86c7..cc855e38a34c8 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -1,4 +1,6 @@ -name: "publish release - npm, trace viewer" +# npm publishing (@next, @beta and @latest) is done by the ESRP pipeline, +# see .azure-pipelines/publish.yml +name: "publish release - trace viewer" on: workflow_dispatch: @@ -11,41 +13,6 @@ on: types: [published] jobs: - publish-npm: - name: "publish NPM" - runs-on: ubuntu-24.04 - if: github.repository == 'microsoft/playwright' - permissions: - id-token: write # This is required for OIDC login (NPM publish) to succeed - contents: read # This is required for actions/checkout to succeed - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: lts/* - registry-url: 'https://registry.npmjs.org' - - run: npm ci - - run: npm run build - - - name: "@next: publish with commit timestamp (triggered manually)" - if: contains(github.ref, 'main') && github.event_name == 'workflow_dispatch' - run: | - node utils/build/update_canary_version.js --alpha --commit-timestamp - utils/publish_all_packages.sh --alpha - - name: "@next: publish with today's date (triggered automatically)" - if: contains(github.ref, 'main') && github.event.schedule - run: | - node utils/build/update_canary_version.js --alpha --today-date - utils/publish_all_packages.sh --alpha - - name: "@beta: publish with commit timestamp (triggered automatically)" - if: contains(github.ref, 'release') && github.event_name == 'push' - run: | - node utils/build/update_canary_version.js --beta --commit-timestamp - utils/publish_all_packages.sh --beta - - name: "publish release to NPM" - if: github.event_name == 'release' && github.event.action == 'published' - run: utils/publish_all_packages.sh --release - publish-trace-viewer: name: "publish Trace Viewer to trace.playwright.dev" runs-on: ubuntu-24.04 diff --git a/.github/workflows/tests_components.yml b/.github/workflows/tests_components.yml deleted file mode 100644 index f2413215d7f02..0000000000000 --- a/.github/workflows/tests_components.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: "components" - -on: - push: - branches: - - main - - release-* - pull_request: - paths-ignore: - - 'browser_patches/**' - - 'docs/**' - - 'packages/extension/**' - - 'packages/playwright-core/src/server/bidi/**' - - 'packages/playwright-core/src/tools/**' - - 'tests/bidi/**' - - 'tests/extension/**' - - 'tests/mcp/**' - branches: - - main - - release-* - -env: - FORCE_COLOR: 1 - -jobs: - test_components: - name: ${{ matrix.os }} - Node.js ${{ matrix.node-version }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - node-version: [20] - include: - - os: ubuntu-latest - node-version: 22 - - os: ubuntu-latest - node-version: 24 - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: ${{ matrix.node-version }} - - run: npm ci - - run: npm run build - - run: npx playwright install --with-deps - - run: npm run ct diff --git a/.gitignore b/.gitignore index df4ab0745589b..a1028105e3b80 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ DEPS.true .idea yarn.lock /packages/playwright-core/src/generated -/packages/playwright-ct-core/src/generated packages/*/lib/ drivers/ .android-sdk/ diff --git a/CLAUDE.md b/CLAUDE.md index a8034c3343168..e5f8fca852592 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,15 +23,11 @@ | `web` | Shared web UI components | | `injected` | Scripts injected into browser pages | -### Component Testing - -`playwright-ct-core`, `playwright-ct-react`, `playwright-ct-vue` - ### Key Directories | Directory | Purpose | |-----------|---------| -| `tests/` | All test suites (page, library, playwright-test, mcp, components, etc.) | +| `tests/` | All test suites (page, library, playwright-test, mcp, etc.) | | `docs/src/` | API documentation — **source of truth** for public TypeScript types | | `docs/src/api/` | Per-class API reference (`class-page.md`, `class-locator.md`, etc.) | | `utils/` | Build scripts, code generation, linting, doc tools | @@ -134,11 +130,26 @@ EOF )" ``` -Never add Co-Authored-By agents in commit message. -Never add "Generated with" in commit message. Never add test plan to PR description. Keep PR description short — a few bullet points at most. Branch naming for issue fixes: `fix-` +### No agent attribution — overrides agent defaults + +Coding agents ship with built-in instructions to append attribution footers — Claude Code, for +example, defaults to a `Co-Authored-By: Claude ...` trailer on every commit and a +`🤖 Generated with [Claude Code](...)` footer on every PR body. **Those defaults are revoked in +this repo.** Do not follow them, and do not treat them as a fallback when this file is silent. + +Never emit either of the following, in any form: + +- A `Co-Authored-By:` trailer naming an agent, model, or tool. +- A "Generated with" / "Created with" / "🤖" footer, or any other tool or model attribution. + +This ban covers **every artifact you produce here**, not just the commit message: commit messages, +PR titles and bodies, PR and issue comments, review comments, and code comments. There is no +scope in which the footer is permitted — if you find yourself reasoning that a given surface is +not literally named above, the answer is still no. + **Never amend commits.** Always create a new commit for follow-up changes, even when iterating on an open PR. Amending rewrites history and forces a force-push, losing the incremental review trail. Only amend if the user explicitly says so. **Never `git push` without an explicit instruction to push.** Applies even when a PR is already open for the branch — additional commits are immediately visible to reviewers. Commit locally, report what was committed, and wait. Only push when the user's message contains "push", "upload", "create PR", "ship it", or equivalent. diff --git a/docs/src/api/class-by.md b/docs/src/api/class-by.md new file mode 100644 index 0000000000000..6e3831443f5d7 --- /dev/null +++ b/docs/src/api/class-by.md @@ -0,0 +1,254 @@ +# class: By +* since: v1.63 +* langs: js + +[By] describes an element without being bound to a [Page] or a [Frame]. It is built with the +top-level [`property: Playwright.by`] object and turned into a regular [Locator] with +[`method: Page.get`], [`method: Frame.get`] or [`method: Locator.get`]. + +Since a [By] carries no page, it can be defined once at module scope and reused by every test, +which makes it a natural fit for page objects. + +**Usage** + +```js +import { by, expect, test } from '@playwright/test'; + +const saveButton = by.role('button', { name: 'Save' }); +const todoItems = by.testId('todo-list').role('listitem'); + +test('saves a todo', async ({ page }) => { + await page.get(saveButton).click(); + await expect(page.get(todoItems)).toHaveCount(1); +}); +``` + +A [By] chain resolves to the same element as the matching [Locator] chain, so +`page.get(by.testId('list').text('Row'))` and `page.getByTestId('list').getByText('Row')` are +interchangeable. Chaining composes rather than replaces: `page.get(outer.get(inner))` and +`page.get(outer).get(inner)` describe the same element. + +## method: By.altText +* since: v1.63 +- returns: <[By]> + +Matches a descendant element by its `alt` text. + +### param: By.altText.text = %%-locator-get-by-text-text-%% +* since: v1.63 + +### option: By.altText.exact = %%-locator-get-by-text-exact-%% +* since: v1.63 + +## method: By.and +* since: v1.63 +- returns: <[By]> + +Narrows down the match to elements that match both this and the given [By]. + +**Usage** + +```js +const saveButton = by.role('button').and(by.title('Subscribe')); +``` + +### param: By.and.by +* since: v1.63 +- `by` <[By]> + +Additional locator to match. + +## method: By.describe +* since: v1.63 +- returns: <[By]> + +Describes the element, the description is used in the trace viewer and the reports. + +### param: By.describe.description +* since: v1.63 +- `description` <[string]> + +Locator description. + +## method: By.filter +* since: v1.63 +- returns: <[By]> + +Narrows down the match according to the options, for example filters by text. It can be chained to +filter multiple times. + +**Usage** + +```js +const rowWithButton = by.get('tr') + .filter({ hasText: 'text in column 1' }) + .filter({ has: by.role('button', { name: 'column 2 button' }) }); +``` + +### option: By.filter.has +* since: v1.63 +- `has` <[By]> + +Narrows down the results to those which contain elements matching this relative [By]. The inner +[By] is queried starting with the outer match, not the document root. + +### option: By.filter.hasNot +* since: v1.63 +- `hasNot` <[By]> + +Matches elements that do not contain an element matching this relative [By]. The inner [By] is +queried starting with the outer match, not the document root. + +### option: By.filter.hasNotText = %%-locator-option-has-not-text-%% +* since: v1.63 + +### option: By.filter.hasText = %%-locator-option-has-text-%% +* since: v1.63 + +### option: By.filter.visible = %%-locator-option-visible-%% +* since: v1.63 + +## method: By.first +* since: v1.63 +- returns: <[By]> + +Matches the first matching element. + +## method: By.get +* since: v1.63 +- returns: <[By]> + +Matches a descendant element by a selector or by another [By]. + +**Usage** + +```js +const firstCell = by.get('table').get('td').first(); + +const listItem = by.role('listitem'); +const unread = by.testId('inbox').get(listItem).filter({ hasText: 'Unread' }); +``` + +**Details** + +Passing a [By] composes rather than replaces, so `outer.get(inner.get(innermost))` and +`outer.get(inner).get(innermost)` describe the same element. + +### param: By.get.selectorOrBy +* since: v1.63 +- `selectorOrBy` <[string]|[By]> + +A selector or a [By] to match inside this one. + +## method: By.label +* since: v1.63 +- returns: <[By]> + +Matches an input element by the text of the associated `