diff --git a/docs/src/actionability.md b/docs/src/actionability.md index cff339faffb01..3510171e5c328 100644 --- a/docs/src/actionability.md +++ b/docs/src/actionability.md @@ -66,7 +66,7 @@ Playwright includes auto-retrying assertions that remove flakiness by waiting un | [`method: LocatorAssertions.toHaveAttribute`] | Element has a DOM attribute | | [`method: LocatorAssertions.toHaveClass`] | Element has a class property | | [`method: LocatorAssertions.toHaveCount`] | List has exact number of children | -| [`method: LocatorAssertions.toHaveCSS#1`] | Element has CSS property | +| [`method: LocatorAssertions.toHaveCSS`] | Element has CSS property | | [`method: LocatorAssertions.toHaveId`] | Element has an ID | | [`method: LocatorAssertions.toHaveJSProperty`] | Element has a JavaScript property | | [`method: LocatorAssertions.toHaveText`] | Element matches text | diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md index f7c5e9028f8a2..4473a9a333f36 100644 --- a/docs/src/api/class-locatorassertions.md +++ b/docs/src/api/class-locatorassertions.md @@ -351,7 +351,7 @@ Expected count. * since: v1.20 * langs: python -The opposite of [`method: LocatorAssertions.toHaveCSS#1`]. +The opposite of [`method: LocatorAssertions.toHaveCSS`]. ### param: LocatorAssertions.NotToHaveCSS.name * since: v1.18 @@ -1216,7 +1216,7 @@ await expect(page.locator('ul')).toContainText(['Text 3']); ```java // ✓ Contains the right items in the right order -assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3", "Text 4"}); +assertThat(page.locator("ul > li")).containsText(new String[] {"Text 1", "Text 3"}); // ✖ Wrong order assertThat(page.locator("ul > li")).containsText(new String[] {"Text 3", "Text 2"}); @@ -1232,7 +1232,7 @@ assertThat(page.locator("ul")).containsText(new String[] {"Text 3"}); from playwright.async_api import expect # ✓ Contains the right items in the right order -await expect(page.locator("ul > li")).to_contain_text(["Text 1", "Text 3", "Text 4"]) +await expect(page.locator("ul > li")).to_contain_text(["Text 1", "Text 3"]) # ✖ Wrong order await expect(page.locator("ul > li")).to_contain_text(["Text 3", "Text 2"]) @@ -1248,7 +1248,7 @@ await expect(page.locator("ul")).to_contain_text(["Text 3"]) from playwright.sync_api import expect # ✓ Contains the right items in the right order -expect(page.locator("ul > li")).to_contain_text(["Text 1", "Text 3", "Text 4"]) +expect(page.locator("ul > li")).to_contain_text(["Text 1", "Text 3"]) # ✖ Wrong order expect(page.locator("ul > li")).to_contain_text(["Text 3", "Text 2"]) @@ -1262,7 +1262,7 @@ expect(page.locator("ul")).to_contain_text(["Text 3"]) ```csharp // ✓ Contains the right items in the right order -await Expect(Page.Locator("ul > li")).ToContainTextAsync(new string[] {"Text 1", "Text 3", "Text 4"}); +await Expect(Page.Locator("ul > li")).ToContainTextAsync(new string[] {"Text 1", "Text 3"}); // ✖ Wrong order await Expect(Page.Locator("ul > li")).ToContainTextAsync(new string[] {"Text 3", "Text 2"}); @@ -1694,7 +1694,7 @@ Expected count. ### option: LocatorAssertions.toHaveCount.timeout = %%-csharp-java-python-assertions-timeout-%% * since: v1.18 -## async method: LocatorAssertions.toHaveCSS#1 +## async method: LocatorAssertions.toHaveCSS * since: v1.20 * langs: - alias-java: hasCSS @@ -1731,53 +1731,24 @@ var locator = Page.GetByRole(AriaRole.Button); await Expect(locator).ToHaveCSSAsync("display", "flex"); ``` -### param: LocatorAssertions.toHaveCSS#1.name +### param: LocatorAssertions.toHaveCSS.name * since: v1.18 - `name` <[string]> CSS property name. -### param: LocatorAssertions.toHaveCSS#1.value +### param: LocatorAssertions.toHaveCSS.value * since: v1.18 - `value` <[string]|[RegExp]> CSS property value. -### option: LocatorAssertions.toHaveCSS#1.timeout = %%-js-assertions-timeout-%% +### option: LocatorAssertions.toHaveCSS.timeout = %%-js-assertions-timeout-%% * since: v1.18 -### option: LocatorAssertions.toHaveCSS#1.timeout = %%-csharp-java-python-assertions-timeout-%% +### option: LocatorAssertions.toHaveCSS.timeout = %%-csharp-java-python-assertions-timeout-%% * since: v1.18 -## async method: LocatorAssertions.toHaveCSS#2 -* since: v1.58 -* langs: js - -Ensures the [Locator] resolves to an element with the given computed CSS properties. - -:::note -The `CSSProperties` object parameter for toHaveCSS requires `react` to be installed for type checking. -::: - -**Usage** - -```js -const locator = page.getByRole('button'); -await expect(locator).toHaveCSS({ - display: 'flex', - backgroundColor: 'rgb(255, 0, 0)' -}); -``` - -### param: LocatorAssertions.toHaveCSS#2.styles -* since: v1.58 -- `styles` <[CSSProperties]> - -CSS properties object. - -### option: LocatorAssertions.toHaveCSS#2.timeout = %%-js-assertions-timeout-%% -* since: v1.58 - ## async method: LocatorAssertions.toHaveId * since: v1.20 * langs: diff --git a/docs/src/release-notes-js.md b/docs/src/release-notes-js.md index 48911bf35a67b..c81b166855e4f 100644 --- a/docs/src/release-notes-js.md +++ b/docs/src/release-notes-js.md @@ -3154,7 +3154,7 @@ List of all new assertions: - [`expect(locator).toHaveAttribute(name, value)`](./api/class-locatorassertions#locator-assertions-to-have-attribute) - [`expect(locator).toHaveClass(expected)`](./api/class-locatorassertions#locator-assertions-to-have-class) - [`expect(locator).toHaveCount(count)`](./api/class-locatorassertions#locator-assertions-to-have-count) -- [`expect(locator).toHaveCSS(name, value)`](./api/class-locatorassertions#locator-assertions-to-have-css-1) +- [`expect(locator).toHaveCSS(name, value)`](./api/class-locatorassertions#locator-assertions-to-have-css) - [`expect(locator).toHaveId(id)`](./api/class-locatorassertions#locator-assertions-to-have-id) - [`expect(locator).toHaveJSProperty(name, value)`](./api/class-locatorassertions#locator-assertions-to-have-js-property) - [`expect(locator).toHaveText(expected, options)`](./api/class-locatorassertions#locator-assertions-to-have-text) diff --git a/docs/src/test-assertions-csharp-java-python.md b/docs/src/test-assertions-csharp-java-python.md index 520a9419b87e3..114de1624a8cc 100644 --- a/docs/src/test-assertions-csharp-java-python.md +++ b/docs/src/test-assertions-csharp-java-python.md @@ -24,7 +24,7 @@ title: "Assertions" | [`method: LocatorAssertions.toHaveAttribute`] | Element has a DOM attribute | | [`method: LocatorAssertions.toHaveClass`] | Element has a class property | | [`method: LocatorAssertions.toHaveCount`] | List has exact number of children | -| [`method: LocatorAssertions.toHaveCSS#1`] | Element has CSS property | +| [`method: LocatorAssertions.toHaveCSS`] | Element has CSS property | | [`method: LocatorAssertions.toHaveId`] | Element has an ID | | [`method: LocatorAssertions.toHaveJSProperty`] | Element has a JavaScript property | | [`method: LocatorAssertions.toHaveRole`] | Element has a specific [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles) | diff --git a/docs/src/test-assertions-js.md b/docs/src/test-assertions-js.md index 7baca2e707736..c809dce782764 100644 --- a/docs/src/test-assertions-js.md +++ b/docs/src/test-assertions-js.md @@ -46,7 +46,7 @@ Note that retrying assertions are async, so you must `await` them. | [await expect(locator).toHaveAttribute()](./api/class-locatorassertions.md#locator-assertions-to-have-attribute) | Element has a DOM attribute | | [await expect(locator).toHaveClass()](./api/class-locatorassertions.md#locator-assertions-to-have-class) | Element has specified CSS class property | | [await expect(locator).toHaveCount()](./api/class-locatorassertions.md#locator-assertions-to-have-count) | List has exact number of children | -| [await expect(locator).toHaveCSS()](./api/class-locatorassertions.md#locator-assertions-to-have-css-1) | Element has CSS property | +| [await expect(locator).toHaveCSS()](./api/class-locatorassertions.md#locator-assertions-to-have-css) | Element has CSS property | | [await expect(locator).toHaveId()](./api/class-locatorassertions.md#locator-assertions-to-have-id) | Element has an ID | | [await expect(locator).toHaveJSProperty()](./api/class-locatorassertions.md#locator-assertions-to-have-js-property) | Element has a JavaScript property | | [await expect(locator).toHaveRole()](./api/class-locatorassertions.md#locator-assertions-to-have-role) | Element has a specific [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles) | diff --git a/packages/html-reporter/src/testResultView.css b/packages/html-reporter/src/testResultView.css index 7291d2a1c5548..3dbfb1da4a9e3 100644 --- a/packages/html-reporter/src/testResultView.css +++ b/packages/html-reporter/src/testResultView.css @@ -43,6 +43,51 @@ border-radius: 12px; color: var(--color-canvas-default); padding: 2px 8px; + line-height: normal; +} + +.step-title-container { + display: flex; + align-items: center; + flex: auto; + min-width: 0; +} + +.step-title-container > * { + flex-shrink: 0; +} + +.step-title-text { + flex-shrink: 1; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + min-width: 0; +} + +.step-spacer { + flex: auto; +} + +.step-attachment-link { + display: flex; + flex: none; + border-radius: 4px; + padding: 4px; +} + +.step-attachment-link:hover { + background-color: var(--color-neutral-muted); +} + +.step-attachment-link .octicon { + margin-right: 0; +} + +.step-duration { + flex: none; + white-space: nowrap; + margin-left: 4px; } :root.light-mode { diff --git a/packages/html-reporter/src/testResultView.tsx b/packages/html-reporter/src/testResultView.tsx index d611b144d7a5e..08db1aad958ce 100644 --- a/packages/html-reporter/src/testResultView.tsx +++ b/packages/html-reporter/src/testResultView.tsx @@ -194,20 +194,23 @@ const StepTreeItem: React.FC<{ depth: number, }> = ({ test, step, result, depth }) => { const searchParams = useSearchParams(); - return - {msToString(step.duration)} + return + {statusIcon(step.error || step.duration === -1 ? 'failed' : (step.skipped ? 'skipped' : 'passed'))} + + {step.title} + {step.count > 1 && <> ✕ {step.count}} + {step.location && — {step.location.file}:{step.location.line}} + + {step.attachments.length > 0 && { evt.stopPropagation(); }}> {icons.attachment()} } - {statusIcon(step.error || step.duration === -1 ? 'failed' : (step.skipped ? 'skipped' : 'passed'))} - {step.title} - {step.count > 1 && <> ✕ {step.count}} - {step.location && — {step.location.file}:{step.location.line}} - } loadChildren={step.steps.length || step.snippet ? () => { + {msToString(step.duration)} + } loadChildren={step.steps.length || step.snippet ? () => { const snippet = step.snippet ? [] : []; const steps = step.steps.map((s, i) => ); return snippet.concat(steps); diff --git a/packages/html-reporter/src/treeItem.css b/packages/html-reporter/src/treeItem.css index b957d1ec5c032..b6ada77a4b623 100644 --- a/packages/html-reporter/src/treeItem.css +++ b/packages/html-reporter/src/treeItem.css @@ -15,14 +15,20 @@ */ .tree-item { - text-overflow: ellipsis; + display: flex; + flex-direction: column; overflow: hidden; - white-space: nowrap; + min-width: 0; line-height: 38px; } .tree-item-title { cursor: pointer; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; + display: flex; + align-items: center; } .tree-item-body { diff --git a/packages/html-reporter/src/treeItem.tsx b/packages/html-reporter/src/treeItem.tsx index 389e558650071..3e06d7087a0e3 100644 --- a/packages/html-reporter/src/treeItem.tsx +++ b/packages/html-reporter/src/treeItem.tsx @@ -30,12 +30,12 @@ export const TreeItem: React.FunctionComponent<{ }> = ({ title, loadChildren, onClick, expandByDefault, depth, style, flash }) => { const [expanded, setExpanded] = React.useState(expandByDefault || false); return
- { onClick?.(); setExpanded(!expanded); }} > +
{ onClick?.(); setExpanded(!expanded); }} > {loadChildren && !!expanded && icons.downArrow()} {loadChildren && !expanded && icons.rightArrow()} {!loadChildren && {icons.rightArrow()}} {title} - +
{expanded && loadChildren?.()}
; }; diff --git a/packages/playwright-core/src/client/events.ts b/packages/playwright-core/src/client/events.ts index e3e4df611ccfe..f56c52f3a01fd 100644 --- a/packages/playwright-core/src/client/events.ts +++ b/packages/playwright-core/src/client/events.ts @@ -55,7 +55,6 @@ export const Events = { }, Page: { - AgentTurn: 'agentturn', Close: 'close', Crash: 'crash', Console: 'console', @@ -79,7 +78,6 @@ export const Events = { Worker: 'worker', }, - PageAgent: { Turn: 'turn', }, diff --git a/packages/playwright-core/src/client/pageAgent.ts b/packages/playwright-core/src/client/pageAgent.ts index ec6a16cc9aeab..bcf0cdb839d98 100644 --- a/packages/playwright-core/src/client/pageAgent.ts +++ b/packages/playwright-core/src/client/pageAgent.ts @@ -33,7 +33,7 @@ export class PageAgent extends ChannelOwner implement constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.PageAgentInitializer) { super(parent, type, guid, initializer); this._page = Page.from(initializer.page); - this._channel.on('turn', params => this.emit(Events.Page.AgentTurn, params)); + this._channel.on('turn', params => this.emit(Events.PageAgent.Turn, params)); } async expect(expectation: string, options: channels.PageAgentExpectOptions = {}) { diff --git a/packages/playwright-core/src/server/dispatchers/pageAgentDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageAgentDispatcher.ts index c30f551085b4d..65b57bab06576 100644 --- a/packages/playwright-core/src/server/dispatchers/pageAgentDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageAgentDispatcher.ts @@ -67,6 +67,9 @@ export class PageAgentDispatcher extends Dispatcher { + progress.metadata.potentiallyClosesScope = true; + void this.stopPendingOperations(new Error('The agent is disposed')); + this._dispose(); } private _eventSupport(): loopTypes.LoopEvents { diff --git a/packages/playwright-core/src/server/progress.ts b/packages/playwright-core/src/server/progress.ts index 07b580bff6376..58001fefb7f14 100644 --- a/packages/playwright-core/src/server/progress.ts +++ b/packages/playwright-core/src/server/progress.ts @@ -52,7 +52,7 @@ export class ProgressController { (error as any)[kAbortErrorSymbol] = true; this._state = { error }; this._forceAbortPromise.reject(error); - this._controller.abort(); + this._controller.abort(error); } await this._donePromise; } diff --git a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts index 546c105e73d6b..6e18d54d56b95 100644 --- a/packages/playwright-core/src/utils/isomorphic/stringUtils.ts +++ b/packages/playwright-core/src/utils/isomorphic/stringUtils.ts @@ -47,11 +47,6 @@ export function toSnakeCase(name: string): string { return name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/([A-Z])([A-Z][a-z])/g, '$1_$2').toLowerCase(); } -export function toKebabCase(name: string): string { - // E.g. backgroundColor => background-color. - return name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/([A-Z])([A-Z][a-z])/g, '$1-$2').toLowerCase(); -} - export function formatObject(value: any, indent = ' ', mode: 'multiline' | 'oneline' = 'multiline'): string { if (typeof value === 'string') return escapeWithQuotes(value, '\''); diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts index dbde0329e851d..08321af0c4cfe 100644 --- a/packages/playwright/src/matchers/matchers.ts +++ b/packages/playwright/src/matchers/matchers.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { asLocatorDescription, constructURLBasedOnBaseURL, isRegExp, isString, isTextualMimeType, pollAgainstDeadline, serializeExpectedTextValues, toKebabCase, formatMatcherMessage } from 'playwright-core/lib/utils'; +import { asLocatorDescription, constructURLBasedOnBaseURL, isRegExp, isString, isTextualMimeType, pollAgainstDeadline, serializeExpectedTextValues, formatMatcherMessage } from 'playwright-core/lib/utils'; import { colors } from 'playwright-core/lib/utils'; import { expectTypes } from '../util'; @@ -32,7 +32,7 @@ import type { ExpectMatcherState } from '../../types/test'; import type { TestStepInfoImpl } from '../worker/testInfo'; import type { APIResponse, Locator, Frame, Page } from 'playwright-core'; import type { FrameExpectParams } from 'playwright-core/lib/client/types'; -import type { CSSProperties, ExpectMatcherUtils } from '../../types/test'; +import type { ExpectMatcherUtils } from '../../types/test'; import type { InternalMatcherUtils } from 'playwright-core/lib/utils'; export type ExpectMatcherStateInternal = Omit & { @@ -314,40 +314,17 @@ export function toHaveCount( } export function toHaveCSS(this: ExpectMatcherStateInternal, locator: LocatorEx, name: string, expected: string | RegExp, options?: { timeout?: number }): Promise>; -export function toHaveCSS(this: ExpectMatcherStateInternal, locator: LocatorEx, styles: CSSProperties, options?: { timeout?: number }): Promise>; export function toHaveCSS( this: ExpectMatcherStateInternal, locator: LocatorEx, - nameOrStyles: string | CSSProperties, - expectedOrOptions?: (string | RegExp) | { timeout?: number }, + name: string, + expected: string | RegExp, options?: { timeout?: number }, ) { - if (typeof nameOrStyles === 'string') { - if (expectedOrOptions === undefined) - throw new Error(`toHaveCSS expected value must be provided`); - const propertyName = nameOrStyles as string; - const expected = expectedOrOptions as string | RegExp; - return toMatchText.call(this, 'toHaveCSS', locator, 'Locator', async (isNot, timeout) => { - const expectedText = serializeExpectedTextValues([expected]); - return await locator._expect('to.have.css', { expressionArg: propertyName, expectedText, isNot, timeout }); - }, expected, options); - } else { - const styles = nameOrStyles as CSSProperties; - const options = expectedOrOptions as { timeout?: number }; - return toEqual.call(this, 'toHaveCSS', locator, 'Locator', async (isNot, timeout) => { - const results: any[] = []; - for (const [name, value] of Object.entries(styles)) { - const propertyName = convertStylePropertyNameFromJsToCss(name); - const expected = value as string; - const expectedText = serializeExpectedTextValues([expected]); - const result = await locator._expect('to.have.css', { expressionArg: propertyName, expectedText, isNot, timeout }); - results.push(result); - if (!result.matches) - return result; - } - return { matches: true }; - }, styles, options); - } + return toMatchText.call(this, 'toHaveCSS', locator, 'Locator', async (isNot, timeout) => { + const expectedText = serializeExpectedTextValues([expected]); + return await locator._expect('to.have.css', { expressionArg: name, expectedText, isNot, timeout }); + }, expected, options); } export function toHaveId( @@ -537,11 +514,3 @@ export function computeMatcherTitleSuffix(matcherName: string, receiver: any, ar } return {}; } - -function convertStylePropertyNameFromJsToCss(name: string): string { - const vendorMatch = name.match(/^(Webkit|Moz|Ms|O)([A-Z].*)/); - if (vendorMatch) - return `-${toKebabCase(name)}`; - - return toKebabCase(name); -} diff --git a/packages/playwright/src/mcp/test/testContext.ts b/packages/playwright/src/mcp/test/testContext.ts index 16fce05c55a5f..863a34de9a551 100644 --- a/packages/playwright/src/mcp/test/testContext.ts +++ b/packages/playwright/src/mcp/test/testContext.ts @@ -322,7 +322,7 @@ const bestPracticesMarkdown = ` `; class MCPListReporter extends ListReporter { - async onTestPaused() { + override async onTestPaused() { // ListReporter waits for user input to resume, we don't want that in MCP. await new Promise(() => {}); } diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts index f94ffebf29bf2..200a975740972 100644 --- a/packages/playwright/src/reporters/base.ts +++ b/packages/playwright/src/reporters/base.ts @@ -359,8 +359,8 @@ export class TerminalReporter implements ReporterV2 { return formatError(this.screen, error); } - formatSingleResult(test: TestCase, result: TestResult, index?: number): string { - return formatSingleResult(this.screen, this.config, test, result, index); + formatResultErrors(test: TestCase, result: TestResult): string { + return formatResultErrors(this.screen, test, result); } writeLine(line?: string) { @@ -368,10 +368,8 @@ export class TerminalReporter implements ReporterV2 { } } -function formatSingleResult(screen: Screen, config: FullConfig, test: TestCase, result: TestResult, index?: number): string { +function formatResultErrors(screen: Screen, test: TestCase, result: TestResult): string { const lines: string[] = []; - const header = formatTestHeader(screen, config, test, { indent: ' ', index }); - lines.push(test.outcome() === 'unexpected' ? screen.colors.red(header) : screen.colors.yellow(header)); if (test.outcome() === 'unexpected') { const errorDetails = formatResultFailure(screen, test, result, ' '); if (errorDetails.length > 0) diff --git a/packages/playwright/src/reporters/line.ts b/packages/playwright/src/reporters/line.ts index 4bcda258f6154..3f7b2b631308c 100644 --- a/packages/playwright/src/reporters/line.ts +++ b/packages/playwright/src/reporters/line.ts @@ -86,9 +86,15 @@ class LineReporter extends TerminalReporter { if (!process.env.PW_TEST_DEBUG_REPORTERS) this.screen.stdout.write(`\u001B[1A\u001B[2K`); - this.writeLine(this.formatSingleResult(test, result, test.outcome() === 'unexpected' ? ++this._failures : undefined)); - markErrorsAsReported(result); - this.writeLine(this.screen.colors.yellow(` Paused ${test.outcome() === 'unexpected' ? 'on error' : 'at test end'}. Press Ctrl+C to end.`) + '\n\n'); + if (test.outcome() === 'unexpected') { + this.writeLine(this.screen.colors.red(this.formatTestHeader(test, { indent: ' ', index: ++this._failures }))); + this.writeLine(this.formatResultErrors(test, result)); + markErrorsAsReported(result); + this.writeLine(this.screen.colors.yellow(` Paused on error. Press Ctrl+C to end.`) + '\n\n'); + } else { + this.writeLine(this.screen.colors.yellow(this.formatTestHeader(test, { indent: ' ' }))); + this.writeLine(this.screen.colors.yellow(` Paused at test end. Press Ctrl+C to end.`) + '\n\n'); + } this._updateLine(test, result, undefined); diff --git a/packages/playwright/src/reporters/list.ts b/packages/playwright/src/reporters/list.ts index b2b399e6eb0d6..9315e3bfa38c6 100644 --- a/packages/playwright/src/reporters/list.ts +++ b/packages/playwright/src/reporters/list.ts @@ -17,7 +17,7 @@ import { getAsBooleanFromENV } from 'playwright-core/lib/utils'; import { ms as milliseconds } from 'playwright-core/lib/utilsBundle'; -import { TerminalReporter, stepSuffix } from './base'; +import { markErrorsAsReported, TerminalReporter, stepSuffix } from './base'; import { stripAnsiEscapes } from '../util'; import type { ListReporterOptions } from '../../types/test'; @@ -38,6 +38,7 @@ class ListReporter extends TerminalReporter { private _stepIndex = new Map(); private _needNewLine = false; private _printSteps: boolean; + private _paused = new Set(); constructor(options?: ListReporterOptions & CommonReporterOptions & TerminalReporterOptions) { super(options); @@ -165,9 +166,34 @@ class ListReporter extends TerminalReporter { stream.write(chunk); } + async onTestPaused(test: TestCase, result: TestResult) { + // Without TTY, user cannot interrupt the pause. Let's skip it. + if (!process.stdin.isTTY && !process.env.PW_TEST_DEBUG_REPORTERS) + return; + + this._paused.add(result); + + this._updateTestLine(test, result); + this._maybeWriteNewLine(); + if (test.outcome() === 'unexpected') { + const errors = this.formatResultErrors(test, result); + this.writeLine(errors); + this._updateLineCountAndNewLineFlagForOutput(errors); + markErrorsAsReported(result); + } + this._appendLine(this.screen.colors.yellow(`Paused ${test.outcome() === 'unexpected' ? 'on error' : 'at test end'}. Press Ctrl+C to end.`), this._testPrefix('', '')); + + await new Promise(() => {}); + } + override onTestEnd(test: TestCase, result: TestResult) { super.onTestEnd(test, result); + const wasPaused = this._paused.delete(result); + if (!wasPaused) + this._updateTestLine(test, result); + } + private _updateTestLine(test: TestCase, result: TestResult) { const title = this.formatTestTitle(test); let prefix = ''; let text = ''; diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 4119c6d1b1695..33409b8445e3a 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -18,10 +18,6 @@ import type { APIRequestContext, Browser, BrowserContext, BrowserContextOptions, Page, PageAgent, LaunchOptions, ViewportSize, Geolocation, HTTPCredentials, Locator, APIResponse, PageScreenshotOptions } from 'playwright-core'; export * from 'playwright-core'; -// @ts-ignore ReactCSSProperties will be any if react is not installed -type ReactCSSProperties = import('react').CSSProperties; -export type CSSProperties = keyof ReactCSSProperties extends string ? ReactCSSProperties : never; - export type BlobReporterOptions = { outputDir?: string, fileName?: string }; export type ListReporterOptions = { printSteps?: boolean }; export type JUnitReporterOptions = { outputFile?: string, stripANSIControlSequences?: boolean, includeProjectInTestName?: boolean }; @@ -9184,32 +9180,6 @@ interface LocatorAssertions { timeout?: number; }): Promise; - /** - * Ensures the [Locator](https://playwright.dev/docs/api/class-locator) resolves to an element with the given computed - * CSS properties. - * - * **NOTE** The `CSSProperties` object parameter for toHaveCSS requires `react` to be installed for type checking. - * - * **Usage** - * - * ```js - * const locator = page.getByRole('button'); - * await expect(locator).toHaveCSS({ - * display: 'flex', - * backgroundColor: 'rgb(255, 0, 0)' - * }); - * ``` - * - * @param styles CSS properties object. - * @param options - */ - toHaveCSS(styles: CSSProperties, options?: { - /** - * Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`. - */ - timeout?: number; - }): Promise; - /** * Ensures the [Locator](https://playwright.dev/docs/api/class-locator) points to an element with the given DOM Node * ID. diff --git a/tests/library/agent-perform.spec.ts b/tests/library/agent-perform.spec.ts index d4f196d3a97bb..915915cd80a05 100644 --- a/tests/library/agent-perform.spec.ts +++ b/tests/library/agent-perform.spec.ts @@ -250,3 +250,48 @@ test('perform reports error', async ({ context }) => { const e = await agent.perform('click the Rabbit button').catch(e => e); expect(e.message).toContain('Agent refused to perform action:'); }); + +test('should dispatch event and respect dispose()', async ({ context, server }) => { + let apiResponse; + server.setRoute('/api', (req, res) => { + apiResponse = res; + // stall + }); + + const apiRequestPromise = server.waitForRequest('/api'); + const { page, agent } = await generateAgent(context, { + provider: { + api: 'anthropic', + apiKey: 'not a real key', + apiEndpoint: server.PREFIX + '/api', + model: 'no such model', + }, + }); + await page.setContent(``); + + const promiseCanceledByDispose = agent.perform('click the Wolf button').catch(e => e); + let promiseAfterDispose; + let eventCounter = 0; + + agent.on('turn', async () => { + ++eventCounter; + if (eventCounter > 1) + return; + + await apiRequestPromise; + void agent.dispose(); + promiseAfterDispose = agent.perform('click the Wolf button again').catch(e => e); + apiResponse.end(); + }); + + const errorCanceledByDispose = await promiseCanceledByDispose; + expect(errorCanceledByDispose.message).toContain('The agent is disposed'); + expect(errorCanceledByDispose.message).not.toContain('after being disposed'); + + const errorAfterDispose = await promiseAfterDispose; + expect(errorAfterDispose.message).toContain('Target page, context or browser has been closed'); + + // no more events after dispose + await page.waitForTimeout(1000); + expect(eventCounter).toBe(1); +}); diff --git a/tests/page/expect-misc.spec.ts b/tests/page/expect-misc.spec.ts index a3ec6a88a8638..e550d3ca72ee9 100644 --- a/tests/page/expect-misc.spec.ts +++ b/tests/page/expect-misc.spec.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { CSSProperties } from 'packages/playwright-test'; import { stripAnsi } from '../config/utils'; import { test, expect } from './pageTest'; @@ -508,44 +507,17 @@ Timeout: 1000ms`); }); test.describe('toHaveCSS', () => { - test('pass with css property', async ({ page }) => { + test('pass', async ({ page }) => { await page.setContent('
Text content
'); const locator = page.locator('#node'); await expect(locator).toHaveCSS('color', 'rgb(255, 0, 0)'); }); - test('pass with custom css property', async ({ page }) => { + test('custom css properties', async ({ page }) => { await page.setContent('
Text content
'); const locator = page.locator('#node'); await expect(locator).toHaveCSS('--custom-color-property', '#FF00FF'); }); - - test('pass with CSSPProperties object', async ({ page }) => { - await page.setContent('
Text content
'); - const locator = page.locator('#node'); - await expect(locator).toHaveCSS({ 'color': 'rgb(255, 0, 0)', 'display': 'flex' }); - }); - - test('pass with CSSPProperties object with camelCased properties', async ({ page }) => { - await page.setContent('
Text content
'); - const locator = page.locator('#node'); - await expect(locator).toHaveCSS({ 'backgroundColor': 'rgb(255, 0, 0)' }); - }); - - test('pass with CSSPProperties object with vendor-prefixed properties', async ({ page, browserName }) => { - await page.setContent('
Text content
'); - const locator = page.locator('#node'); - if (browserName === 'firefox') - await expect(locator).toHaveCSS({ 'MozTransform': 'matrix(0.707107, 0.707107, -0.707107, 0.707107, 0, 0)' }); - else - await expect(locator).toHaveCSS({ 'WebkitTransform': 'matrix(0.707107, 0.707107, -0.707107, 0.707107, 0, 0)' }); - }); - - test('pass with CSSPProperties object with custom properties', async ({ page }) => { - await page.setContent('
Text content
'); - const locator = page.locator('#node'); - await expect(locator).toHaveCSS({ '--my-color': 'blue' } as CSSProperties); - }); }); test.describe('toHaveId', () => { diff --git a/tests/playwright-test/reporter-list.spec.ts b/tests/playwright-test/reporter-list.spec.ts index 976ce6f2f9140..7d2a87f1600aa 100644 --- a/tests/playwright-test/reporter-list.spec.ts +++ b/tests/playwright-test/reporter-list.spec.ts @@ -416,6 +416,141 @@ for (const useIntermediateMergeReport of [false, true] as const) { }); } +test.describe('onTestPaused', () => { + test.skip(process.platform === 'win32', 'No SIGINT on windows'); + + test('pause at end', async ({ interactWithTestRunner }) => { + const runner = await interactWithTestRunner({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('foo', async ({}) => { + }); + + test.afterEach(() => { + console.log('Running teardown'); + }); + `, + }, { pause: true, reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1' }); + + await runner.waitForOutput('Paused at test end. Press Ctrl+C to end.'); + const { exitCode } = await runner.kill('SIGINT'); + expect(exitCode).toBe(130); + + const output = stripAnsi(runner.output).replace(/\d+(\.\d+)?m?s/g, 'Xms'); + expect(output).toEqual(` +Running 1 test using 1 worker + +#0 : ${POSITIVE_STATUS_MARK} 1 a.test.ts:3:13 › foo (Xms) +#1 : Paused at test end. Press Ctrl+C to end. +Running teardown + + 1 interrupted + a.test.ts:3:13 › foo ─────────────────────────────────────────────────────────────────────────── +`); + }); + + test('pause at end - error in teardown', async ({ interactWithTestRunner }) => { + const runner = await interactWithTestRunner({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('foo', async ({}) => { + }); + + test.afterEach(() => { + throw new Error('teardown error'); + }); + `, + }, { pause: true, reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1' }); + + await runner.waitForOutput('Paused at test end. Press Ctrl+C to end.'); + const { exitCode } = await runner.kill('SIGINT'); + expect(exitCode).toBe(130); + + const output = stripAnsi(runner.output).replace(/\d+(\.\d+)?m?s/g, 'Xms'); + expect(output).toEqual(` +Running 1 test using 1 worker + +#0 : ${POSITIVE_STATUS_MARK} 1 a.test.ts:3:13 › foo (Xms) +#1 : Paused at test end. Press Ctrl+C to end. + + + 1) a.test.ts:3:13 › foo ────────────────────────────────────────────────────────────────────────── + + Test was interrupted. + + Error: teardown error + + 5 | + 6 | test.afterEach(() => { + > 7 | throw new Error('teardown error'); + | ^ + 8 | }); + 9 | + at ${test.info().outputPath('a.test.ts')}:7:17 + + 1 interrupted + a.test.ts:3:13 › foo ─────────────────────────────────────────────────────────────────────────── +`); + }); + + test('pause on error', async ({ interactWithTestRunner }) => { + const runner = await interactWithTestRunner({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('fails', async ({}) => { + expect.soft(2).toBe(3); + expect(3).toBe(4); + }); + `, + }, { pause: true, reporter: 'list' }, { PW_TEST_DEBUG_REPORTERS: '1' }); + + await runner.waitForOutput('Paused on error. Press Ctrl+C to end.'); + const { exitCode } = await runner.kill('SIGINT'); + expect(exitCode).toBe(130); + + const output = stripAnsi(runner.output).replace(/\d+(\.\d+)?m?s/g, 'Xms'); + expect(output).toEqual(` +Running 1 test using 1 worker + +#0 : ${NEGATIVE_STATUS_MARK} 1 a.test.ts:3:13 › fails (Xms) + + Error: expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + + 2 | import { test, expect } from '@playwright/test'; + 3 | test('fails', async ({}) => { + > 4 | expect.soft(2).toBe(3); + | ^ + 5 | expect(3).toBe(4); + 6 | }); + 7 | + at ${test.info().outputPath('a.test.ts')}:4:26 + + Error: expect(received).toBe(expected) // Object.is equality + + Expected: 4 + Received: 3 + + 3 | test('fails', async ({}) => { + 4 | expect.soft(2).toBe(3); + > 5 | expect(3).toBe(4); + | ^ + 6 | }); + 7 | + at ${test.info().outputPath('a.test.ts')}:5:21 + +#1 : Paused on error. Press Ctrl+C to end. + + + + 1 failed + a.test.ts:3:13 › fails ───────────────────────────────────────────────────────────────────────── +`); + }); +}); + function simpleAnsiRenderer(text, ttyWidth) { let lineNumber = 0; let columnNumber = 0; diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 3a0eb2eb2d004..e71d6ebbf7790 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -17,10 +17,6 @@ import type { APIRequestContext, Browser, BrowserContext, BrowserContextOptions, Page, PageAgent, LaunchOptions, ViewportSize, Geolocation, HTTPCredentials, Locator, APIResponse, PageScreenshotOptions } from 'playwright-core'; export * from 'playwright-core'; -// @ts-ignore ReactCSSProperties will be any if react is not installed -type ReactCSSProperties = import('react').CSSProperties; -export type CSSProperties = keyof ReactCSSProperties extends string ? ReactCSSProperties : never; - export type BlobReporterOptions = { outputDir?: string, fileName?: string }; export type ListReporterOptions = { printSteps?: boolean }; export type JUnitReporterOptions = { outputFile?: string, stripANSIControlSequences?: boolean, includeProjectInTestName?: boolean };