Merge branch 'main' into webserver-sigkill
This commit is contained in:
commit
71bce85f1f
|
|
@ -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/*
|
||||||
|
|
|
||||||
|
|
@ -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, {
|
||||||
|
|
|
||||||
19
.github/workflows/tests_bidi.yml
vendored
19
.github/workflows/tests_bidi.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
// ...
|
// ...
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -524,5 +524,3 @@ class ClankBrowserProcess implements BrowserProcess {
|
||||||
await this._browser.close();
|
await this._browser.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = {};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,4 +101,3 @@ class JugglerReadyState extends BrowserReadyState {
|
||||||
this._wsEndpoint.resolve(undefined);
|
this._wsEndpoint.resolve(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1104,4 +1104,3 @@ deps['debian12-arm64'] = {
|
||||||
...deps['debian12-x64'].lib2package,
|
...deps['debian12-x64'].lib2package,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,4 +77,3 @@ function parseOSReleaseText(osReleaseText: string): Map<string, string> {
|
||||||
}
|
}
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
16
packages/playwright-core/types/types.d.ts
vendored
16
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
2
packages/playwright/types/test.d.ts
vendored
2
packages/playwright/types/test.d.ts
vendored
|
|
@ -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).
|
||||||
|
|
|
||||||
145
packages/trace-viewer/src/ui/shared/dialog.tsx
Normal file
145
packages/trace-viewer/src/ui/shared/dialog.tsx
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -309,4 +309,3 @@ it('should dispatch mouse move after context menu was opened', async ({ page, br
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -176,4 +176,3 @@ for (const [name, url] of Object.entries(reacts)) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -168,4 +168,3 @@ for (const [name, url] of Object.entries(vues)) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -319,4 +319,3 @@ function simpleAnsiRenderer(text, ttyWidth) {
|
||||||
|
|
||||||
return screenLines.map(line => line.join('')).join('\n');
|
return screenLines.map(line => line.join('')).join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue