From e866e3306ee7e07918733cefca33ce6451bbb3e1 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 21 Oct 2024 17:00:10 +0200 Subject: [PATCH 1/6] devops: stop publishing Ubuntu 20.04 (#33203) --- docs/src/docker.md | 4 +-- utils/docker/Dockerfile.focal | 51 ---------------------------------- utils/docker/build.sh | 6 ++-- utils/docker/publish_docker.sh | 22 +++------------ 4 files changed, 8 insertions(+), 75 deletions(-) delete mode 100644 utils/docker/Dockerfile.focal diff --git a/docs/src/docker.md b/docs/src/docker.md index 665904a144..bfcf5e73b5 100644 --- a/docs/src/docker.md +++ b/docs/src/docker.md @@ -5,7 +5,7 @@ title: "Docker" ## Introduction -[Dockerfile.jammy] can be used to run Playwright scripts in Docker environment. This image includes the [Playwright browsers](./browsers.md#install-browsers) and [browser system dependencies](./browsers.md#install-system-dependencies). The Playwright package/dependency is not included in the image and should be installed separately. +[Dockerfile.noble] can be used to run Playwright scripts in Docker environment. This image includes the [Playwright browsers](./browsers.md#install-browsers) and [browser system dependencies](./browsers.md#install-system-dependencies). The Playwright package/dependency is not included in the image and should be installed separately. ## Usage @@ -111,7 +111,6 @@ We currently publish images with the following tags: - `:v%%VERSION%%` - Playwright v%%VERSION%% release docker image based on Ubuntu 24.04 LTS (Noble Numbat). - `:v%%VERSION%%-noble` - Playwright v%%VERSION%% release docker image based on Ubuntu 24.04 LTS (Noble Numbat). - `:v%%VERSION%%-jammy` - Playwright v%%VERSION%% release docker image based on Ubuntu 22.04 LTS (Jammy Jellyfish). -- `:v%%VERSION%%-focal` - Playwright v%%VERSION%% release docker image based on Ubuntu 20.04 LTS (Focal Fossa). :::note It is recommended to always pin your Docker image to a specific version if possible. If the Playwright version in your Docker image does not match the version in your project/tests, Playwright will be unable to locate browser executables. @@ -122,7 +121,6 @@ It is recommended to always pin your Docker image to a specific version if possi We currently publish images based on the following [Ubuntu](https://hub.docker.com/_/ubuntu) versions: - **Ubuntu 24.04 LTS** (Noble Numbat), image tags include `noble` - **Ubuntu 22.04 LTS** (Jammy Jellyfish), image tags include `jammy` -- **Ubuntu 20.04 LTS** (Focal Fossa), image tags include `focal` #### Alpine diff --git a/utils/docker/Dockerfile.focal b/utils/docker/Dockerfile.focal deleted file mode 100644 index cd1d1d7c6e..0000000000 --- a/utils/docker/Dockerfile.focal +++ /dev/null @@ -1,51 +0,0 @@ -FROM ubuntu:focal - -ARG DEBIAN_FRONTEND=noninteractive -ARG TZ=America/Los_Angeles -ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright:v%version%-focal" - -ENV LANG=C.UTF-8 -ENV LC_ALL=C.UTF-8 - -# === INSTALL Node.js === - -RUN apt-get update && \ - # Install Node.js - apt-get install -y curl wget gpg ca-certificates && \ - mkdir -p /etc/apt/keyrings && \ - curl -sL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ - echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list && \ - apt-get update && \ - apt-get install -y nodejs && \ - # Feature-parity with node.js base images. - apt-get install -y --no-install-recommends git openssh-client && \ - npm install -g yarn && \ - # clean apt cache - rm -rf /var/lib/apt/lists/* && \ - # Create the pwuser - adduser pwuser - -# === BAKE BROWSERS INTO IMAGE === - -ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright - -# 1. Add tip-of-tree Playwright package to install its browsers. -# The package should be built beforehand from tip-of-tree Playwright. -COPY ./playwright-core.tar.gz /tmp/playwright-core.tar.gz - -# 2. Bake in Playwright Agent. -# Playwright Agent is used to bake in browsers and browser dependencies, -# and run docker server later on. -# Browsers will be downloaded in `/ms-playwright`. -# Note: make sure to set 777 to the registry so that any user can access -# registry. -RUN mkdir /ms-playwright && \ - mkdir /ms-playwright-agent && \ - cd /ms-playwright-agent && npm init -y && \ - npm i /tmp/playwright-core.tar.gz && \ - npm exec --no -- playwright-core mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ - npm exec --no -- playwright-core install --with-deps && rm -rf /var/lib/apt/lists/* && \ - rm /tmp/playwright-core.tar.gz && \ - rm -rf /ms-playwright-agent && \ - rm -rf ~/.npm/ && \ - chmod -R 777 /ms-playwright diff --git a/utils/docker/build.sh b/utils/docker/build.sh index 46e8d9e6b6..280727eac5 100755 --- a/utils/docker/build.sh +++ b/utils/docker/build.sh @@ -3,12 +3,12 @@ set -e set +x if [[ ($1 == '--help') || ($1 == '-h') || ($1 == '') || ($2 == '') ]]; then - echo "usage: $(basename $0) {--arm64,--amd64} {focal,jammy} playwright:localbuild-focal" + echo "usage: $(basename $0) {--arm64,--amd64} {jammy,noble} playwright:localbuild-noble" echo - echo "Build Playwright docker image and tag it as 'playwright:localbuild-focal'." + echo "Build Playwright docker image and tag it as 'playwright:localbuild-noble'." echo "Once image is built, you can run it with" echo "" - echo " docker run --rm -it playwright:localbuild-focal /bin/bash" + echo " docker run --rm -it playwright:localbuild-noble /bin/bash" echo "" echo "NOTE: this requires on Playwright dependencies to be installed with 'npm install'" echo " and Playwright itself being built with 'npm run build'" diff --git a/utils/docker/publish_docker.sh b/utils/docker/publish_docker.sh index a4892024a8..870da29a90 100755 --- a/utils/docker/publish_docker.sh +++ b/utils/docker/publish_docker.sh @@ -21,11 +21,6 @@ else exit 1 fi -# Ubuntu 20.04 -FOCAL_TAGS=( - "v${PW_VERSION}-focal" -) - # Ubuntu 22.04 JAMMY_TAGS=( "v${PW_VERSION}-jammy" @@ -69,14 +64,12 @@ install_oras_if_needed() { publish_docker_images_with_arch_suffix() { local FLAVOR="$1" local TAGS=() - if [[ "$FLAVOR" == "focal" ]]; then - TAGS=("${FOCAL_TAGS[@]}") - elif [[ "$FLAVOR" == "jammy" ]]; then + if [[ "$FLAVOR" == "jammy" ]]; then TAGS=("${JAMMY_TAGS[@]}") elif [[ "$FLAVOR" == "noble" ]]; then TAGS=("${NOBLE_TAGS[@]}") else - echo "ERROR: unknown flavor - $FLAVOR. Must be either 'focal', 'jammy', or 'noble'" + echo "ERROR: unknown flavor - $FLAVOR. Must be either 'jammy', or 'noble'" exit 1 fi local ARCH="$2" @@ -97,14 +90,12 @@ publish_docker_images_with_arch_suffix() { publish_docker_manifest () { local FLAVOR="$1" local TAGS=() - if [[ "$FLAVOR" == "focal" ]]; then - TAGS=("${FOCAL_TAGS[@]}") - elif [[ "$FLAVOR" == "jammy" ]]; then + if [[ "$FLAVOR" == "jammy" ]]; then TAGS=("${JAMMY_TAGS[@]}") elif [[ "$FLAVOR" == "noble" ]]; then TAGS=("${NOBLE_TAGS[@]}") else - echo "ERROR: unknown flavor - $FLAVOR. Must be either 'focal', 'jammy', or 'noble'" + echo "ERROR: unknown flavor - $FLAVOR. Must be either 'jammy', or 'noble'" exit 1 fi @@ -123,11 +114,6 @@ publish_docker_manifest () { done } -# Ubuntu 20.04 -publish_docker_images_with_arch_suffix focal amd64 -publish_docker_images_with_arch_suffix focal arm64 -publish_docker_manifest focal amd64 arm64 - # Ubuntu 22.04 publish_docker_images_with_arch_suffix jammy amd64 publish_docker_images_with_arch_suffix jammy arm64 From 36d3a6764ea8bcb42e29e84d00e4106b0d3f2827 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 21 Oct 2024 18:41:27 +0200 Subject: [PATCH 2/6] docs: set minimal Ubuntu version to 22 and Debian to 12 (#33207) --- docs/src/intro-csharp.md | 4 ++-- docs/src/intro-java.md | 4 ++-- docs/src/intro-js.md | 4 ++-- docs/src/intro-python.md | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/src/intro-csharp.md b/docs/src/intro-csharp.md index d26874d456..e0491aa3cc 100644 --- a/docs/src/intro-csharp.md +++ b/docs/src/intro-csharp.md @@ -180,8 +180,8 @@ See our doc on [Running and Debugging Tests](./running-tests.md) to learn more a - Playwright is distributed as a .NET Standard 2.0 library. We recommend .NET 8. - Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). -- macOS 13 Ventura, or macOS 14 Sonoma. -- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. +- macOS 13 Ventura, or later. +- Debian 12, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. ## What's next diff --git a/docs/src/intro-java.md b/docs/src/intro-java.md index 4e1503f2a3..733fee7fdc 100644 --- a/docs/src/intro-java.md +++ b/docs/src/intro-java.md @@ -130,8 +130,8 @@ By default browsers launched with Playwright run headless, meaning no browser UI - Java 8 or higher. - Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). -- macOS 13 Ventura, or macOS 14 Sonoma. -- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. +- macOS 13 Ventura, or later. +- Debian 12, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. ## What's next diff --git a/docs/src/intro-js.md b/docs/src/intro-js.md index 09c070aecf..8c29641bd9 100644 --- a/docs/src/intro-js.md +++ b/docs/src/intro-js.md @@ -288,8 +288,8 @@ pnpm exec playwright --version - Node.js 18+ - Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). -- macOS 13 Ventura, or macOS 14 Sonoma. -- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. +- macOS 13 Ventura, or later. +- Debian 12, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. ## What's next diff --git a/docs/src/intro-python.md b/docs/src/intro-python.md index 3d17c2077d..e44f8a2642 100644 --- a/docs/src/intro-python.md +++ b/docs/src/intro-python.md @@ -101,8 +101,8 @@ pip install pytest-playwright playwright -U - Python 3.8 or higher. - Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). -- macOS 13 Ventura, or macOS 14 Sonoma. -- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. +- macOS 13 Ventura, or later. +- Debian 12, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. ## What's next From aebceb345e7849804752842c57ce02af130e202f Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 21 Oct 2024 11:15:55 -0700 Subject: [PATCH 3/6] chore: expose expect error details on TestError (#33183) --- docs/src/test-reporter-api/class-testerror.md | 36 +++++++++++ .../playwright/src/matchers/matcherHint.ts | 4 ++ .../playwright/src/matchers/toBeTruthy.ts | 29 +++++++-- packages/playwright/src/matchers/toEqual.ts | 46 +++++++++----- .../src/matchers/toMatchSnapshot.ts | 4 ++ .../playwright/src/matchers/toMatchText.ts | 62 ++++++++++++++----- packages/playwright/src/reporters/base.ts | 14 +++++ packages/playwright/src/worker/testInfo.ts | 7 ++- packages/playwright/src/worker/util.ts | 45 ++++++++++++++ packages/playwright/src/worker/workerMain.ts | 9 +-- packages/playwright/types/testReporter.d.ts | 30 +++++++++ tests/page/expect-matcher-result.spec.ts | 30 +++++++++ 12 files changed, 274 insertions(+), 42 deletions(-) create mode 100644 packages/playwright/src/worker/util.ts diff --git a/docs/src/test-reporter-api/class-testerror.md b/docs/src/test-reporter-api/class-testerror.md index 7a872c63fc..8e76d6f595 100644 --- a/docs/src/test-reporter-api/class-testerror.md +++ b/docs/src/test-reporter-api/class-testerror.md @@ -4,18 +4,54 @@ Information about an error thrown during test execution. +## property: TestError.expected +* since: v1.49 +- type: ?<[string]> + +Expected value formatted as a human-readable string. + +## property: TestError.locator +* since: v1.49 +- type: ?<[string]> + +Receiver's locator. + +## property: TestError.log +* since: v1.49 +- type: ?<[Array]<[string]>> + +Call log. + +## property: TestError.matcherName +* since: v1.49 +- type: ?<[string]> + +Expect matcher name. + ## property: TestError.message * since: v1.10 - type: ?<[string]> Error message. Set when [Error] (or its subclass) has been thrown. +## property: TestError.received +* since: v1.49 +- type: ?<[string]> + +Received value formatted as a human-readable string. + ## property: TestError.stack * since: v1.10 - type: ?<[string]> Error stack. Set when [Error] (or its subclass) has been thrown. +## property: TestError.timeout +* since: v1.49 +- type: ?<[int]> + +Timeout in milliseconds, if the error was caused by a timeout. + ## property: TestError.value * since: v1.10 - type: ?<[string]> diff --git a/packages/playwright/src/matchers/matcherHint.ts b/packages/playwright/src/matchers/matcherHint.ts index 8a78932c68..5ffc745263 100644 --- a/packages/playwright/src/matchers/matcherHint.ts +++ b/packages/playwright/src/matchers/matcherHint.ts @@ -39,6 +39,10 @@ export type MatcherResult = { actual?: A; log?: string[]; timeout?: number; + locator?: string; + printedReceived?: string; + printedExpected?: string; + printedDiff?: string; }; export class ExpectError extends Error { diff --git a/packages/playwright/src/matchers/toBeTruthy.ts b/packages/playwright/src/matchers/toBeTruthy.ts index 0941ab7a63..8902a14eea 100644 --- a/packages/playwright/src/matchers/toBeTruthy.ts +++ b/packages/playwright/src/matchers/toBeTruthy.ts @@ -39,22 +39,41 @@ export async function toBeTruthy( }; const timeout = options.timeout ?? this.timeout; - const { matches, log, timedOut, received } = await query(!!this.isNot, timeout); + const { matches: pass, log, timedOut, received } = await query(!!this.isNot, timeout); + if (pass === !this.isNot) { + return { + name: matcherName, + message: () => '', + pass, + expected + }; + } + const notFound = received === kNoElementsFoundError ? received : undefined; - const actual = matches ? expected : unexpected; + const actual = pass ? expected : unexpected; + let printedReceived: string | undefined; + let printedExpected: string | undefined; + if (pass) { + printedExpected = `Expected: not ${expected}`; + printedReceived = `Received: ${notFound ? kNoElementsFoundError : expected}`; + } else { + printedExpected = `Expected: ${expected}`; + printedReceived = `Received: ${notFound ? kNoElementsFoundError : unexpected}`; + } const message = () => { const header = matcherHint(this, receiver, matcherName, 'locator', arg, matcherOptions, timedOut ? timeout : undefined); const logText = callLogText(log); - return matches ? `${header}Expected: not ${expected}\nReceived: ${notFound ? kNoElementsFoundError : expected}${logText}` : - `${header}Expected: ${expected}\nReceived: ${notFound ? kNoElementsFoundError : unexpected}${logText}`; + return `${header}${printedExpected}\n${printedReceived}${logText}`; }; return { message, - pass: matches, + pass, actual, name: matcherName, expected, log, timeout: timedOut ? timeout : undefined, + ...(printedReceived ? { printedReceived } : {}), + ...(printedExpected ? { printedExpected } : {}), }; } diff --git a/packages/playwright/src/matchers/toEqual.ts b/packages/playwright/src/matchers/toEqual.ts index 29d3fd4866..f75caf87f5 100644 --- a/packages/playwright/src/matchers/toEqual.ts +++ b/packages/playwright/src/matchers/toEqual.ts @@ -44,22 +44,35 @@ export async function toEqual( const timeout = options.timeout ?? this.timeout; const { matches: pass, received, log, timedOut } = await query(!!this.isNot, timeout); + if (pass === !this.isNot) { + return { + name: matcherName, + message: () => '', + pass, + expected + }; + } - const message = pass - ? () => - matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined) + - `Expected: not ${this.utils.printExpected(expected)}\n` + - `Received: ${this.utils.printReceived(received)}` + callLogText(log) - : () => - matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined) + - this.utils.printDiffOrStringify( - expected, - received, - EXPECTED_LABEL, - RECEIVED_LABEL, - false, - ) + callLogText(log); - + let printedReceived: string | undefined; + let printedExpected: string | undefined; + let printedDiff: string | undefined; + if (pass) { + printedExpected = `Expected: not ${this.utils.printExpected(expected)}`; + printedReceived = `Received: ${this.utils.printReceived(received)}`; + } else { + printedDiff = this.utils.printDiffOrStringify( + expected, + received, + EXPECTED_LABEL, + RECEIVED_LABEL, + false, + ); + } + const message = () => { + const header = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined); + const details = printedDiff || `${printedExpected}\n${printedReceived}`; + return `${header}${details}${callLogText(log)}`; + }; // Passing the actual and expected objects so that a custom reporter // could access them, for example in order to display a custom visual diff, // or create a different error message @@ -70,5 +83,8 @@ export async function toEqual( pass, log, timeout: timedOut ? timeout : undefined, + ...(printedReceived ? { printedReceived } : {}), + ...(printedExpected ? { printedExpected } : {}), + ...(printedDiff ? { printedDiff } : {}), }; } diff --git a/packages/playwright/src/matchers/toMatchSnapshot.ts b/packages/playwright/src/matchers/toMatchSnapshot.ts index b3fa3f556e..86504062d3 100644 --- a/packages/playwright/src/matchers/toMatchSnapshot.ts +++ b/packages/playwright/src/matchers/toMatchSnapshot.ts @@ -194,6 +194,10 @@ class SnapshotHelper { pass, message: () => message, log, + // eslint-disable-next-line @typescript-eslint/no-base-to-string + ...(this.locator ? { locator: this.locator.toString() } : {}), + printedExpected: this.expectedPath, + printedReceived: this.actualPath, }; return Object.fromEntries(Object.entries(unfiltered).filter(([_, v]) => v !== undefined)) as ImageMatcherResult; } diff --git a/packages/playwright/src/matchers/toMatchText.ts b/packages/playwright/src/matchers/toMatchText.ts index ebac8f8028..76ed48af1e 100644 --- a/packages/playwright/src/matchers/toMatchText.ts +++ b/packages/playwright/src/matchers/toMatchText.ts @@ -58,29 +58,56 @@ export async function toMatchText( const timeout = options.timeout ?? this.timeout; const { matches: pass, received, log, timedOut } = await query(!!this.isNot, timeout); + if (pass === !this.isNot) { + return { + name: matcherName, + message: () => '', + pass, + expected + }; + } + const stringSubstring = options.matchSubstring ? 'substring' : 'string'; const receivedString = received || ''; const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined); const notFound = received === kNoElementsFoundError; - const message = () => { - if (pass) { - if (typeof expected === 'string') { - if (notFound) - return messagePrefix + `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log); - const printedReceived = printReceivedStringContainExpectedSubstring(receivedString, receivedString.indexOf(expected), expected.length); - return messagePrefix + `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}\nReceived string: ${printedReceived}` + callLogText(log); + + let printedReceived: string | undefined; + let printedExpected: string | undefined; + let printedDiff: string | undefined; + if (pass) { + if (typeof expected === 'string') { + if (notFound) { + printedExpected = `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}`; + printedReceived = `Received: ${received}`; } else { - if (notFound) - return messagePrefix + `Expected pattern: not ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log); - const printedReceived = printReceivedStringContainExpectedResult(receivedString, typeof expected.exec === 'function' ? expected.exec(receivedString) : null); - return messagePrefix + `Expected pattern: not ${this.utils.printExpected(expected)}\nReceived string: ${printedReceived}` + callLogText(log); + printedExpected = `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}`; + const formattedReceived = printReceivedStringContainExpectedSubstring(receivedString, receivedString.indexOf(expected), expected.length); + printedReceived = `Received string: ${formattedReceived}`; } } else { - const labelExpected = `Expected ${typeof expected === 'string' ? stringSubstring : 'pattern'}`; - if (notFound) - return messagePrefix + `${labelExpected}: ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log); - return messagePrefix + this.utils.printDiffOrStringify(expected, receivedString, labelExpected, 'Received string', false) + callLogText(log); + if (notFound) { + printedExpected = `Expected pattern: not ${this.utils.printExpected(expected)}`; + printedReceived = `Received: ${received}`; + } else { + printedExpected = `Expected pattern: not ${this.utils.printExpected(expected)}`; + const formattedReceived = printReceivedStringContainExpectedResult(receivedString, typeof expected.exec === 'function' ? expected.exec(receivedString) : null); + printedReceived = `Received string: ${formattedReceived}`; + } } + } else { + const labelExpected = `Expected ${typeof expected === 'string' ? stringSubstring : 'pattern'}`; + if (notFound) { + printedExpected = `${labelExpected}: ${this.utils.printExpected(expected)}`; + printedReceived = `Received: ${received}`; + } else { + printedDiff = this.utils.printDiffOrStringify(expected, receivedString, labelExpected, 'Received string', false); + } + } + + const message = () => { + const resultDetails = printedDiff ? printedDiff : printedExpected + '\n' + printedReceived; + return messagePrefix + resultDetails + callLogText(log); }; return { @@ -91,5 +118,10 @@ export async function toMatchText( actual: received, log, timeout: timedOut ? timeout : undefined, + // eslint-disable-next-line @typescript-eslint/no-base-to-string + locator: receiver.toString(), + ...(printedReceived ? { printedReceived } : {}), + ...(printedExpected ? { printedExpected } : {}), + ...(printedDiff ? { printedDiff } : {}), }; } diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts index 4249429a36..138820baee 100644 --- a/packages/playwright/src/reporters/base.ts +++ b/packages/playwright/src/reporters/base.ts @@ -32,6 +32,13 @@ type Annotation = { type ErrorDetails = { message: string; location?: Location; + timeout?: number; + matcherName?: string; + locator?: string; + expected?: string; + received?: string; + log?: string[]; + snippet?: string; }; type TestSummary = { @@ -383,6 +390,13 @@ export function formatResultFailure(test: TestCase, result: TestResult, initialI errorDetails.push({ message: indent(formattedError.message, initialIndent), location: formattedError.location, + timeout: error.timeout, + matcherName: error.matcherName, + locator: error.locator, + expected: error.expected, + received: error.received, + log: error.log, + snippet: error.snippet, }); } return errorDetails; diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index 378b32524f..e41f1a9a52 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -24,10 +24,11 @@ import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutMana import type { RunnableDescription } from './timeoutManager'; import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config'; import type { FullConfig, Location } from '../../types/testReporter'; -import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, serializeError, trimLongString, windowsFilesystemFriendlyLength } from '../util'; +import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, trimLongString, windowsFilesystemFriendlyLength } from '../util'; import { TestTracing } from './testTracing'; import type { Attachment } from './testTracing'; import type { StackFrame } from '@protocol/channels'; +import { serializeWorkerError } from './util'; export interface TestStepInternal { complete(result: { error?: Error | unknown, attachments?: Attachment[] }): void; @@ -272,7 +273,7 @@ export class TestInfoImpl implements TestInfo { if (result.error) { if (typeof result.error === 'object' && !(result.error as any)?.[stepSymbol]) (result.error as any)[stepSymbol] = step; - const error = serializeError(result.error); + const error = serializeWorkerError(result.error); if (data.boxedStack) error.stack = `${error.message}\n${stringifyStackFrames(data.boxedStack).join('\n')}`; step.error = error; @@ -330,7 +331,7 @@ export class TestInfoImpl implements TestInfo { _failWithError(error: Error | unknown) { if (this.status === 'passed' || this.status === 'skipped') this.status = error instanceof TimeoutManagerError ? 'timedOut' : 'failed'; - const serialized = serializeError(error); + const serialized = serializeWorkerError(error); const step: TestStepInternal | undefined = typeof error === 'object' ? (error as any)?.[stepSymbol] : undefined; if (step && step.boxedStack) serialized.stack = `${(error as Error).name}: ${(error as Error).message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`; diff --git a/packages/playwright/src/worker/util.ts b/packages/playwright/src/worker/util.ts new file mode 100644 index 0000000000..d24d337191 --- /dev/null +++ b/packages/playwright/src/worker/util.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { TestError } from '../../types/testReporter'; +import type { TestInfoError } from '../../types/test'; +import type { MatcherResult } from '../matchers/matcherHint'; +import { serializeError } from '../util'; + + +type MatcherResultDetails = Pick; + +export function serializeWorkerError(error: Error | any): TestInfoError & MatcherResultDetails { + return { + ...serializeError(error), + ...serializeExpectDetails(error), + }; +} + +function serializeExpectDetails(e: Error): MatcherResultDetails { + const matcherResult = (e as any).matcherResult as MatcherResult; + if (!matcherResult) + return {}; + return { + timeout: matcherResult.timeout, + matcherName: matcherResult.name, + locator: matcherResult.locator, + expected: matcherResult.printedExpected, + received: matcherResult.printedReceived, + log: matcherResult.log, + }; +} + diff --git a/packages/playwright/src/worker/workerMain.ts b/packages/playwright/src/worker/workerMain.ts index f180f3d08b..5680c3ddb3 100644 --- a/packages/playwright/src/worker/workerMain.ts +++ b/packages/playwright/src/worker/workerMain.ts @@ -15,7 +15,7 @@ */ import { colors } from 'playwright-core/lib/utilsBundle'; -import { debugTest, relativeFilePath, serializeError } from '../util'; +import { debugTest, relativeFilePath } from '../util'; import { type TestBeginPayload, type TestEndPayload, type RunPayload, type DonePayload, type WorkerInitParams, type TeardownErrorsPayload, stdioChunkToParams } from '../common/ipc'; import { setCurrentTestInfo, setIsWorkerProcess } from '../common/globals'; import { deserializeConfig } from '../common/configLoader'; @@ -32,6 +32,7 @@ import type { TestInfoError } from '../../types/test'; import type { Location } from '../../types/testReporter'; import { inheritFixtureNames } from '../common/fixtures'; import { type TimeSlot } from './timeoutManager'; +import { serializeWorkerError } from './util'; export class WorkerMain extends ProcessRunner { private _params: WorkerInitParams; @@ -112,7 +113,7 @@ export class WorkerMain extends ProcessRunner { await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => gracefullyCloseAll()).catch(() => {}); this._fatalErrors.push(...fakeTestInfo.errors); } catch (e) { - this._fatalErrors.push(serializeError(e)); + this._fatalErrors.push(serializeWorkerError(e)); } if (this._fatalErrors.length) { @@ -153,7 +154,7 @@ export class WorkerMain extends ProcessRunner { // No current test - fatal error. if (!this._currentTest) { if (!this._fatalErrors.length) - this._fatalErrors.push(serializeError(error)); + this._fatalErrors.push(serializeWorkerError(error)); void this._stop(); return; } @@ -224,7 +225,7 @@ export class WorkerMain extends ProcessRunner { // In theory, we should run above code without any errors. // However, in the case we screwed up, or loadTestFile failed in the worker // but not in the runner, let's do a fatal error. - this._fatalErrors.push(serializeError(e)); + this._fatalErrors.push(serializeWorkerError(e)); void this._stop(); } finally { const donePayload: DonePayload = { diff --git a/packages/playwright/types/testReporter.d.ts b/packages/playwright/types/testReporter.d.ts index a9d1f020ae..5663f92f98 100644 --- a/packages/playwright/types/testReporter.d.ts +++ b/packages/playwright/types/testReporter.d.ts @@ -554,16 +554,41 @@ export interface TestCase { * Information about an error thrown during test execution. */ export interface TestError { + /** + * Expected value formatted as a human-readable string. + */ + expected?: string; + /** * Error location in the source code. */ location?: Location; + /** + * Receiver's locator. + */ + locator?: string; + + /** + * Call log. + */ + log?: Array; + + /** + * Expect matcher name. + */ + matcherName?: string; + /** * Error message. Set when [Error] (or its subclass) has been thrown. */ message?: string; + /** + * Received value formatted as a human-readable string. + */ + received?: string; + /** * Source code snippet with highlighted error. */ @@ -574,6 +599,11 @@ export interface TestError { */ stack?: string; + /** + * Timeout in milliseconds, if the error was caused by a timeout. + */ + timeout?: number; + /** * The value that was thrown. Set when anything except the [Error] (or its subclass) has been thrown. */ diff --git a/tests/page/expect-matcher-result.spec.ts b/tests/page/expect-matcher-result.spec.ts index 8f8a83bc83..7767ecf5f6 100644 --- a/tests/page/expect-matcher-result.spec.ts +++ b/tests/page/expect-matcher-result.spec.ts @@ -24,12 +24,16 @@ test('toMatchText-based assertions should have matcher result', async ({ page }) { const e = await expect(locator).toHaveText(/Text2/, { timeout: 1 }).catch(e => e); e.matcherResult.message = stripAnsi(e.matcherResult.message); + e.matcherResult.printedDiff = stripAnsi(e.matcherResult.printedDiff); expect.soft(e.matcherResult).toEqual({ actual: 'Text content', expected: /Text2/, message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toHaveText(expected)`), name: 'toHaveText', pass: false, + locator: `locator('#node')`, + printedDiff: `Expected pattern: /Text2/ +Received string: \"Text content\"`, log: expect.any(Array), timeout: 1, }); @@ -46,12 +50,17 @@ Call log`); { const e = await expect(locator).not.toHaveText(/Text/, { timeout: 1 }).catch(e => e); e.matcherResult.message = stripAnsi(e.matcherResult.message); + e.matcherResult.printedExpected = stripAnsi(e.matcherResult.printedExpected); + e.matcherResult.printedReceived = stripAnsi(e.matcherResult.printedReceived); expect.soft(e.matcherResult).toEqual({ actual: 'Text content', expected: /Text/, message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toHaveText(expected)`), name: 'toHaveText', pass: true, + locator: `locator('#node')`, + printedExpected: 'Expected pattern: not /Text/', + printedReceived: `Received string: \"Text content\"`, log: expect.any(Array), timeout: 1, }); @@ -79,6 +88,8 @@ test('toBeTruthy-based assertions should have matcher result', async ({ page }) name: 'toBeVisible', pass: false, log: expect.any(Array), + printedExpected: 'Expected: visible', + printedReceived: 'Received: ', timeout: 1, }); @@ -101,6 +112,8 @@ Call log`); name: 'toBeVisible', pass: true, log: expect.any(Array), + printedExpected: 'Expected: not visible', + printedReceived: 'Received: visible', timeout: 1, }); @@ -120,6 +133,7 @@ test('toEqual-based assertions should have matcher result', async ({ page }) => { const e = await expect(page.locator('#node2')).toHaveCount(1, { timeout: 1 }).catch(e => e); e.matcherResult.message = stripAnsi(e.matcherResult.message); + e.matcherResult.printedDiff = stripAnsi(e.matcherResult.printedDiff); expect.soft(e.matcherResult).toEqual({ actual: 0, expected: 1, @@ -127,6 +141,8 @@ test('toEqual-based assertions should have matcher result', async ({ page }) => name: 'toHaveCount', pass: false, log: expect.any(Array), + printedDiff: `Expected: 1 +Received: 0`, timeout: 1, }); @@ -141,6 +157,8 @@ Call log`); { const e = await expect(page.locator('#node')).not.toHaveCount(1, { timeout: 1 }).catch(e => e); e.matcherResult.message = stripAnsi(e.matcherResult.message); + e.matcherResult.printedExpected = stripAnsi(e.matcherResult.printedExpected); + e.matcherResult.printedReceived = stripAnsi(e.matcherResult.printedReceived); expect.soft(e.matcherResult).toEqual({ actual: 1, expected: 1, @@ -148,6 +166,8 @@ Call log`); name: 'toHaveCount', pass: true, log: expect.any(Array), + printedExpected: `Expected: not 1`, + printedReceived: `Received: 1`, timeout: 1, }); @@ -177,6 +197,8 @@ test('toBeChecked({ checked: false }) should have expected: false', async ({ pag name: 'toBeChecked', pass: false, log: expect.any(Array), + printedExpected: 'Expected: checked', + printedReceived: 'Received: unchecked', timeout: 1, }); @@ -199,6 +221,8 @@ Call log`); name: 'toBeChecked', pass: true, log: expect.any(Array), + printedExpected: 'Expected: not checked', + printedReceived: 'Received: checked', timeout: 1, }); @@ -221,6 +245,8 @@ Call log`); name: 'toBeChecked', pass: false, log: expect.any(Array), + printedExpected: 'Expected: unchecked', + printedReceived: 'Received: checked', timeout: 1, }); @@ -243,6 +269,8 @@ Call log`); name: 'toBeChecked', pass: true, log: expect.any(Array), + printedExpected: 'Expected: not unchecked', + printedReceived: 'Received: unchecked', timeout: 1, }); @@ -271,6 +299,8 @@ test('toHaveScreenshot should populate matcherResult', async ({ page, server, is name: 'toHaveScreenshot', pass: false, log: expect.any(Array), + printedExpected: expect.stringContaining('screenshot-sanity-'), + printedReceived: expect.stringContaining('screenshot-sanity-actual'), }); expect.soft(stripAnsi(e.toString())).toContain(`Error: Screenshot comparison failed: From 0351fd9401a70a6cd300329e7fa1869449cf0a03 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 21 Oct 2024 21:21:30 +0200 Subject: [PATCH 4/6] docs: use WebSocketFrame abstraction for Java & .NET (#33211) --- docs/src/api/class-page.md | 8 ++--- docs/src/api/class-websocketroute.md | 52 ++++++++++++++-------------- docs/src/mock.md | 20 +++++------ docs/src/release-notes-csharp.md | 4 +-- docs/src/release-notes-java.md | 4 +-- 5 files changed, 44 insertions(+), 44 deletions(-) diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 373649154d..f65a904932 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -3691,8 +3691,8 @@ await page.routeWebSocket('/ws', ws => { ```java page.routeWebSocket("/ws", ws -> { - ws.onMessage(message -> { - if ("request".equals(message)) + ws.onMessage(frame -> { + if ("request".equals(frame.text())) ws.send("response"); }); }); @@ -3722,8 +3722,8 @@ page.route_web_socket("/ws", handler) ```csharp await page.RouteWebSocketAsync("/ws", ws => { - ws.OnMessage(message => { - if (message == "request") + ws.OnMessage(frame => { + if (frame.Text == "request") ws.Send("response"); }); }); diff --git a/docs/src/api/class-websocketroute.md b/docs/src/api/class-websocketroute.md index b827db25dd..e23316ebcb 100644 --- a/docs/src/api/class-websocketroute.md +++ b/docs/src/api/class-websocketroute.md @@ -18,8 +18,8 @@ await page.routeWebSocket('wss://example.com/ws', ws => { ```java page.routeWebSocket("wss://example.com/ws", ws -> { - ws.onMessage(message -> { - if ("request".equals(message)) + ws.onMessage(frame -> { + if ("request".equals(frame.text())) ws.send("response"); }); }); @@ -47,8 +47,8 @@ page.route_web_socket("wss://example.com/ws", lambda ws: ws.on_message( ```csharp await page.RouteWebSocketAsync("wss://example.com/ws", ws => { - ws.OnMessage(message => { - if (message == "request") + ws.OnMessage(frame => { + if (frame.Text == "request") ws.Send("response"); }); }); @@ -70,8 +70,8 @@ await page.routeWebSocket('wss://example.com/ws', ws => { ```java page.routeWebSocket("wss://example.com/ws", ws -> { - ws.onMessage(message -> { - JsonObject json = new JsonParser().parse(message).getAsJsonObject(); + ws.onMessage(frame -> { + JsonObject json = new JsonParser().parse(frame.text()).getAsJsonObject(); if ("question".equals(json.get("request").getAsString())) { Map result = new HashMap(); result.put("response", "answer"); @@ -105,8 +105,8 @@ page.route_web_socket("wss://example.com/ws", lambda ws: ws.on_message( ```csharp await page.RouteWebSocketAsync("wss://example.com/ws", ws => { - ws.OnMessage(message => { - using var jsonDoc = JsonDocument.Parse(message); + ws.OnMessage(frame => { + using var jsonDoc = JsonDocument.Parse(frame.Text); JsonElement root = jsonDoc.RootElement; if (root.TryGetProperty("request", out JsonElement requestElement) && requestElement.GetString() == "question") { @@ -140,11 +140,11 @@ await page.routeWebSocket('/ws', ws => { ```java page.routeWebSocket("/ws", ws -> { WebSocketRoute server = ws.connectToServer(); - ws.onMessage(message -> { - if ("request".equals(message)) + ws.onMessage(frame -> { + if ("request".equals(frame.text())) server.send("request2"); else - server.send(message); + server.send(frame.text()); }); }); ``` @@ -180,11 +180,11 @@ page.route_web_socket("/ws", handler) ```csharp await page.RouteWebSocketAsync("/ws", ws => { var server = ws.ConnectToServer(); - ws.OnMessage(message => { - if (message == "request") + ws.OnMessage(frame => { + if (frame.Text == "request") server.Send("request2"); else - server.Send(message); + server.Send(frame.Text); }); }); ``` @@ -215,13 +215,13 @@ await page.routeWebSocket('/ws', ws => { ```java page.routeWebSocket("/ws", ws -> { WebSocketRoute server = ws.connectToServer(); - ws.onMessage(message -> { - if (!"blocked-from-the-page".equals(message)) - server.send(message); + ws.onMessage(frame -> { + if (!"blocked-from-the-page".equals(frame.text())) + server.send(frame.text()); }); - server.onMessage(message -> { - if (!"blocked-from-the-server".equals(message)) - ws.send(message); + server.onMessage(frame -> { + if (!"blocked-from-the-server".equals(frame.text())) + ws.send(frame.text()); }); }); ``` @@ -263,13 +263,13 @@ page.route_web_socket("/ws", handler) ```csharp await page.RouteWebSocketAsync("/ws", ws => { var server = ws.ConnectToServer(); - ws.OnMessage(message => { - if (message != "blocked-from-the-page") - server.Send(message); + ws.OnMessage(frame => { + if (frame.Text != "blocked-from-the-page") + server.Send(frame.Text); }); - server.OnMessage(message => { - if (message != "blocked-from-the-server") - ws.Send(message); + server.OnMessage(frame => { + if (frame.Text != "blocked-from-the-server") + ws.Send(frame.Text); }); }); ``` diff --git a/docs/src/mock.md b/docs/src/mock.md index 468690904a..50bc3915ce 100644 --- a/docs/src/mock.md +++ b/docs/src/mock.md @@ -451,8 +451,8 @@ await page.routeWebSocket('wss://example.com/ws', ws => { ```java page.routeWebSocket("wss://example.com/ws", ws -> { - ws.onMessage(message -> { - if ("request".equals(message)) + ws.onMessage(frame -> { + if ("request".equals(frame.text())) ws.send("response"); }); }); @@ -480,8 +480,8 @@ page.route_web_socket("wss://example.com/ws", lambda ws: ws.on_message( ```csharp await page.RouteWebSocketAsync("wss://example.com/ws", ws => { - ws.OnMessage(message => { - if (message == "request") + ws.OnMessage(frame => { + if (frame.Text == "request") ws.Send("response"); }); }); @@ -504,11 +504,11 @@ await page.routeWebSocket('wss://example.com/ws', ws => { ```java page.routeWebSocket("wss://example.com/ws", ws -> { WebSocketRoute server = ws.connectToServer(); - ws.onMessage(message -> { - if ("request".equals(message)) + ws.onMessage(frame -> { + if ("request".equals(frame.text())) server.send("request2"); else - server.send(message); + server.send(frame.text()); }); }); ``` @@ -544,11 +544,11 @@ page.route_web_socket("wss://example.com/ws", handler) ```csharp await page.RouteWebSocketAsync("wss://example.com/ws", ws => { var server = ws.ConnectToServer(); - ws.OnMessage(message => { - if (message == "request") + ws.OnMessage(frame => { + if (frame.Text == "request") server.Send("request2"); else - server.Send(message); + server.Send(frame.Text); }); }); ``` diff --git a/docs/src/release-notes-csharp.md b/docs/src/release-notes-csharp.md index fee7d732fe..130b9d4a71 100644 --- a/docs/src/release-notes-csharp.md +++ b/docs/src/release-notes-csharp.md @@ -13,8 +13,8 @@ New methods [`method: Page.routeWebSocket`] and [`method: BrowserContext.routeWe ```csharp await page.RouteWebSocketAsync("/ws", ws => { - ws.OnMessage(message => { - if (message == "request") + ws.OnMessage(frame => { + if (frame.Text == "request") ws.Send("response"); }); }); diff --git a/docs/src/release-notes-java.md b/docs/src/release-notes-java.md index 908a768357..a5d01668de 100644 --- a/docs/src/release-notes-java.md +++ b/docs/src/release-notes-java.md @@ -12,8 +12,8 @@ New methods [`method: Page.routeWebSocket`] and [`method: BrowserContext.routeWe ```java page.routeWebSocket("/ws", ws -> { - ws.onMessage(message -> { - if ("request".equals(message)) + ws.onMessage(frame -> { + if ("request".equals(frame.text())) ws.send("response"); }); }); From 2a3d67195dff6ece12bd36aab970a1fc95776998 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 21 Oct 2024 21:54:06 -0700 Subject: [PATCH 5/6] chore: use aria snapshots in some ui mode tests (#33212) --- .../src/server/injected/ariaSnapshot.ts | 11 +- .../playwright-core/src/utils/sequence.ts | 63 ++++ .../playwright-core/src/utils/stackTrace.ts | 20 +- .../src/ui/uiModeTestListView.tsx | 9 +- packages/web/src/components/treeView.tsx | 7 +- tests/library/sequence.spec.ts | 157 ++++++++++ .../stable-test-runner/package-lock.json | 46 +-- .../stable-test-runner/package.json | 2 +- .../playwright-test/ui-mode-test-run.spec.ts | 270 ++++++++++++++++-- 9 files changed, 534 insertions(+), 51 deletions(-) create mode 100644 packages/playwright-core/src/utils/sequence.ts create mode 100644 tests/library/sequence.spec.ts diff --git a/packages/playwright-core/src/server/injected/ariaSnapshot.ts b/packages/playwright-core/src/server/injected/ariaSnapshot.ts index 56044a2027..682f48365a 100644 --- a/packages/playwright-core/src/server/injected/ariaSnapshot.ts +++ b/packages/playwright-core/src/server/injected/ariaSnapshot.ts @@ -208,7 +208,7 @@ function matchesText(text: string | undefined, template: RegExp | string | undef export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: boolean, received: string } { const root = generateAriaTree(rootElement); const matches = matchesNodeDeep(root, template); - return { matches, received: renderAriaTree(root) }; + return { matches, received: renderAriaTree(root, { noText: true }) }; } function matchesNode(node: AriaNode | string, template: AriaTemplateNode | RegExp | string, depth: number): boolean { @@ -276,11 +276,12 @@ function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode): boolean { return !!results.length; } -export function renderAriaTree(ariaNode: AriaNode): string { +export function renderAriaTree(ariaNode: AriaNode, options?: { noText?: boolean }): string { const lines: string[] = []; const visit = (ariaNode: AriaNode | string, indent: string) => { if (typeof ariaNode === 'string') { - lines.push(indent + '- text: ' + escapeYamlString(ariaNode)); + if (!options?.noText) + lines.push(indent + '- text: ' + escapeYamlString(ariaNode)); return; } let line = `${indent}- ${ariaNode.role}`; @@ -301,10 +302,12 @@ export function renderAriaTree(ariaNode: AriaNode): string { line += ` [pressed=mixed]`; if (ariaNode.pressed === true) line += ` [pressed]`; + if (ariaNode.selected === true) + line += ` [selected]`; const stringValue = !ariaNode.children.length || (ariaNode.children?.length === 1 && typeof ariaNode.children[0] === 'string'); if (stringValue) { - if (ariaNode.children.length) + if (!options?.noText && ariaNode.children.length) line += ': ' + escapeYamlString(ariaNode.children?.[0] as string); lines.push(line); return; diff --git a/packages/playwright-core/src/utils/sequence.ts b/packages/playwright-core/src/utils/sequence.ts new file mode 100644 index 0000000000..2af5429bd6 --- /dev/null +++ b/packages/playwright-core/src/utils/sequence.ts @@ -0,0 +1,63 @@ +/** + * 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. + */ + +export function findRepeatedSubsequences(s: string[]): { sequence: string[]; count: number }[] { + const n = s.length; + const result = []; + let i = 0; + + const arraysEqual = (a1: string[], a2: string[]) => { + if (a1.length !== a2.length) return false; + for (let j = 0; j < a1.length; j++) { + if (a1[j] !== a2[j]) return false; + } + return true; + }; + + while (i < n) { + let maxRepeatCount = 1; + let maxRepeatSubstr = [s[i]]; // Initialize with the element at index i + let maxRepeatLength = 1; + + // Try substrings of length from 1 to the remaining length of the array + for (let p = 1; p <= n - i; p++) { + const substr = s.slice(i, i + p); // Extract substring as array + let k = 1; + + // Count how many times the substring repeats consecutively + while ( + i + p * k <= n && + arraysEqual(s.slice(i + p * (k - 1), i + p * k), substr) + ) { + k += 1; + } + k -= 1; // Adjust k since it increments one extra time in the loop + + // Update the maximal repeating substring if necessary + if (k > 1 && (k * p) > (maxRepeatCount * maxRepeatLength)) { + maxRepeatCount = k; + maxRepeatSubstr = substr; + maxRepeatLength = p; + } + } + + // Record the substring and its count + result.push({ sequence: maxRepeatSubstr, count: maxRepeatCount }); + i += maxRepeatLength * maxRepeatCount; // Move index forward + } + + return result; +} \ No newline at end of file diff --git a/packages/playwright-core/src/utils/stackTrace.ts b/packages/playwright-core/src/utils/stackTrace.ts index 77e1365b3f..6f9a87578b 100644 --- a/packages/playwright-core/src/utils/stackTrace.ts +++ b/packages/playwright-core/src/utils/stackTrace.ts @@ -19,6 +19,7 @@ import { parseStackTraceLine } from '../utilsBundle'; import { isUnderTest } from './'; import type { StackFrame } from '@protocol/channels'; import { colors } from '../utilsBundle'; +import { findRepeatedSubsequences } from './sequence'; export function rewriteErrorMessage(e: E, newMessage: string): E { const lines: string[] = (e.stack?.split('\n') || []).filter(l => l.startsWith(' at ')); @@ -132,9 +133,26 @@ export function splitErrorMessage(message: string): { name: string, message: str export function formatCallLog(log: string[] | undefined): string { if (!log || !log.some(l => !!l)) return ''; + + const lines: string[] = []; + + for (const block of findRepeatedSubsequences(log)) { + for (let i = 0; i < block.sequence.length; i++) { + const line = block.sequence[i]; + const leadingWhitespace = line.match(/^\s*/); + const whitespacePrefix = ' ' + leadingWhitespace?.[0] || ''; + const countPrefix = `${block.count} × `; + if (block.count > 1 && i === 0) + lines.push(whitespacePrefix + countPrefix + line.trim()); + else if (block.count > 1) + lines.push(whitespacePrefix + ' '.repeat(countPrefix.length - 2) + '- ' + line.trim()); + else + lines.push(whitespacePrefix + '- ' + line.trim()); + } + } return ` Call log: - ${colors.dim('- ' + (log || []).join('\n - '))} +${colors.dim(lines.join('\n'))} `; } diff --git a/packages/trace-viewer/src/ui/uiModeTestListView.tsx b/packages/trace-viewer/src/ui/uiModeTestListView.tsx index e0ef2a8bca..a6cb82fb8a 100644 --- a/packages/trace-viewer/src/ui/uiModeTestListView.tsx +++ b/packages/trace-viewer/src/ui/uiModeTestListView.tsx @@ -159,12 +159,15 @@ export const TestListView: React.FC<{ rootItem={testTree.rootItem} dataTestId='test-tree' render={treeItem => { - return
-
+ const prefixId = treeItem.id.replace(/[^\w\d-_]/g, '-'); + const labelId = prefixId + '-label'; + const timeId = prefixId + '-time'; + return
+
{treeItem.title} {treeItem.kind === 'case' ? treeItem.tags.map(tag => handleTagClick(e, tag)} />) : null}
- {!!treeItem.duration && treeItem.status !== 'skipped' &&
{msToString(treeItem.duration)}
} + {!!treeItem.duration && treeItem.status !== 'skipped' &&
{msToString(treeItem.duration)}
} runTreeItem(treeItem)} disabled={!!runningState && !runningState.completed}> diff --git a/packages/web/src/components/treeView.tsx b/packages/web/src/components/treeView.tsx index 9af8609f3b..cb7ab7150d 100644 --- a/packages/web/src/components/treeView.tsx +++ b/packages/web/src/components/treeView.tsx @@ -249,8 +249,9 @@ export function TreeItemHeader({ const rendered = render(item); const children = expanded && item.children.length ? item.children as T[] : []; const titled = title?.(item); + const iconed = icon?.(item) || 'codicon-blank'; - return
+ return
onAccepted?.(item)} className={clsx( @@ -277,10 +278,10 @@ export function TreeItemHeader({ toggleExpanded(item); }} /> - {icon && } + {icon &&
} {typeof rendered === 'string' ?
{rendered}
: rendered}
- {!!children.length &&
+ {!!children.length &&
{children.map(child => { const itemData = treeItems.get(child); return itemData && { + const input = []; + const expectedOutput = []; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle a single-element array', () => { + const input = ['a']; + const expectedOutput = [{ sequence: ['a'], count: 1 }]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle an array with no repeats', () => { + const input = ['a', 'b', 'c']; + const expectedOutput = [ + { sequence: ['a'], count: 1 }, + { sequence: ['b'], count: 1 }, + { sequence: ['c'], count: 1 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle contiguous repeats of single elements', () => { + const input = ['a', 'a', 'a', 'b', 'b', 'c']; + const expectedOutput = [ + { sequence: ['a'], count: 3 }, + { sequence: ['b'], count: 2 }, + { sequence: ['c'], count: 1 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should detect longer repeating substrings', () => { + const input = ['a', 'b', 'a', 'b', 'a', 'b']; + const expectedOutput = [{ sequence: ['a', 'b'], count: 3 }]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle multiple repeating substrings', () => { + const input = ['a', 'a', 'b', 'b', 'a', 'a', 'b', 'b']; + const expectedOutput = [ + { sequence: ['a', 'a', 'b', 'b'], count: 2 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle complex cases with overlapping repeats', () => { + const input = ['a', 'a', 'a', 'a']; + const expectedOutput = [{ sequence: ['a'], count: 4 }]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle complex acceptance cases with multiple possible repeats', () => { + const input = ['a', 'a', 'b', 'b', 'a', 'a', 'b', 'b', 'c', 'c', 'c', 'c']; + const expectedOutput = [ + { sequence: ['a', 'a', 'b', 'b'], count: 2 }, + { sequence: ['c'], count: 4 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle non-repeating sequences correctly', () => { + const input = ['a', 'b', 'c', 'd', 'e']; + const expectedOutput = [ + { sequence: ['a'], count: 1 }, + { sequence: ['b'], count: 1 }, + { sequence: ['c'], count: 1 }, + { sequence: ['d'], count: 1 }, + { sequence: ['e'], count: 1 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle a case where the entire array is a repeating sequence', () => { + const input = ['x', 'y', 'x', 'y', 'x', 'y']; + const expectedOutput = [{ sequence: ['x', 'y'], count: 3 }]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should correctly identify the maximal repeating substring', () => { + const input = ['a', 'b', 'a', 'b', 'a', 'b', 'c', 'c', 'c', 'c']; + const expectedOutput = [ + { sequence: ['a', 'b'], count: 3 }, + { sequence: ['c'], count: 4 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle repeats with varying lengths', () => { + const input = ['a', 'a', 'b', 'b', 'b', 'b', 'a', 'a']; + const expectedOutput = [ + { sequence: ['a'], count: 2 }, + { sequence: ['b'], count: 4 }, + { sequence: ['a'], count: 2 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should correctly handle a repeat count of one (k adjustment to zero)', () => { + const input = ['a', 'b', 'a', 'b', 'c']; + const expectedOutput = [ + { sequence: ['a', 'b'], count: 2 }, + { sequence: ['c'], count: 1 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should correctly handle repeats at the end of the array', () => { + const input = ['x', 'y', 'x', 'y', 'x', 'y', 'z']; + const expectedOutput = [ + { sequence: ['x', 'y'], count: 3 }, + { sequence: ['z'], count: 1 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should not overcount repeats when the last potential repeat is incomplete', () => { + const input = ['m', 'n', 'm', 'n', 'm']; + const expectedOutput = [ + { sequence: ['m', 'n'], count: 2 }, + { sequence: ['m'], count: 1 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); + +it('should handle single repeats correctly when the substring length is greater than one', () => { + const input = ['a', 'b', 'c', 'a', 'b', 'd']; + const expectedOutput = [ + { sequence: ['a'], count: 1 }, + { sequence: ['b'], count: 1 }, + { sequence: ['c'], count: 1 }, + { sequence: ['a'], count: 1 }, + { sequence: ['b'], count: 1 }, + { sequence: ['d'], count: 1 }, + ]; + expect(findRepeatedSubsequences(input)).toEqual(expectedOutput); +}); diff --git a/tests/playwright-test/stable-test-runner/package-lock.json b/tests/playwright-test/stable-test-runner/package-lock.json index df6792d59d..1ebdfb52cc 100644 --- a/tests/playwright-test/stable-test-runner/package-lock.json +++ b/tests/playwright-test/stable-test-runner/package-lock.json @@ -5,15 +5,15 @@ "packages": { "": { "dependencies": { - "@playwright/test": "1.49.0-alpha-2024-10-17" + "@playwright/test": "1.49.0-alpha-2024-10-20" } }, "node_modules/@playwright/test": { - "version": "1.49.0-alpha-2024-10-17", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-17.tgz", - "integrity": "sha512-HLZY3sM6xt9Wi8K09zPwjJQtcUBZNBcNSIVoMZhtJM3+TikCKx4SiJ3P8vbSlk7Tm3s2oqlS+wA181IxhbTGBA==", + "version": "1.49.0-alpha-2024-10-20", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-20.tgz", + "integrity": "sha512-lSagJ8KSD636T/TNfSJRh+vuBBssCL5xJgYmsvsF37cDMATTdVf2OVozVK91V9MAL7CxP4F5sQFVq/8rqu23WA==", "dependencies": { - "playwright": "1.49.0-alpha-2024-10-17" + "playwright": "1.49.0-alpha-2024-10-20" }, "bin": { "playwright": "cli.js" @@ -36,11 +36,11 @@ } }, "node_modules/playwright": { - "version": "1.49.0-alpha-2024-10-17", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-17.tgz", - "integrity": "sha512-IgcLunnpocVS/AEq2lcftVOu0DGQzFm1Qt25SCJsrVvKVe83ElKXZYskPz7yA0HeuOVxQyN69EDWI09ph7lfoQ==", + "version": "1.49.0-alpha-2024-10-20", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-20.tgz", + "integrity": "sha512-lkZXCaLoVKaa3eVu8qJJiLym6SkjXD+ilE4XZJx3AIE0o4vqMEYVB8tjLzAcl4UZx8wVcCps/WcCvTWhOSIXRA==", "dependencies": { - "playwright-core": "1.49.0-alpha-2024-10-17" + "playwright-core": "1.49.0-alpha-2024-10-20" }, "bin": { "playwright": "cli.js" @@ -53,9 +53,9 @@ } }, "node_modules/playwright-core": { - "version": "1.49.0-alpha-2024-10-17", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-17.tgz", - "integrity": "sha512-XLTKmPBm2ZIOXBckXtiimSOIjQsYy8MqEP9CsHSgytsP0E+j/44v1BuwHOOMaG8sfjcuZLZ1QdFidnl07A9wSg==", + "version": "1.49.0-alpha-2024-10-20", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-20.tgz", + "integrity": "sha512-TeQNA7vsGVrHaArr+giPyiWPAV27+wIcuMLrAJXzUB0leVA9bkXbNQ5lA5+G4OhqlmYAbMOpJMtN+TREDv4nXA==", "bin": { "playwright-core": "cli.js" }, @@ -66,11 +66,11 @@ }, "dependencies": { "@playwright/test": { - "version": "1.49.0-alpha-2024-10-17", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-17.tgz", - "integrity": "sha512-HLZY3sM6xt9Wi8K09zPwjJQtcUBZNBcNSIVoMZhtJM3+TikCKx4SiJ3P8vbSlk7Tm3s2oqlS+wA181IxhbTGBA==", + "version": "1.49.0-alpha-2024-10-20", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-20.tgz", + "integrity": "sha512-lSagJ8KSD636T/TNfSJRh+vuBBssCL5xJgYmsvsF37cDMATTdVf2OVozVK91V9MAL7CxP4F5sQFVq/8rqu23WA==", "requires": { - "playwright": "1.49.0-alpha-2024-10-17" + "playwright": "1.49.0-alpha-2024-10-20" } }, "fsevents": { @@ -80,18 +80,18 @@ "optional": true }, "playwright": { - "version": "1.49.0-alpha-2024-10-17", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-17.tgz", - "integrity": "sha512-IgcLunnpocVS/AEq2lcftVOu0DGQzFm1Qt25SCJsrVvKVe83ElKXZYskPz7yA0HeuOVxQyN69EDWI09ph7lfoQ==", + "version": "1.49.0-alpha-2024-10-20", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-20.tgz", + "integrity": "sha512-lkZXCaLoVKaa3eVu8qJJiLym6SkjXD+ilE4XZJx3AIE0o4vqMEYVB8tjLzAcl4UZx8wVcCps/WcCvTWhOSIXRA==", "requires": { "fsevents": "2.3.2", - "playwright-core": "1.49.0-alpha-2024-10-17" + "playwright-core": "1.49.0-alpha-2024-10-20" } }, "playwright-core": { - "version": "1.49.0-alpha-2024-10-17", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-17.tgz", - "integrity": "sha512-XLTKmPBm2ZIOXBckXtiimSOIjQsYy8MqEP9CsHSgytsP0E+j/44v1BuwHOOMaG8sfjcuZLZ1QdFidnl07A9wSg==" + "version": "1.49.0-alpha-2024-10-20", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-20.tgz", + "integrity": "sha512-TeQNA7vsGVrHaArr+giPyiWPAV27+wIcuMLrAJXzUB0leVA9bkXbNQ5lA5+G4OhqlmYAbMOpJMtN+TREDv4nXA==" } } } diff --git a/tests/playwright-test/stable-test-runner/package.json b/tests/playwright-test/stable-test-runner/package.json index 14625ebe6d..dbe21acd15 100644 --- a/tests/playwright-test/stable-test-runner/package.json +++ b/tests/playwright-test/stable-test-runner/package.json @@ -1,6 +1,6 @@ { "private": true, "dependencies": { - "@playwright/test": "1.49.0-alpha-2024-10-17" + "@playwright/test": "1.49.0-alpha-2024-10-20" } } diff --git a/tests/playwright-test/ui-mode-test-run.spec.ts b/tests/playwright-test/ui-mode-test-run.spec.ts index f60a6dde86..3673faab45 100644 --- a/tests/playwright-test/ui-mode-test-run.spec.ts +++ b/tests/playwright-test/ui-mode-test-run.spec.ts @@ -61,22 +61,25 @@ test('should run visible', async ({ runUITest }) => { ⊘ skipped `); - // await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` - // - tree: - // - treeitem "a.test.ts" [expanded]: - // - treeitem "passes" - // - treeitem "fails" [selected]: - // - button "Run" - // - button "Show source" - // - button "Watch" - // - treeitem "suite" - // - treeitem "b.test.ts" [expanded]: - // - treeitem "passes" - // - treeitem "fails" - // - treeitem "c.test.ts" [expanded]: - // - treeitem "passes" - // - treeitem "skipped" - // `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-error] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem ${/\[icon-error\] fails \d+ms/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-error] suite" + - treeitem "[icon-error] b.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem ${/\[icon-error\] fails \d+ms/} + - treeitem "[icon-check] c.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem "[icon-circle-slash] skipped" + `); await expect(page.getByTestId('status-line')).toHaveText('4/8 passed (50%)'); }); @@ -117,6 +120,17 @@ test('should run on hover', async ({ runUITest }) => { ✅ passes <= ◯ fails `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-circle-outline] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/}: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-circle-outline] fails" + `); }); test('should run on double click', async ({ runUITest }) => { @@ -135,6 +149,17 @@ test('should run on double click', async ({ runUITest }) => { ✅ passes <= ◯ fails `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-circle-outline] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-circle-outline] fails" + `); }); test('should run on Enter', async ({ runUITest }) => { @@ -154,6 +179,17 @@ test('should run on Enter', async ({ runUITest }) => { ◯ passes ❌ fails <= `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-error] a.test.ts" [expanded]: + - group: + - treeitem "[icon-circle-outline] passes" + - treeitem ${/\[icon-error\] fails \d+ms/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + `); }); test('should run by project', async ({ runUITest }) => { @@ -185,6 +221,26 @@ test('should run by project', async ({ runUITest }) => { ⊘ skipped `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-error] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem ${/\[icon-error\] fails \d+ms/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-error] suite" + - treeitem "[icon-error] b.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem ${/\[icon-error\] fails \d+ms/} + - treeitem "[icon-check] c.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem "[icon-circle-slash] skipped" + `); + await page.getByText('Status:').click(); await page.getByLabel('bar').setChecked(true); @@ -203,6 +259,29 @@ test('should run by project', async ({ runUITest }) => { ► ◯ skipped `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-error] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-circle-outline\] passes/} + - treeitem ${/\[icon-error\] fails/}: + - group: + - treeitem ${/\[icon-error\] foo/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-circle-outline] bar" + - treeitem "[icon-error] suite" + - treeitem "[icon-error] b.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-circle-outline\] passes/} + - treeitem ${/\[icon-error\] fails/} + - treeitem "[icon-circle-outline] c.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-circle-outline\] passes/} + - treeitem ${/\[icon-circle-outline\] skipped/} + `); + await page.getByText('Status:').click(); await page.getByTestId('test-tree').getByText('passes').first().click(); @@ -216,6 +295,20 @@ test('should run by project', async ({ runUITest }) => { ► ❌ fails `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-error] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-circle-outline\] passes \d+ms/} [expanded] [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - group: + - treeitem ${/\[icon-check\] foo \d+ms/} + - treeitem ${/\[icon-circle-outline\] bar/} + - treeitem ${/\[icon-error\] fails \d+ms/} + `); + await expect(page.getByText('Projects: foo bar')).toBeVisible(); await page.getByTitle('Run all').click(); @@ -235,6 +328,32 @@ test('should run by project', async ({ runUITest }) => { ► ✅ passes ► ⊘ skipped `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-error] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} [expanded]: + - group: + - treeitem ${/\[icon-check\] foo \d+ms/} + - treeitem ${/\[icon-check\] bar \d+ms/} + - treeitem ${/\[icon-error\] fails \d+ms/} [expanded]: + - group: + - treeitem ${/\[icon-error\] foo \d+ms/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem ${/\[icon-error\] bar \d+ms/} + - treeitem ${/\[icon-error\] suite/} + - treeitem "[icon-error] b.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes/} + - treeitem ${/\[icon-error\] fails/} + - treeitem "[icon-check] c.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes/} + - treeitem ${/\[icon-circle-slash\] skipped/} + `); }); test('should stop', async ({ runUITest }) => { @@ -261,6 +380,16 @@ test('should stop', async ({ runUITest }) => { 🕦 test 3 `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-loading] a.test.ts" [expanded]: + - group: + - treeitem "[icon-circle-slash] test 0" + - treeitem ${/\[icon-check\] test 1 \d+ms/} + - treeitem ${/\[icon-loading\] test 2/} + - treeitem ${/\[icon-clock\] test 3/} + `); + await expect(page.getByTitle('Run all')).toBeDisabled(); await expect(page.getByTitle('Stop')).toBeEnabled(); @@ -273,6 +402,16 @@ test('should stop', async ({ runUITest }) => { ◯ test 2 ◯ test 3 `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-circle-outline] a.test.ts" [expanded]: + - group: + - treeitem "[icon-circle-slash] test 0" + - treeitem ${/\[icon-check\] test 1 \d+ms/} + - treeitem ${/\[icon-circle-outline\] test 2/} + - treeitem ${/\[icon-circle-outline\] test 3/} + `); }); test('should run folder', async ({ runUITest }) => { @@ -301,6 +440,17 @@ test('should run folder', async ({ runUITest }) => { ▼ ◯ in-a.test.ts ◯ passes `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] folder-b" [expanded] [selected]: + - group: + - treeitem "[icon-check] folder-c" + - treeitem "[icon-check] in-b.test.ts" + - treeitem "[icon-circle-outline] in-a.test.ts" [expanded]: + - group: + - treeitem "[icon-circle-outline] passes" + `); }); test('should show time', async ({ runUITest }) => { @@ -324,6 +474,26 @@ test('should show time', async ({ runUITest }) => { ⊘ skipped `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-error] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem ${/\[icon-error\] fails \d+ms/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-error] suite" + - treeitem "[icon-error] b.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem ${/\[icon-error\] fails \d+ms/} + - treeitem "[icon-check] c.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] passes \d+ms/} + - treeitem "[icon-circle-slash] skipped" + `); + await expect(page.getByTestId('status-line')).toHaveText('4/8 passed (50%)'); }); @@ -348,6 +518,13 @@ test('should show test.fail as passing', async ({ runUITest }) => { ✅ should fail XXms `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] should fail \d+ms/} + `); + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); }); @@ -377,6 +554,13 @@ test('should ignore repeatEach', async ({ runUITest }) => { ✅ should pass `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] should pass \d+ms/} + `); + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); }); @@ -404,6 +588,14 @@ test('should remove output folder before test run', async ({ runUITest }) => { ▼ ✅ a.test.ts ✅ should pass `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] should pass \d+ms/} + `); + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); await page.getByTitle('Run all').click(); @@ -411,6 +603,14 @@ test('should remove output folder before test run', async ({ runUITest }) => { ▼ ✅ a.test.ts ✅ should pass `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] should pass \d+ms/} + `); + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); }); @@ -451,6 +651,18 @@ test('should show proper total when using deps', async ({ runUITest }) => { ✅ run @setup <= ◯ run @chromium `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-circle-outline] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] run @setup setup \d+ms/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + - treeitem "[icon-circle-outline] run @chromium chromium" + `); + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); await page.getByTitle('run @chromium').dblclick(); @@ -459,6 +671,18 @@ test('should show proper total when using deps', async ({ runUITest }) => { ✅ run @setup ✅ run @chromium <= `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] run @setup setup \d+ms/} + - treeitem ${/\[icon-check\] run @chromium chromium \d+ms/} [selected]: + - button "Run" + - button "Show source" + - button "Watch" + `); + await expect(page.getByTestId('status-line')).toHaveText('2/2 passed (100%)'); }); @@ -518,6 +742,13 @@ test('should respect --tsconfig option', { ✅ test `); + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] test \d+ms/} + `); + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); }); @@ -539,4 +770,11 @@ test('should respect --ignore-snapshots option', { ▼ ✅ a.test.ts ✅ snapshot `); + + await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(` + - tree: + - treeitem "[icon-check] a.test.ts" [expanded]: + - group: + - treeitem ${/\[icon-check\] snapshot \d+ms/} + `); }); From b275c1961237c61492ac6fe20402524cb88d368b Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 22 Oct 2024 11:52:20 +0200 Subject: [PATCH 6/6] chore: update eslintignore to lint files in utils/ folders (#33218) --- .eslintignore | 9 +++------ packages/playwright-core/src/utils/crypto.ts | 11 +++++----- .../src/utils/happy-eyeballs.ts | 20 +++++++++---------- .../src/utils/isomorphic/locatorGenerators.ts | 2 +- packages/playwright-core/src/utils/network.ts | 2 +- .../playwright-core/src/utils/sequence.ts | 11 ++++++---- .../playwright-core/src/utils/stackTrace.ts | 2 -- packages/playwright-core/src/utils/zones.ts | 3 ++- 8 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.eslintignore b/.eslintignore index 60b8fd360f..9d22f618d8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,14 +8,11 @@ test/assets/modernizr.js /packages/playwright-ct-core/src/generated/* /index.d.ts node_modules/ -browser_patches/*/checkout/ -browser_patches/chromium/output/ **/*.d.ts output/ test-results/ -tests/components/ -tests/installation/fixture-scripts/ -examples/ +/tests/components/ +/tests/installation/fixture-scripts/ DEPS .cache/ -utils/ +/utils/ diff --git a/packages/playwright-core/src/utils/crypto.ts b/packages/playwright-core/src/utils/crypto.ts index 5da56d4e9b..f538912d24 100644 --- a/packages/playwright-core/src/utils/crypto.ts +++ b/packages/playwright-core/src/utils/crypto.ts @@ -33,11 +33,12 @@ function encodeBase128(value: number): Buffer { do { let byte = value & 0x7f; value >>>= 7; - if (bytes.length > 0) byte |= 0x80; + if (bytes.length > 0) + byte |= 0x80; bytes.push(byte); } while (value > 0); return Buffer.from(bytes.reverse()); -}; +} // ASN1/DER Speficiation: https://www.itu.int/rec/T-REC-X.680-X.693-202102-I/en class DER { @@ -49,13 +50,13 @@ class DER { return this._encode(0x02, Buffer.from([data])); } static encodeObjectIdentifier(oid: string): Buffer { - const parts = oid.split('.').map((v) => Number(v)); + const parts = oid.split('.').map(v => Number(v)); // Encode the second part, which could be large, using base-128 encoding if necessary const output = [encodeBase128(40 * parts[0] + parts[1])]; - for (let i = 2; i < parts.length; i++) { + for (let i = 2; i < parts.length; i++) output.push(encodeBase128(parts[i])); - } + return this._encode(0x06, Buffer.concat(output)); } diff --git a/packages/playwright-core/src/utils/happy-eyeballs.ts b/packages/playwright-core/src/utils/happy-eyeballs.ts index 18de1a938c..02b78de4f0 100644 --- a/packages/playwright-core/src/utils/happy-eyeballs.ts +++ b/packages/playwright-core/src/utils/happy-eyeballs.ts @@ -29,8 +29,8 @@ import { monotonicTime } from './time'; // Same as in Chromium (https://source.chromium.org/chromium/chromium/src/+/5666ff4f5077a7e2f72902f3a95f5d553ea0d88d:net/socket/transport_connect_job.cc;l=102) const connectionAttemptDelayMs = 300; -const kDNSLookupAt = Symbol('kDNSLookupAt') -const kTCPConnectionAt = Symbol('kTCPConnectionAt') +const kDNSLookupAt = Symbol('kDNSLookupAt'); +const kTCPConnectionAt = Symbol('kTCPConnectionAt'); class HttpHappyEyeballsAgent extends http.Agent { createConnection(options: http.ClientRequestArgs, oncreate?: (err: Error | null, socket?: net.Socket) => void): net.Socket | undefined { @@ -75,7 +75,7 @@ export async function createTLSSocket(options: tls.ConnectionOptions): Promise { assert(options.host, 'host is required'); if (net.isIP(options.host)) { - const socket = tls.connect(options) + const socket = tls.connect(options); socket.on('secureConnect', () => resolve(socket)); socket.on('error', error => reject(error)); } else { @@ -92,20 +92,20 @@ export async function createTLSSocket(options: tls.ConnectionOptions): Promise void) | undefined, + options: http.ClientRequestArgs, + oncreate: ((err: Error | null, socket?: tls.TLSSocket) => void) | undefined, useTLS: true ): Promise; export async function createConnectionAsync( - options: http.ClientRequestArgs, - oncreate: ((err: Error | null, socket?: net.Socket) => void) | undefined, + options: http.ClientRequestArgs, + oncreate: ((err: Error | null, socket?: net.Socket) => void) | undefined, useTLS: false ): Promise; export async function createConnectionAsync( - options: http.ClientRequestArgs, - oncreate: ((err: Error | null, socket?: any) => void) | undefined, + options: http.ClientRequestArgs, + oncreate: ((err: Error | null, socket?: any) => void) | undefined, useTLS: boolean ): Promise { const lookup = (options as any).__testHookLookup || lookupAddresses; @@ -202,5 +202,5 @@ export function timingForSocket(socket: net.Socket | tls.TLSSocket) { return { dnsLookupAt: (socket as any)[kDNSLookupAt] as number | undefined, tcpConnectionAt: (socket as any)[kTCPConnectionAt] as number | undefined, - } + }; } diff --git a/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts b/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts index 56252d02d3..930abaaba6 100644 --- a/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts +++ b/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts @@ -163,7 +163,7 @@ function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFram continue; } - let locatorType: LocatorType = 'default'; + const locatorType: LocatorType = 'default'; const nextPart = parts[index + 1]; diff --git a/packages/playwright-core/src/utils/network.ts b/packages/playwright-core/src/utils/network.ts index 632c74fe3a..25d3a11156 100644 --- a/packages/playwright-core/src/utils/network.ts +++ b/packages/playwright-core/src/utils/network.ts @@ -124,7 +124,7 @@ export function createHttpsServer(...args: any[]): https.Server { return server; } -export function createHttp2Server( onRequestHandler?: (request: http2.Http2ServerRequest, response: http2.Http2ServerResponse) => void,): http2.Http2SecureServer; +export function createHttp2Server(onRequestHandler?: (request: http2.Http2ServerRequest, response: http2.Http2ServerResponse) => void,): http2.Http2SecureServer; export function createHttp2Server(options: http2.SecureServerOptions, onRequestHandler?: (request: http2.Http2ServerRequest, response: http2.Http2ServerResponse) => void,): http2.Http2SecureServer; export function createHttp2Server(...args: any[]): http2.Http2SecureServer { const server = http2.createSecureServer(...args); diff --git a/packages/playwright-core/src/utils/sequence.ts b/packages/playwright-core/src/utils/sequence.ts index 2af5429bd6..27756fabeb 100644 --- a/packages/playwright-core/src/utils/sequence.ts +++ b/packages/playwright-core/src/utils/sequence.ts @@ -20,10 +20,13 @@ export function findRepeatedSubsequences(s: string[]): { sequence: string[]; cou let i = 0; const arraysEqual = (a1: string[], a2: string[]) => { - if (a1.length !== a2.length) return false; + if (a1.length !== a2.length) + return false; for (let j = 0; j < a1.length; j++) { - if (a1[j] !== a2[j]) return false; + if (a1[j] !== a2[j]) + return false; } + return true; }; @@ -41,9 +44,9 @@ export function findRepeatedSubsequences(s: string[]): { sequence: string[]; cou while ( i + p * k <= n && arraysEqual(s.slice(i + p * (k - 1), i + p * k), substr) - ) { + ) k += 1; - } + k -= 1; // Adjust k since it increments one extra time in the loop // Update the maximal repeating substring if necessary diff --git a/packages/playwright-core/src/utils/stackTrace.ts b/packages/playwright-core/src/utils/stackTrace.ts index 6f9a87578b..84d08b0184 100644 --- a/packages/playwright-core/src/utils/stackTrace.ts +++ b/packages/playwright-core/src/utils/stackTrace.ts @@ -16,7 +16,6 @@ import path from 'path'; import { parseStackTraceLine } from '../utilsBundle'; -import { isUnderTest } from './'; import type { StackFrame } from '@protocol/channels'; import { colors } from '../utilsBundle'; import { findRepeatedSubsequences } from './sequence'; @@ -51,7 +50,6 @@ export function captureRawStack(): RawStack { export function captureLibraryStackTrace(): { frames: StackFrame[], apiName: string } { const stack = captureRawStack(); - const isTesting = isUnderTest(); type ParsedFrame = { frame: StackFrame; frameText: string; diff --git a/packages/playwright-core/src/utils/zones.ts b/packages/playwright-core/src/utils/zones.ts index 68cb6fa7fa..e6fabac0f1 100644 --- a/packages/playwright-core/src/utils/zones.ts +++ b/packages/playwright-core/src/utils/zones.ts @@ -46,8 +46,9 @@ class ZoneManager { if (zone.type === 'apiZone') str += `(${(zone.data as any).apiName})`; zones.push(str); - + } + // eslint-disable-next-line no-console console.log('zones: ', zones.join(' -> ')); } }