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/
*.js
/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/types/*
/packages/playwright-ct-core/src/generated/*

View file

@ -115,7 +115,7 @@ module.exports = {
"@typescript-eslint/type-annotation-spacing": 2,
// 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-trailing-spaces": 2,
"linebreak-style": [ process.platform === "win32" ? 0 : 2, "unix" ],
@ -123,6 +123,7 @@ module.exports = {
"key-spacing": [2, {
"beforeColon": false
}],
"eol-last": 2,
// copyright
"notice/notice": [2, {

View file

@ -48,6 +48,23 @@ jobs:
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: csv-report
name: csv-report-${{ matrix.channel }}
path: test-results/report.csv
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.
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**
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
const newEmail = page.getByRole('button', { name: 'New' });
const dialog = page.getByText('Confirm security settings');
await expect(newEmail.or(dialog)).toBeVisible();
await expect(newEmail.or(dialog).first()).toBeVisible();
if (await dialog.isVisible())
await page.getByRole('button', { name: 'Dismiss' }).click();
await newEmail.click();
@ -1735,7 +1740,7 @@ await newEmail.click();
```java
Locator newEmail = page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("New"));
Locator dialog = page.getByText("Confirm security settings");
assertThat(newEmail.or(dialog)).isVisible();
assertThat(newEmail.or(dialog).first()).isVisible();
if (dialog.isVisible())
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Dismiss")).click();
newEmail.click();
@ -1744,7 +1749,7 @@ newEmail.click();
```python async
new_email = page.get_by_role("button", name="New")
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()):
await page.get_by_role("button", name="Dismiss").click()
await new_email.click()
@ -1753,7 +1758,7 @@ await new_email.click()
```python sync
new_email = page.get_by_role("button", name="New")
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()):
page.get_by_role("button", name="Dismiss").click()
new_email.click()
@ -1762,7 +1767,7 @@ new_email.click()
```csharp
var newEmail = page.GetByRole(AriaRole.Button, new() { Name = "New" });
var dialog = page.GetByText("Confirm security settings");
await Expect(newEmail.Or(dialog)).ToBeVisibleAsync();
await Expect(newEmail.Or(dialog).First).ToBeVisibleAsync();
if (await dialog.IsVisibleAsync())
await page.GetByRole(AriaRole.Button, new() { Name = "Dismiss" }).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.
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).

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.
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
# 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
```
#### 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):
@ -419,6 +419,28 @@ pytest test_login.py --browser-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
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
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"
// ...

View file

@ -51,4 +51,4 @@ export function bundle(): Plugin {
}
},
};
}
}

View file

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

View file

@ -20,4 +20,4 @@ export type Rect = Size & Point;
export type Quad = [ Point, Point, Point, Point ];
export type TimeoutOptions = { timeout?: number };
export type NameValue = { name: string, value: string };
export type HeadersArray = NameValue[];
export type HeadersArray = NameValue[];

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;
}
}

View file

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

View file

@ -77,4 +77,4 @@ export async function prepareFilesForUpload(frame: Frame, params: channels.Eleme
}));
return { localPaths, localDirectory, filePayloads };
}
}

View file

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

View file

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

View file

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

View file

@ -354,4 +354,4 @@ export function rewriteOpenSSLErrorIfNeeded(error: Error): Error {
'For more details, see https://github.com/openssl/openssl/blob/master/README-PROVIDERS.md#the-legacy-provider',
'You could probably modernize the certificate by following the steps at https://github.com/nodejs/node/issues/40672#issuecomment-1243648223',
].join('\n'));
}
}

View file

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

View file

@ -105,4 +105,4 @@ export class WKProvisionalPage {
assert(!frameTree.frame.parentId);
this._mainFrameId = frameTree.frame.id;
}
}
}

View file

@ -20,4 +20,4 @@ export function isJsonMimeType(mimeType: string) {
export function isTextualMimeType(mimeType: string) {
return !!mimeType.match(/^(text\/.*?|application\/(json|(x-)?javascript|xml.*?|ecmascript|graphql|x-www-form-urlencoded)|image\/svg(\+xml)?|application\/.*?(\+json|\+xml))(;\s*charset=.*)?$/);
}
}

View file

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

View file

@ -63,4 +63,4 @@ export function findRepeatedSubsequences(s: string[]): { sequence: string[]; cou
}
return result;
}
}

View file

@ -13853,18 +13853,22 @@ export interface Locator {
/**
* 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](https://playwright.dev/docs/locators#strictness) guidelines.
* Note that when both locators match something, the resulting locator will have multiple matches, potentially causing
* a [locator strictness](https://playwright.dev/docs/locators#strictness) violation.
*
* **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.
*
* **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
* const newEmail = page.getByRole('button', { name: 'New' });
* const dialog = page.getByText('Confirm security settings');
* await expect(newEmail.or(dialog)).toBeVisible();
* await expect(newEmail.or(dialog).first()).toBeVisible();
* if (await dialog.isVisible())
* await page.getByRole('button', { name: 'Dismiss' }).click();
* await newEmail.click();
@ -14716,7 +14720,7 @@ export interface BrowserType<Unused = {}> {
/**
* 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
* "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.
*
* 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
* "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.
*
* 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
* "msedge-canary" to use branded [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge).

View file

@ -18,4 +18,4 @@ import jsxRuntime from './jsx-runtime.js';
export const jsx = jsxRuntime.jsx;
export const jsxs = jsxRuntime.jsxs;
export const Fragment = jsxRuntime.Fragment;
export const Fragment = jsxRuntime.Fragment;

View file

@ -298,4 +298,4 @@ const configInternalSymbol = Symbol('configInternalSymbol');
export function getProjectId(project: FullProject): string {
return (project as any).__projectId!;
}
}

View file

@ -21,7 +21,7 @@ import path from 'path';
import type { TransformCallback } from 'stream';
import { Transform } from 'stream';
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 { colors, formatError, formatResultFailure, stripAnsiEscapes } from './base';
import { resolveReporterOutputPath } from '../util';
@ -56,8 +56,8 @@ type HtmlReporterOptions = {
};
class HtmlReporter implements ReporterV2 {
private config!: FullConfig;
private suite!: Suite;
private config!: api.FullConfig;
private suite!: api.Suite;
private _options: HtmlReporterOptions;
private _outputFolder!: string;
private _attachmentsBaseURL!: string;
@ -65,7 +65,7 @@ class HtmlReporter implements ReporterV2 {
private _port: number | undefined;
private _host: string | undefined;
private _buildResult: { ok: boolean, singleTestId: string | undefined } | undefined;
private _topLevelErrors: TestError[] = [];
private _topLevelErrors: api.TestError[] = [];
constructor(options: HtmlReporterOptions) {
this._options = options;
@ -79,11 +79,11 @@ class HtmlReporter implements ReporterV2 {
return false;
}
onConfigure(config: FullConfig) {
onConfigure(config: api.FullConfig) {
this.config = config;
}
onBegin(suite: Suite) {
onBegin(suite: api.Suite) {
const { outputFolder, open, attachmentsBaseURL, host, port } = this._resolveOptions();
this._outputFolder = outputFolder;
this._open = open;
@ -125,11 +125,11 @@ class HtmlReporter implements ReporterV2 {
return !!relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
}
onError(error: TestError): void {
onError(error: api.TestError): void {
this._topLevelErrors.push(error);
}
async onEnd(result: FullResult) {
async onEnd(result: api.FullResult) {
const projectSuites = this.suite.suites;
await removeFolders([this._outputFolder]);
const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL);
@ -223,14 +223,14 @@ export function startHtmlReportServer(folder: string): HttpServer {
}
class HtmlBuilder {
private _config: FullConfig;
private _config: api.FullConfig;
private _reportFolder: string;
private _stepsInFile = new MultiMap<string, TestStep>();
private _dataZipFile: ZipFile;
private _hasTraces = false;
private _attachmentsBaseURL: string;
constructor(config: FullConfig, outputDir: string, attachmentsBaseURL: string) {
constructor(config: api.FullConfig, outputDir: string, attachmentsBaseURL: string) {
this._config = config;
this._reportFolder = outputDir;
fs.mkdirSync(this._reportFolder, { recursive: true });
@ -238,7 +238,7 @@ class HtmlBuilder {
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 }>();
for (const projectSuite of projectSuites) {
for (const fileSuite of projectSuite.suites) {
@ -378,7 +378,7 @@ class HtmlBuilder {
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];
suite.entries().forEach(e => {
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 location = this._relativeLocation(test.location)!;
path = path.slice(1).filter(path => path.length > 0);
@ -500,7 +500,7 @@ class HtmlBuilder {
}).filter(Boolean) as TestAttachment[];
}
private _createTestResult(test: TestCasePublic, result: TestResultPublic): TestResult {
private _createTestResult(test: api.TestCase, result: api.TestResult): TestResult {
return {
duration: result.duration,
startTime: result.startTime.toISOString(),
@ -531,7 +531,7 @@ class HtmlBuilder {
return result;
}
private _relativeLocation(location: Location | undefined): Location | undefined {
private _relativeLocation(location: api.Location | undefined): api.Location | undefined {
if (!location)
return undefined;
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[] = [];
let lastResult = undefined;
for (const step of steps) {

View file

@ -54,4 +54,4 @@ export async function detectChangedTestFiles(baseCommit: string, configDir: stri
const trackedFilesWithChanges = gitFileList(`diff ${baseCommit} --name-only`).map(file => path.join(gitRoot, file));
return new Set(affectedTestFiles([...untrackedFiles, ...trackedFilesWithChanges]));
}
}

View file

@ -6002,7 +6002,7 @@ export interface PlaywrightWorkerOptions {
/**
* 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
* "msedge-canary" to use branded [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge).

View file

@ -28,4 +28,4 @@ export function bundle(): Plugin {
},
},
};
}
}

View file

@ -160,4 +160,4 @@ export class TraceViewerServer {
return;
return response;
}
}
}

View file

@ -282,4 +282,4 @@ export async function generateFetchCall(resource: Entry, style: FetchStyle = Fet
async function fetchRequestPostData(resource: Entry) {
return resource.request.postData?._sha1 ? await fetch(`sha1/${resource.request.postData._sha1}`).then(r => r.text()) : resource.request.postData?.text;
}
}

View file

@ -150,4 +150,4 @@ function excludeOrigin(url: string): string {
} catch (error) {
return url;
}
}
}

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

@ -59,4 +59,4 @@ export function emptySource(): Source {
label: '',
highlight: []
};
}
}

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(sidebarBox).toEqual({ x: 0, y: 101, width: 500, height: 399 });
});

View file

@ -68,4 +68,4 @@ test('select webview from socketName', async function({ androidDevice }) {
await newPage.close();
await context.close();
});
});

View file

@ -79,4 +79,4 @@ function csvEscape(str) {
return str;
}
export default CsvReporter;
export default CsvReporter;

View file

@ -86,4 +86,4 @@ function getOutcome(test: TestCase): TestExpectation {
return 'unknown';
}
export default ExpectationReporter;
export default ExpectationReporter;

View file

@ -588,4 +588,4 @@ for (const toThrow of ['toThrowError', 'toThrow'] as const) {
).toThrowErrorMatchingSnapshot();
});
});
}
}

View file

@ -556,4 +556,4 @@ it('should ignore aborted requests', async ({ contextFactory, server }) => {
const result = await Promise.race([evalPromise, page2.waitForTimeout(1000).then(() => 'timeout')]);
expect(result).toBe('timeout');
}
});
});

View file

@ -629,4 +629,4 @@ test.describe('PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1', () => {
const req = await requestPromise;
expect(req.headers['x-custom-header']).toBe('custom!');
});
});
});

View file

@ -122,4 +122,4 @@ test('should generate routeFromHAR with --save-har and --save-har-glob', async (
await cli.waitForCleanExit();
const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8'));
expect(json.log.creator.name).toBe('Playwright');
});
});

View file

@ -288,4 +288,4 @@ async function waitForRafs(page: Page, count: number): Promise<void> {
};
window.builtinRequestAnimationFrame(onRaf);
}), count);
}
}

View file

@ -180,4 +180,3 @@ it('evaluateHandle should work', async ({ page, server }) => {
const windowHandle = await mainFrame.evaluateHandle(() => window);
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 page.frameLocator('iframe').locator(divLocator).locator('input').inputValue()).toBe('inner');
});

View file

@ -272,4 +272,4 @@ it('<picture> resource should have type image', async ({ page }) => {
`)
]);
expect(request.resourceType()).toBe('image');
});
});

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(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

@ -195,4 +195,4 @@ test('should focus a single test suite', async ({ runInlineTest }) => {
expect(result.skipped).toBe(0);
expect(result.report.suites[0].suites[0].suites[0].specs[0].title).toEqual('pass2');
expect(result.report.suites[0].suites[0].suites[0].specs[1].title).toEqual('pass3');
});
});

View file

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

View file

@ -148,4 +148,4 @@ test('should produce correct test steps', async ({ runInlineTest, runServer }) =
'onStepEnd fixture: context',
'onStepEnd After Hooks'
]);
});
});

View file

@ -2052,4 +2052,4 @@ test('project filter in report name', async ({ runInlineTest }) => {
const reportFiles = await fs.promises.readdir(reportDir);
expect(reportFiles.sort()).toEqual(['report-foo-b-r-6d9d49e-1.zip']);
}
});
});

View file

@ -112,4 +112,4 @@ for (const useIntermediateMergeReport of [false, true] as const) {
colors.green('·').repeat(3));
});
});
}
}

View file

@ -98,4 +98,4 @@ for (const useIntermediateMergeReport of [false, true] as const) {
expect(result.exitCode).toBe(1);
});
});
}
}

View file

@ -594,4 +594,4 @@ for (const useIntermediateMergeReport of [false, true] as const) {
expect(time).toBeGreaterThan(1);
});
});
}
}

View file

@ -189,4 +189,4 @@ for (const useIntermediateMergeReport of [false, true] as const) {
expect(result.exitCode).toBe(1);
});
});
}
}

View file

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

View file

@ -157,4 +157,4 @@ test('report with worker error', async ({ runInlineTest }) => {
**0 passed**
:heavy_check_mark::heavy_check_mark::heavy_check_mark:
`);
});
});

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(fs.existsSync(snapshotOutputPath)).toBe(true);
});

View file

@ -370,4 +370,4 @@ test('should always work with unix separators', async ({ runInlineTest }) => {
expect(result.passed).toBe(1);
expect(result.report.suites.map(s => s.file).sort()).toEqual(['a.test.ts']);
expect(result.exitCode).toBe(0);
});
});

View file

@ -186,4 +186,3 @@ test('test.use() should throw if called from beforeAll ', async ({ runInlineTest
expect(result.exitCode).toBe(1);
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'))
.toHaveAttribute('href', 'https://github.com/microsoft/playwright');
});

View file

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