Merge branch 'main' into webserver-sigkill

This commit is contained in:
Simon Knott 2024-12-22 11:54:08 +01:00
commit 71bce85f1f
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
65 changed files with 269 additions and 92 deletions

View file

@ -3,6 +3,9 @@ test/assets/modernizr.js
/packages/*/lib/ /packages/*/lib/
*.js *.js
/packages/playwright-core/src/generated/* /packages/playwright-core/src/generated/*
/packages/playwright-core/src/protocol/debug.ts
/packages/playwright-core/src/protocol/validator.ts
/packages/playwright-core/src/server/injected/recorder/clipPaths.ts
/packages/playwright-core/src/third_party/ /packages/playwright-core/src/third_party/
/packages/playwright-core/types/* /packages/playwright-core/types/*
/packages/playwright-ct-core/src/generated/* /packages/playwright-ct-core/src/generated/*

View file

@ -115,7 +115,7 @@ module.exports = {
"@typescript-eslint/type-annotation-spacing": 2, "@typescript-eslint/type-annotation-spacing": 2,
// file whitespace // file whitespace
"no-multiple-empty-lines": [2, {"max": 2}], "no-multiple-empty-lines": [2, {"max": 2, "maxEOF": 0}],
"no-mixed-spaces-and-tabs": 2, "no-mixed-spaces-and-tabs": 2,
"no-trailing-spaces": 2, "no-trailing-spaces": 2,
"linebreak-style": [ process.platform === "win32" ? 0 : 2, "unix" ], "linebreak-style": [ process.platform === "win32" ? 0 : 2, "unix" ],
@ -123,6 +123,7 @@ module.exports = {
"key-spacing": [2, { "key-spacing": [2, {
"beforeColon": false "beforeColon": false
}], }],
"eol-last": 2,
// copyright // copyright
"notice/notice": [2, { "notice/notice": [2, {

View file

@ -48,6 +48,23 @@ jobs:
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: csv-report name: csv-report-${{ matrix.channel }}
path: test-results/report.csv path: test-results/report.csv
retention-days: 7 retention-days: 7
- name: Azure Login
if: ${{ !cancelled() && github.ref == 'refs/heads/main' }}
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_BLOB_REPORTS_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_BLOB_REPORTS_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_BLOB_REPORTS_SUBSCRIPTION_ID }}
- name: Upload report.csv to Azure
if: ${{ !cancelled() && github.ref == 'refs/heads/main' }}
run: |
REPORT_DIR='bidi-reports'
azcopy cp "./test-results/report.csv" "https://mspwblobreport.blob.core.windows.net/\$web/$REPORT_DIR/${{ matrix.channel }}.csv"
echo "Report url: https://mspwblobreport.z1.web.core.windows.net/$REPORT_DIR/${{ matrix.channel }}.csv"
env:
AZCOPY_AUTO_LOGIN_TYPE: AZCLI

View file

@ -1717,16 +1717,21 @@ var banana = await page.GetByRole(AriaRole.Listitem).Nth(2);
Creates a locator matching all elements that match one or both of the two locators. Creates a locator matching all elements that match one or both of the two locators.
Note that when both locators match something, the resulting locator will have multiple matches and violate [locator strictness](../locators.md#strictness) guidelines. Note that when both locators match something, the resulting locator will have multiple matches, potentially causing a [locator strictness](../locators.md#strictness) violation.
**Usage** **Usage**
Consider a scenario where you'd like to click on a "New email" button, but sometimes a security settings dialog shows up instead. In this case, you can wait for either a "New email" button, or a dialog and act accordingly. Consider a scenario where you'd like to click on a "New email" button, but sometimes a security settings dialog shows up instead. In this case, you can wait for either a "New email" button, or a dialog and act accordingly.
:::note
If both "New email" button and security dialog appear on screen, the "or" locator will match both of them,
possibly throwing the ["strict mode violation" error](../locators.md#strictness). In this case, you can use [`method: Locator.first`] to only match one of them.
:::
```js ```js
const newEmail = page.getByRole('button', { name: 'New' }); const newEmail = page.getByRole('button', { name: 'New' });
const dialog = page.getByText('Confirm security settings'); const dialog = page.getByText('Confirm security settings');
await expect(newEmail.or(dialog)).toBeVisible(); await expect(newEmail.or(dialog).first()).toBeVisible();
if (await dialog.isVisible()) if (await dialog.isVisible())
await page.getByRole('button', { name: 'Dismiss' }).click(); await page.getByRole('button', { name: 'Dismiss' }).click();
await newEmail.click(); await newEmail.click();
@ -1735,7 +1740,7 @@ await newEmail.click();
```java ```java
Locator newEmail = page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("New")); Locator newEmail = page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("New"));
Locator dialog = page.getByText("Confirm security settings"); Locator dialog = page.getByText("Confirm security settings");
assertThat(newEmail.or(dialog)).isVisible(); assertThat(newEmail.or(dialog).first()).isVisible();
if (dialog.isVisible()) if (dialog.isVisible())
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Dismiss")).click(); page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Dismiss")).click();
newEmail.click(); newEmail.click();
@ -1744,7 +1749,7 @@ newEmail.click();
```python async ```python async
new_email = page.get_by_role("button", name="New") new_email = page.get_by_role("button", name="New")
dialog = page.get_by_text("Confirm security settings") dialog = page.get_by_text("Confirm security settings")
await expect(new_email.or_(dialog)).to_be_visible() await expect(new_email.or_(dialog).first).to_be_visible()
if (await dialog.is_visible()): if (await dialog.is_visible()):
await page.get_by_role("button", name="Dismiss").click() await page.get_by_role("button", name="Dismiss").click()
await new_email.click() await new_email.click()
@ -1753,7 +1758,7 @@ await new_email.click()
```python sync ```python sync
new_email = page.get_by_role("button", name="New") new_email = page.get_by_role("button", name="New")
dialog = page.get_by_text("Confirm security settings") dialog = page.get_by_text("Confirm security settings")
expect(new_email.or_(dialog)).to_be_visible() expect(new_email.or_(dialog).first).to_be_visible()
if (dialog.is_visible()): if (dialog.is_visible()):
page.get_by_role("button", name="Dismiss").click() page.get_by_role("button", name="Dismiss").click()
new_email.click() new_email.click()
@ -1762,7 +1767,7 @@ new_email.click()
```csharp ```csharp
var newEmail = page.GetByRole(AriaRole.Button, new() { Name = "New" }); var newEmail = page.GetByRole(AriaRole.Button, new() { Name = "New" });
var dialog = page.GetByText("Confirm security settings"); var dialog = page.GetByText("Confirm security settings");
await Expect(newEmail.Or(dialog)).ToBeVisibleAsync(); await Expect(newEmail.Or(dialog).First).ToBeVisibleAsync();
if (await dialog.IsVisibleAsync()) if (await dialog.IsVisibleAsync())
await page.GetByRole(AriaRole.Button, new() { Name = "Dismiss" }).ClickAsync(); await page.GetByRole(AriaRole.Button, new() { Name = "Dismiss" }).ClickAsync();
await newEmail.ClickAsync(); await newEmail.ClickAsync();

View file

@ -1003,7 +1003,7 @@ Additional arguments to pass to the browser instance. The list of Chromium flags
Browser distribution channel. Browser distribution channel.
Use "chromium" to [opt in to new headless mode](../browsers.md#opt-in-to-new-headless-mode). Use "chromium" to [opt in to new headless mode](../browsers.md#chromium-new-headless-mode).
Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or "msedge-canary" to use branded [Google Chrome and Microsoft Edge](../browsers.md#google-chrome--microsoft-edge). Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or "msedge-canary" to use branded [Google Chrome and Microsoft Edge](../browsers.md#google-chrome--microsoft-edge).

View file

@ -338,11 +338,11 @@ dotnet test --settings:webkit.runsettings
For Google Chrome, Microsoft Edge and other Chromium-based browsers, by default, Playwright uses open source Chromium builds. Since the Chromium project is ahead of the branded browsers, when the world is on Google Chrome N, Playwright already supports Chromium N+1 that will be released in Google Chrome and Microsoft Edge a few weeks later. For Google Chrome, Microsoft Edge and other Chromium-based browsers, by default, Playwright uses open source Chromium builds. Since the Chromium project is ahead of the branded browsers, when the world is on Google Chrome N, Playwright already supports Chromium N+1 that will be released in Google Chrome and Microsoft Edge a few weeks later.
Playwright ships a regular Chromium build for headed operations and a separate [chromium headless shell](https://developer.chrome.com/blog/chrome-headless-shell) for headless mode. See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for details. ### Chromium: headless shell
#### Optimize download size on CI Playwright ships a regular Chromium build for headed operations and a separate [chromium headless shell](https://developer.chrome.com/blog/chrome-headless-shell) for headless mode.
If you are only running tests in headless shell (i.e. the `channel` option is not specified), for example on CI, you can avoid downloading the full Chromium browser by passing `--only-shell` during installation. If you are only running tests in headless shell (i.e. the `channel` option is **not** specified), for example on CI, you can avoid downloading the full Chromium browser by passing `--only-shell` during installation.
```bash js ```bash js
# only running tests headlessly # only running tests headlessly
@ -364,7 +364,7 @@ playwright install --with-deps --only-shell
pwsh bin/Debug/netX/playwright.ps1 install --with-deps --only-shell pwsh bin/Debug/netX/playwright.ps1 install --with-deps --only-shell
``` ```
#### Opt-in to new headless mode ### Chromium: new headless mode
You can opt into the new headless mode by using `'chromium'` channel. As [official Chrome documentation puts it](https://developer.chrome.com/blog/chrome-headless-shell): You can opt into the new headless mode by using `'chromium'` channel. As [official Chrome documentation puts it](https://developer.chrome.com/blog/chrome-headless-shell):
@ -419,6 +419,28 @@ pytest test_login.py --browser-channel chromium
dotnet test -- Playwright.BrowserName=chromium Playwright.LaunchOptions.Channel=chromium dotnet test -- Playwright.BrowserName=chromium Playwright.LaunchOptions.Channel=chromium
``` ```
With the new headless mode, you can skip downloading the headless shell during browser installation by using the `--no-shell` option:
```bash js
# only running tests headlessly
npx playwright install --with-deps --no-shell
```
```bash java
# only running tests headlessly
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps --no-shell"
```
```bash python
# only running tests headlessly
playwright install --with-deps --no-shell
```
```bash csharp
# only running tests headlessly
pwsh bin/Debug/netX/playwright.ps1 install --with-deps --no-shell
```
### Google Chrome & Microsoft Edge ### Google Chrome & Microsoft Edge
While Playwright can download and use the recent Chromium build, it can operate against the branded Google Chrome and Microsoft Edge browsers available on the machine (note that Playwright doesn't install them by default). In particular, the current Playwright version will support Stable and Beta channels of these browsers. While Playwright can download and use the recent Chromium build, it can operate against the branded Google Chrome and Microsoft Edge browsers available on the machine (note that Playwright doesn't install them by default). In particular, the current Playwright version will support Stable and Beta channels of these browsers.

View file

@ -214,7 +214,7 @@ def test_popup_page(page: Page, extension_id: str) -> None:
## Headless mode ## Headless mode
By default, Chrome's headless mode in Playwright does not support Chrome extensions. To overcome this limitation, you can run Chrome's persistent context with a new headless mode by using [channel `chromium`](./browsers.md#opt-in-to-new-headless-mode): By default, Chrome's headless mode in Playwright does not support Chrome extensions. To overcome this limitation, you can run Chrome's persistent context with a new headless mode by using [channel `chromium`](./browsers.md#chromium-new-headless-mode):
```js title="fixtures.ts" ```js title="fixtures.ts"
// ... // ...

View file

@ -47,4 +47,3 @@ export function hashStringToInt(str: string) {
hash = str.charCodeAt(i) + ((hash << 8) - hash); hash = str.charCodeAt(i) + ((hash << 8) - hash);
return Math.abs(hash % 6); return Math.abs(hash % 6);
} }

View file

@ -124,4 +124,3 @@ export class FastStats implements Stats {
return (this._sum(this._partialSumMult, x1, y1, x2, y2) - this._sum(this._partialSumC1, x1, y1, x2, y2) * this._sum(this._partialSumC2, x1, y1, x2, y2) / N) / N; return (this._sum(this._partialSumMult, x1, y1, x2, y2) - this._sum(this._partialSumC1, x1, y1, x2, y2) * this._sum(this._partialSumC2, x1, y1, x2, y2) / N) / N;
} }
} }

View file

@ -524,5 +524,3 @@ class ClankBrowserProcess implements BrowserProcess {
await this._browser.close(); await this._browser.close();
} }
} }

View file

@ -436,4 +436,3 @@ function toJugglerProxyOptions(proxy: types.ProxySettings) {
// Prefs for quick fixes that didn't make it to the build. // Prefs for quick fixes that didn't make it to the build.
// Should all be moved to `playwright.cfg`. // Should all be moved to `playwright.cfg`.
const kBandaidFirefoxUserPrefs = {}; const kBandaidFirefoxUserPrefs = {};

View file

@ -101,4 +101,3 @@ class JugglerReadyState extends BrowserReadyState {
this._wsEndpoint.resolve(undefined); this._wsEndpoint.resolve(undefined);
} }
} }

View file

@ -1104,4 +1104,3 @@ deps['debian12-arm64'] = {
...deps['debian12-x64'].lib2package, ...deps['debian12-x64'].lib2package,
}, },
}; };

View file

@ -83,4 +83,3 @@ export class SocksInterceptor {
function tChannelForSocks(names: '*' | string[], arg: any, path: string, context: ValidatorContext) { function tChannelForSocks(names: '*' | string[], arg: any, path: string, context: ValidatorContext) {
throw new ValidationError(`${path}: channels are not expected in SocksSupport`); throw new ValidationError(`${path}: channels are not expected in SocksSupport`);
} }

View file

@ -77,4 +77,3 @@ function parseOSReleaseText(osReleaseText: string): Map<string, string> {
} }
return fields; return fields;
} }

View file

@ -13853,18 +13853,22 @@ export interface Locator {
/** /**
* Creates a locator matching all elements that match one or both of the two locators. * Creates a locator matching all elements that match one or both of the two locators.
* *
* Note that when both locators match something, the resulting locator will have multiple matches and violate * Note that when both locators match something, the resulting locator will have multiple matches, potentially causing
* [locator strictness](https://playwright.dev/docs/locators#strictness) guidelines. * a [locator strictness](https://playwright.dev/docs/locators#strictness) violation.
* *
* **Usage** * **Usage**
* *
* Consider a scenario where you'd like to click on a "New email" button, but sometimes a security settings dialog * Consider a scenario where you'd like to click on a "New email" button, but sometimes a security settings dialog
* shows up instead. In this case, you can wait for either a "New email" button, or a dialog and act accordingly. * shows up instead. In this case, you can wait for either a "New email" button, or a dialog and act accordingly.
* *
* **NOTE** If both "New email" button and security dialog appear on screen, the "or" locator will match both of them,
* possibly throwing the ["strict mode violation" error](https://playwright.dev/docs/locators#strictness). In this case, you can use
* [locator.first()](https://playwright.dev/docs/api/class-locator#locator-first) to only match one of them.
*
* ```js * ```js
* const newEmail = page.getByRole('button', { name: 'New' }); * const newEmail = page.getByRole('button', { name: 'New' });
* const dialog = page.getByText('Confirm security settings'); * const dialog = page.getByText('Confirm security settings');
* await expect(newEmail.or(dialog)).toBeVisible(); * await expect(newEmail.or(dialog).first()).toBeVisible();
* if (await dialog.isVisible()) * if (await dialog.isVisible())
* await page.getByRole('button', { name: 'Dismiss' }).click(); * await page.getByRole('button', { name: 'Dismiss' }).click();
* await newEmail.click(); * await newEmail.click();
@ -14716,7 +14720,7 @@ export interface BrowserType<Unused = {}> {
/** /**
* Browser distribution channel. * Browser distribution channel.
* *
* Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#opt-in-to-new-headless-mode). * Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#chromium-new-headless-mode).
* *
* Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or * Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or
* "msedge-canary" to use branded [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge). * "msedge-canary" to use branded [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge).
@ -15215,7 +15219,7 @@ export interface BrowserType<Unused = {}> {
/** /**
* Browser distribution channel. * Browser distribution channel.
* *
* Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#opt-in-to-new-headless-mode). * Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#chromium-new-headless-mode).
* *
* Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or * Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or
* "msedge-canary" to use branded [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge). * "msedge-canary" to use branded [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge).
@ -21566,7 +21570,7 @@ export interface LaunchOptions {
/** /**
* Browser distribution channel. * Browser distribution channel.
* *
* Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#opt-in-to-new-headless-mode). * Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#chromium-new-headless-mode).
* *
* Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or * Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or
* "msedge-canary" to use branded [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge). * "msedge-canary" to use branded [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge).

View file

@ -21,7 +21,7 @@ import path from 'path';
import type { TransformCallback } from 'stream'; import type { TransformCallback } from 'stream';
import { Transform } from 'stream'; import { Transform } from 'stream';
import { codeFrameColumns } from '../transform/babelBundle'; import { codeFrameColumns } from '../transform/babelBundle';
import type { FullResult, FullConfig, Location, Suite, TestCase as TestCasePublic, TestResult as TestResultPublic, TestStep as TestStepPublic, TestError } from '../../types/testReporter'; import type * as api from '../../types/testReporter';
import { HttpServer, assert, calculateSha1, copyFileAndMakeWritable, gracefullyProcessExitDoNotHang, removeFolders, sanitizeForFilePath, toPosixPath } from 'playwright-core/lib/utils'; import { HttpServer, assert, calculateSha1, copyFileAndMakeWritable, gracefullyProcessExitDoNotHang, removeFolders, sanitizeForFilePath, toPosixPath } from 'playwright-core/lib/utils';
import { colors, formatError, formatResultFailure, stripAnsiEscapes } from './base'; import { colors, formatError, formatResultFailure, stripAnsiEscapes } from './base';
import { resolveReporterOutputPath } from '../util'; import { resolveReporterOutputPath } from '../util';
@ -56,8 +56,8 @@ type HtmlReporterOptions = {
}; };
class HtmlReporter implements ReporterV2 { class HtmlReporter implements ReporterV2 {
private config!: FullConfig; private config!: api.FullConfig;
private suite!: Suite; private suite!: api.Suite;
private _options: HtmlReporterOptions; private _options: HtmlReporterOptions;
private _outputFolder!: string; private _outputFolder!: string;
private _attachmentsBaseURL!: string; private _attachmentsBaseURL!: string;
@ -65,7 +65,7 @@ class HtmlReporter implements ReporterV2 {
private _port: number | undefined; private _port: number | undefined;
private _host: string | undefined; private _host: string | undefined;
private _buildResult: { ok: boolean, singleTestId: string | undefined } | undefined; private _buildResult: { ok: boolean, singleTestId: string | undefined } | undefined;
private _topLevelErrors: TestError[] = []; private _topLevelErrors: api.TestError[] = [];
constructor(options: HtmlReporterOptions) { constructor(options: HtmlReporterOptions) {
this._options = options; this._options = options;
@ -79,11 +79,11 @@ class HtmlReporter implements ReporterV2 {
return false; return false;
} }
onConfigure(config: FullConfig) { onConfigure(config: api.FullConfig) {
this.config = config; this.config = config;
} }
onBegin(suite: Suite) { onBegin(suite: api.Suite) {
const { outputFolder, open, attachmentsBaseURL, host, port } = this._resolveOptions(); const { outputFolder, open, attachmentsBaseURL, host, port } = this._resolveOptions();
this._outputFolder = outputFolder; this._outputFolder = outputFolder;
this._open = open; this._open = open;
@ -125,11 +125,11 @@ class HtmlReporter implements ReporterV2 {
return !!relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath); return !!relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
} }
onError(error: TestError): void { onError(error: api.TestError): void {
this._topLevelErrors.push(error); this._topLevelErrors.push(error);
} }
async onEnd(result: FullResult) { async onEnd(result: api.FullResult) {
const projectSuites = this.suite.suites; const projectSuites = this.suite.suites;
await removeFolders([this._outputFolder]); await removeFolders([this._outputFolder]);
const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL); const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL);
@ -223,14 +223,14 @@ export function startHtmlReportServer(folder: string): HttpServer {
} }
class HtmlBuilder { class HtmlBuilder {
private _config: FullConfig; private _config: api.FullConfig;
private _reportFolder: string; private _reportFolder: string;
private _stepsInFile = new MultiMap<string, TestStep>(); private _stepsInFile = new MultiMap<string, TestStep>();
private _dataZipFile: ZipFile; private _dataZipFile: ZipFile;
private _hasTraces = false; private _hasTraces = false;
private _attachmentsBaseURL: string; private _attachmentsBaseURL: string;
constructor(config: FullConfig, outputDir: string, attachmentsBaseURL: string) { constructor(config: api.FullConfig, outputDir: string, attachmentsBaseURL: string) {
this._config = config; this._config = config;
this._reportFolder = outputDir; this._reportFolder = outputDir;
fs.mkdirSync(this._reportFolder, { recursive: true }); fs.mkdirSync(this._reportFolder, { recursive: true });
@ -238,7 +238,7 @@ class HtmlBuilder {
this._attachmentsBaseURL = attachmentsBaseURL; this._attachmentsBaseURL = attachmentsBaseURL;
} }
async build(metadata: Metadata, projectSuites: Suite[], result: FullResult, topLevelErrors: TestError[]): Promise<{ ok: boolean, singleTestId: string | undefined }> { async build(metadata: Metadata, projectSuites: api.Suite[], result: api.FullResult, topLevelErrors: api.TestError[]): Promise<{ ok: boolean, singleTestId: string | undefined }> {
const data = new Map<string, { testFile: TestFile, testFileSummary: TestFileSummary }>(); const data = new Map<string, { testFile: TestFile, testFileSummary: TestFileSummary }>();
for (const projectSuite of projectSuites) { for (const projectSuite of projectSuites) {
for (const fileSuite of projectSuite.suites) { for (const fileSuite of projectSuite.suites) {
@ -378,7 +378,7 @@ class HtmlBuilder {
this._dataZipFile.addBuffer(Buffer.from(JSON.stringify(data)), fileName); this._dataZipFile.addBuffer(Buffer.from(JSON.stringify(data)), fileName);
} }
private _processSuite(suite: Suite, projectName: string, path: string[], outTests: TestEntry[]) { private _processSuite(suite: api.Suite, projectName: string, path: string[], outTests: TestEntry[]) {
const newPath = [...path, suite.title]; const newPath = [...path, suite.title];
suite.entries().forEach(e => { suite.entries().forEach(e => {
if (e.type === 'test') if (e.type === 'test')
@ -388,7 +388,7 @@ class HtmlBuilder {
}); });
} }
private _createTestEntry(test: TestCasePublic, projectName: string, path: string[]): TestEntry { private _createTestEntry(test: api.TestCase, projectName: string, path: string[]): TestEntry {
const duration = test.results.reduce((a, r) => a + r.duration, 0); const duration = test.results.reduce((a, r) => a + r.duration, 0);
const location = this._relativeLocation(test.location)!; const location = this._relativeLocation(test.location)!;
path = path.slice(1).filter(path => path.length > 0); path = path.slice(1).filter(path => path.length > 0);
@ -500,7 +500,7 @@ class HtmlBuilder {
}).filter(Boolean) as TestAttachment[]; }).filter(Boolean) as TestAttachment[];
} }
private _createTestResult(test: TestCasePublic, result: TestResultPublic): TestResult { private _createTestResult(test: api.TestCase, result: api.TestResult): TestResult {
return { return {
duration: result.duration, duration: result.duration,
startTime: result.startTime.toISOString(), startTime: result.startTime.toISOString(),
@ -531,7 +531,7 @@ class HtmlBuilder {
return result; return result;
} }
private _relativeLocation(location: Location | undefined): Location | undefined { private _relativeLocation(location: api.Location | undefined): api.Location | undefined {
if (!location) if (!location)
return undefined; return undefined;
const file = toPosixPath(path.relative(this._config.rootDir, location.file)); const file = toPosixPath(path.relative(this._config.rootDir, location.file));
@ -609,9 +609,9 @@ function stdioAttachment(chunk: Buffer | string, type: 'stdout' | 'stderr'): Jso
}; };
} }
type DedupedStep = { step: TestStepPublic, count: number, duration: number }; type DedupedStep = { step: api.TestStep, count: number, duration: number };
function dedupeSteps(steps: TestStepPublic[]) { function dedupeSteps(steps: api.TestStep[]) {
const result: DedupedStep[] = []; const result: DedupedStep[] = [];
let lastResult = undefined; let lastResult = undefined;
for (const step of steps) { for (const step of steps) {

View file

@ -6002,7 +6002,7 @@ export interface PlaywrightWorkerOptions {
/** /**
* Browser distribution channel. * Browser distribution channel.
* *
* Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#opt-in-to-new-headless-mode). * Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#chromium-new-headless-mode).
* *
* Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or * Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or
* "msedge-canary" to use branded [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge). * "msedge-canary" to use branded [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge).

View file

@ -0,0 +1,145 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as React from 'react';
export interface DialogProps {
className?: string;
open: boolean;
width: number;
verticalOffset?: number;
requestClose?: () => void;
anchor?: React.RefObject<HTMLElement>;
}
export const Dialog: React.FC<React.PropsWithChildren<DialogProps>> = ({
className,
open,
width,
verticalOffset,
requestClose,
anchor,
children,
}) => {
const dialogRef = React.useRef<HTMLDialogElement>(null);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, setRecalculateDimensionsCount] = React.useState(0);
let style: React.CSSProperties | undefined = undefined;
if (anchor?.current) {
const bounds = anchor.current.getBoundingClientRect();
style = {
margin: 0,
top: bounds.bottom + (verticalOffset ?? 0),
left: buildTopLeftCoord(bounds, width),
width,
zIndex: 1,
};
}
React.useEffect(() => {
const onClick = (event: MouseEvent) => {
if (!dialogRef.current || !(event.target instanceof Node))
return;
if (!dialogRef.current.contains(event.target))
requestClose?.();
};
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape')
requestClose?.();
};
if (open) {
document.addEventListener('mousedown', onClick);
document.addEventListener('keydown', onKeyDown);
return () => {
document.removeEventListener('mousedown', onClick);
document.removeEventListener('keydown', onKeyDown);
};
}
return () => {};
}, [open, requestClose]);
React.useEffect(() => {
const onResize = () => setRecalculateDimensionsCount(count => count + 1);
window.addEventListener('resize', onResize);
return () => {
window.removeEventListener('resize', onResize);
};
}, []);
return (
open && (
<dialog ref={dialogRef} style={style} className={className} open>
{children}
</dialog>
)
);
};
const buildTopLeftCoord = (bounds: DOMRect, width: number): number => {
const leftAlignCoord = buildTopLeftCoordWithAlignment(bounds, width, 'left');
if (leftAlignCoord.inBounds)
return leftAlignCoord.value;
const rightAlignCoord = buildTopLeftCoordWithAlignment(
bounds,
width,
'right'
);
if (rightAlignCoord.inBounds)
return rightAlignCoord.value;
return leftAlignCoord.value;
};
const buildTopLeftCoordWithAlignment = (
bounds: DOMRect,
width: number,
alignment: 'left' | 'right'
): {
value: number;
inBounds: boolean;
} => {
const maxLeft = document.documentElement.clientWidth;
if (alignment === 'left') {
const value = bounds.left;
return {
value,
inBounds: value + width <= maxLeft,
};
} else {
const value = bounds.right - width;
return {
value,
inBounds: bounds.right - width >= 0,
};
}
};

View file

@ -90,4 +90,3 @@ test('drag resize', async ({ page, mount }) => {
expect.soft(mainBox).toEqual({ x: 0, y: 0, width: 500, height: 100 }); expect.soft(mainBox).toEqual({ x: 0, y: 0, width: 500, height: 100 });
expect.soft(sidebarBox).toEqual({ x: 0, y: 101, width: 500, height: 399 }); expect.soft(sidebarBox).toEqual({ x: 0, y: 101, width: 500, height: 399 });
}); });

View file

@ -180,4 +180,3 @@ it('evaluateHandle should work', async ({ page, server }) => {
const windowHandle = await mainFrame.evaluateHandle(() => window); const windowHandle = await mainFrame.evaluateHandle(() => window);
expect(windowHandle).toBeTruthy(); expect(windowHandle).toBeTruthy();
}); });

View file

@ -173,4 +173,3 @@ it('Locator.locator() and FrameLocator.locator() should accept locator', async (
expect(await divLocator.locator('input').inputValue()).toBe('outer'); expect(await divLocator.locator('input').inputValue()).toBe('outer');
expect(await page.frameLocator('iframe').locator(divLocator).locator('input').inputValue()).toBe('inner'); expect(await page.frameLocator('iframe').locator(divLocator).locator('input').inputValue()).toBe('inner');
}); });

View file

@ -309,4 +309,3 @@ it('should dispatch mouse move after context menu was opened', async ({ page, br
} }
} }
}); });

View file

@ -485,4 +485,3 @@ it('should not go to the network for fulfilled requests body', {
expect(body).toBeTruthy(); expect(body).toBeTruthy();
expect(serverHit).toBe(false); expect(serverHit).toBe(false);
}); });

View file

@ -176,4 +176,3 @@ for (const [name, url] of Object.entries(reacts)) {
}); });
}); });
} }

View file

@ -168,4 +168,3 @@ for (const [name, url] of Object.entries(vues)) {
}); });
}); });
} }

View file

@ -427,4 +427,3 @@ test('exits successfully if there are no changes', async ({ runInlineTest, git,
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
}); });

View file

@ -319,4 +319,3 @@ function simpleAnsiRenderer(text, ttyWidth) {
return screenLines.map(line => line.join('')).join('\n'); return screenLines.map(line => line.join('')).join('\n');
} }

View file

@ -137,4 +137,3 @@ test('arg should receive default arg', async ({ runInlineTest }, testInfo) => {
expect(result.output).toContain(`A snapshot doesn't exist at ${snapshotOutputPath}, writing actual`); expect(result.output).toContain(`A snapshot doesn't exist at ${snapshotOutputPath}, writing actual`);
expect(fs.existsSync(snapshotOutputPath)).toBe(true); expect(fs.existsSync(snapshotOutputPath)).toBe(true);
}); });

View file

@ -186,4 +186,3 @@ test('test.use() should throw if called from beforeAll ', async ({ runInlineTest
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.output).toContain('Playwright Test did not expect test.use() to be called here'); expect(result.output).toContain('Playwright Test did not expect test.use() to be called here');
}); });

View file

@ -46,4 +46,3 @@ test('should display annotations', async ({ runUITest }) => {
await expect(annotations.locator('.annotation-item').filter({ hasText: 'test repo' }).locator('a')) await expect(annotations.locator('.annotation-item').filter({ hasText: 'test repo' }).locator('a'))
.toHaveAttribute('href', 'https://github.com/microsoft/playwright'); .toHaveAttribute('href', 'https://github.com/microsoft/playwright');
}); });

View file

@ -153,6 +153,7 @@ class JSLintingService extends LintingService {
'@typescript-eslint/no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': 'off',
'max-len': ['error', { code: 100 }], 'max-len': ['error', { code: 100 }],
'react/react-in-jsx-scope': 'off', 'react/react-in-jsx-scope': 'off',
'eol-last': 'off',
}, },
} }
}); });