Merge branch 'main' into ui-mode-watch-file
This commit is contained in:
commit
758f9c6fcd
|
|
@ -134,7 +134,7 @@ Defaults to `20`. Pass `0` to not follow redirects.
|
|||
* since: v1.46
|
||||
- `maxRetries` <[int]>
|
||||
|
||||
Maximum number of times socket errors should be retried. Currently only `ECONNRESET` error is retried. An error will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||
Maximum number of times network errors should be retried. Currently only `ECONNRESET` error is retried. Does not retry based on HTTP response codes. An error will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||
|
||||
## method: RequestOptions.setMethod
|
||||
* since: v1.18
|
||||
|
|
|
|||
|
|
@ -24,23 +24,3 @@ X coordinate relative to the main frame's viewport in CSS pixels.
|
|||
- `y` <[float]>
|
||||
|
||||
Y coordinate relative to the main frame's viewport in CSS pixels.
|
||||
|
||||
## async method: Touchscreen.touch
|
||||
* since: v1.46
|
||||
|
||||
Synthesizes a touch event.
|
||||
|
||||
### param: Touchscreen.touch.type
|
||||
* since: v1.46
|
||||
- `type` <[TouchType]<"touchstart"|"touchend"|"touchmove"|"touchcancel">>
|
||||
|
||||
Type of the touch event.
|
||||
|
||||
### param: Touchscreen.touch.touches
|
||||
* since: v1.46
|
||||
- `touchPoints` <[Array]<[Object]>>
|
||||
- `x` <[float]> x coordinate of the event in CSS pixels.
|
||||
- `y` <[float]> y coordinate of the event in CSS pixels.
|
||||
- `id` ?<[int]> Identifier used to track the touch point between events, must be unique within an event. Optional.
|
||||
|
||||
List of touch points for this event. `id` is a unique identifier of a touch point that helps identify it between touch events for the duration of its movement around the surface.
|
||||
|
|
@ -469,7 +469,7 @@ Defaults to `20`. Pass `0` to not follow redirects.
|
|||
* langs: js, python, csharp
|
||||
- `maxRetries` <[int]>
|
||||
|
||||
Maximum number of times socket errors should be retried. Currently only `ECONNRESET` error is retried. An error will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||
Maximum number of times network errors should be retried. Currently only `ECONNRESET` error is retried. Does not retry based on HTTP response codes. An error will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||
|
||||
## evaluate-expression
|
||||
- `expression` <[string]>
|
||||
|
|
@ -523,14 +523,17 @@ Does not enforce fixed viewport, allows resizing window in the headed mode.
|
|||
|
||||
## context-option-clientCertificates
|
||||
- `clientCertificates` <[Array]<[Object]>>
|
||||
- `url` <[string]> Glob pattern to match the URLs that the certificate is valid for.
|
||||
- `certs` <[Array]<[Object]>> List of client certificates to be used.
|
||||
- `certPath` ?<[string]> Path to the file with the certificate in PEM format.
|
||||
- `keyPath` ?<[string]> Path to the file with the private key in PEM format.
|
||||
- `pfxPath` ?<[string]> Path to the PFX or PKCS12 encoded private key and certificate chain.
|
||||
- `passphrase` ?<[string]> Passphrase for the private key (PEM or PFX).
|
||||
- `origin` <[string]> Glob pattern to match against the request origin that the certificate is valid for.
|
||||
- `certPath` ?<[string]> Path to the file with the certificate in PEM format.
|
||||
- `keyPath` ?<[string]> Path to the file with the private key in PEM format.
|
||||
- `pfxPath` ?<[string]> Path to the PFX or PKCS12 encoded private key and certificate chain.
|
||||
- `passphrase` ?<[string]> Passphrase for the private key (PEM or PFX).
|
||||
|
||||
An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the private key is encrypted. If the certificate is valid only for specific URLs, the `url` property should be provided with a glob pattern to match the URLs that the certificate is valid for.
|
||||
TLS Client Authentication allows the server to request a client certificate and verify it.
|
||||
|
||||
**Details**
|
||||
|
||||
An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the certficiate is encrypted. If the certificate is valid only for specific origins, the `origin` property should be provided with a glob pattern to match the origins that the certificate is valid for.
|
||||
|
||||
:::note
|
||||
Using Client Certificates in combination with Proxy Servers is not supported.
|
||||
|
|
|
|||
|
|
@ -461,7 +461,7 @@ Playwright's Firefox version matches the recent [Firefox Stable](https://www.moz
|
|||
|
||||
### WebKit
|
||||
|
||||
Playwright's WebKit version matches the recent WebKit trunk build, before it is used in Apple Safari and other WebKit-based browsers. This gives a lot of lead time to react on the potential browser update issues. Playwright doesn't work with the branded version of Safari since it relies on patches. Instead you can test against the recent WebKit build.
|
||||
Playwright's WebKit is derived from the latest WebKit main branch sources, often before these updates are incorporated into Apple Safari and other WebKit-based browsers. This gives a lot of lead time to react on the potential browser update issues. Playwright doesn't work with the branded version of Safari since it relies on patches. Instead, you can test using the most recent WebKit build. Note that avialability of certain features, which depend heavily on the underlying platform, may vary between operating systems.
|
||||
|
||||
## Install behind a firewall or a proxy
|
||||
|
||||
|
|
|
|||
|
|
@ -383,6 +383,43 @@ jobs:
|
|||
PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }}
|
||||
```
|
||||
|
||||
### Fail-Fast
|
||||
* langs: js
|
||||
|
||||
Even with sharding enabled, large test suites can take very long to execute. Running changed tests first on PRs will give you a faster feedback loop and use less CI resources.
|
||||
|
||||
```yml js title=".github/workflows/playwright.yml"
|
||||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
- name: Run changed Playwright tests
|
||||
run: npx playwright test --only-changed=$GITHUB_BASE_REF
|
||||
if: github.event_name == 'pull_request'
|
||||
- name: Run Playwright tests
|
||||
run: npx playwright test
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
```
|
||||
|
||||
## Create a Repo and Push to GitHub
|
||||
|
||||
|
|
|
|||
|
|
@ -148,22 +148,14 @@ export default defineConfig({
|
|||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
projects: [
|
||||
{
|
||||
name: 'Microsoft Edge',
|
||||
use: {
|
||||
...devices['Desktop Edge'],
|
||||
clientCertificates: [{
|
||||
url: 'https://example.com/**',
|
||||
certs: [{
|
||||
certPath: './cert.pem',
|
||||
keyPath: './key.pem',
|
||||
passphrase: 'mysecretpassword',
|
||||
}],
|
||||
}],
|
||||
},
|
||||
},
|
||||
]
|
||||
use: {
|
||||
clientCertificates: [{
|
||||
origin: 'https://example.com',
|
||||
certPath: './cert.pem',
|
||||
keyPath: './key.pem',
|
||||
passphrase: 'mysecretpassword',
|
||||
}],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ Complete set of Playwright Test options is available in the [configuration file]
|
|||
| `--max-failures <N>` or `-x`| Stop after the first `N` test failures. Passing `-x` stops after the first failure.|
|
||||
| `--no-deps` | Ignore the dependencies between projects and behave as if they were not specified. |
|
||||
| `--output <dir>` | Directory for artifacts produced by tests, defaults to `test-results`. |
|
||||
| `--only-changed [ref]` | Only run tests that have been changed between working tree and "ref". Defaults to running all uncommitted changes with ref=HEAD. Only supports Git. |
|
||||
| `--pass-with-no-tests` | Allows the test suite to pass when no files are found. |
|
||||
| `--project <name>` | Only run tests from the specified [projects](./test-projects.md), supports '*' wildcard. Defaults to running all projects defined in the configuration file.|
|
||||
| `--quiet` | Whether to suppress stdout and stderr from the tests. |
|
||||
|
|
|
|||
|
|
@ -726,51 +726,18 @@ test('update', async ({ mount }) => {
|
|||
|
||||
### Handling network requests
|
||||
|
||||
Playwright provides a `route` fixture to intercept and handle network requests.
|
||||
Playwright provides an **experimental** `router` fixture to intercept and handle network requests. There are two ways to use the `router` fixture:
|
||||
* Call `router.route(url, handler)` that behaves similarly to [`method: Page.route`]. See the [network mocking guide](./mock.md) for more details.
|
||||
* Call `router.use(handlers)` and pass [MSW library](https://mswjs.io/) request handlers to it.
|
||||
|
||||
```ts
|
||||
test.beforeEach(async ({ route }) => {
|
||||
// install common routes before each test
|
||||
await route('*/**/api/v1/fruits', async route => {
|
||||
const json = [{ name: 'Strawberry', id: 21 }];
|
||||
await route.fulfill({ json });
|
||||
});
|
||||
});
|
||||
|
||||
test('example test', async ({ mount }) => {
|
||||
// test as usual, your routes are active
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
You can also introduce test-specific routes.
|
||||
|
||||
```ts
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
test('example test', async ({ mount, route }) => {
|
||||
await route('*/**/api/v1/fruits', async route => {
|
||||
const json = [{ name: 'fruit for this single test', id: 42 }];
|
||||
await route.fulfill({ json });
|
||||
});
|
||||
|
||||
// test as usual, your route is active
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
The `route` fixture works in the same way as [`method: Page.route`]. See the [network mocking guide](./mock.md) for more details.
|
||||
|
||||
**Re-using MSW handlers**
|
||||
|
||||
If you are using the [MSW library](https://mswjs.io/) to handle network requests during development or testing, you can pass them directly to the `route` fixture.
|
||||
Here is an example of reusing your existing MSW handlers in the test.
|
||||
|
||||
```ts
|
||||
import { handlers } from '@src/mocks/handlers';
|
||||
|
||||
test.beforeEach(async ({ route }) => {
|
||||
test.beforeEach(async ({ router }) => {
|
||||
// install common handlers before each test
|
||||
await route(handlers);
|
||||
await router.use(...handlers);
|
||||
});
|
||||
|
||||
test('example test', async ({ mount }) => {
|
||||
|
|
@ -779,13 +746,13 @@ test('example test', async ({ mount }) => {
|
|||
});
|
||||
```
|
||||
|
||||
You can also introduce test-specific handlers.
|
||||
You can also introduce a one-off handler for a specific test.
|
||||
|
||||
```ts
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
test('example test', async ({ mount, route }) => {
|
||||
await route(http.get('/data', async ({ request }) => {
|
||||
test('example test', async ({ mount, router }) => {
|
||||
await router.use(http.get('/data', async ({ request }) => {
|
||||
return HttpResponse.json({ value: 'mocked' });
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -429,6 +429,8 @@ npx playwright test --reporter="./myreporter/my-awesome-reporter.ts"
|
|||
* [Currents](https://www.npmjs.com/package/@currents/playwright)
|
||||
* [GitHub Actions Reporter](https://www.npmjs.com/package/@estruyf/github-actions-reporter)
|
||||
* [GitHub Pull Request Comment](https://github.com/marketplace/actions/playwright-report-comment)
|
||||
* [Mail Reporter](https://www.npmjs.com/package/playwright-mail-reporter)
|
||||
* [Microsoft Teams Reporter](https://www.npmjs.com/package/playwright-msteams-reporter)
|
||||
* [Monocart](https://github.com/cenfun/monocart-reporter)
|
||||
* [ReportPortal](https://github.com/reportportal/agent-js-playwright)
|
||||
* [Serenity/JS](https://serenity-js.org/handbook/test-runners/playwright-test)
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@
|
|||
},
|
||||
{
|
||||
"name": "chromium-tip-of-tree",
|
||||
"revision": "1242",
|
||||
"revision": "1243",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "128.0.6603.0"
|
||||
"browserVersion": "128.0.6613.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
},
|
||||
{
|
||||
"name": "webkit",
|
||||
"revision": "2048",
|
||||
"revision": "2051",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"mac10.14": "1446",
|
||||
|
|
|
|||
|
|
@ -550,20 +550,16 @@ function toAcceptDownloadsProtocol(acceptDownloads?: boolean) {
|
|||
return 'deny';
|
||||
}
|
||||
|
||||
export async function toClientCertificatesProtocol(clientCertificates?: BrowserContextOptions['clientCertificates']): Promise<channels.PlaywrightNewRequestParams['clientCertificates']> {
|
||||
if (!clientCertificates)
|
||||
export async function toClientCertificatesProtocol(certs?: BrowserContextOptions['clientCertificates']): Promise<channels.PlaywrightNewRequestParams['clientCertificates']> {
|
||||
if (!certs)
|
||||
return undefined;
|
||||
return await Promise.all(clientCertificates.map(async clientCertificate => {
|
||||
return await Promise.all(certs.map(async cert => {
|
||||
return {
|
||||
url: clientCertificate.url,
|
||||
certs: await Promise.all(clientCertificate.certs.map(async cert => {
|
||||
return {
|
||||
cert: cert.certPath ? await fs.promises.readFile(cert.certPath) : undefined,
|
||||
key: cert.keyPath ? await fs.promises.readFile(cert.keyPath) : undefined,
|
||||
pfx: cert.pfxPath ? await fs.promises.readFile(cert.pfxPath) : undefined,
|
||||
passphrase: cert.passphrase,
|
||||
};
|
||||
}))
|
||||
origin: cert.origin,
|
||||
cert: cert.certPath ? await fs.promises.readFile(cert.certPath) : undefined,
|
||||
key: cert.keyPath ? await fs.promises.readFile(cert.keyPath) : undefined,
|
||||
pfx: cert.pfxPath ? await fs.promises.readFile(cert.pfxPath) : undefined,
|
||||
passphrase: cert.passphrase,
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,8 +89,4 @@ export class Touchscreen implements api.Touchscreen {
|
|||
async tap(x: number, y: number) {
|
||||
await this._page._channel.touchscreenTap({ x, y });
|
||||
}
|
||||
|
||||
async touch(type: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[]) {
|
||||
await this._page._channel.touchscreenTouch({ type, touchPoints });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,13 +48,11 @@ export type LifecycleEvent = channels.LifecycleEvent;
|
|||
export const kLifecycleEvents: Set<LifecycleEvent> = new Set(['load', 'domcontentloaded', 'networkidle', 'commit']);
|
||||
|
||||
export type ClientCertificate = {
|
||||
url: string;
|
||||
certs: {
|
||||
certPath?: string;
|
||||
keyPath?: string;
|
||||
pfxPath?: string;
|
||||
passphrase?: string;
|
||||
}[];
|
||||
origin: string;
|
||||
certPath?: string;
|
||||
keyPath?: string;
|
||||
pfxPath?: string;
|
||||
passphrase?: string;
|
||||
};
|
||||
|
||||
export type BrowserContextOptions = Omit<channels.BrowserNewContextOptions, 'viewport' | 'noDefaultViewport' | 'extraHTTPHeaders' | 'clientCertificates' | 'storageState' | 'recordHar' | 'colorScheme' | 'reducedMotion' | 'forcedColors' | 'acceptDownloads'> & {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ export const slowMoActions = new Set([
|
|||
'Page.mouseClick',
|
||||
'Page.mouseWheel',
|
||||
'Page.touchscreenTap',
|
||||
'Page.touchscreenTouch',
|
||||
'Frame.blur',
|
||||
'Frame.check',
|
||||
'Frame.click',
|
||||
|
|
@ -90,7 +89,6 @@ export const commandsWithTracingSnapshots = new Set([
|
|||
'Page.mouseClick',
|
||||
'Page.mouseWheel',
|
||||
'Page.touchscreenTap',
|
||||
'Page.touchscreenTouch',
|
||||
'Frame.evalOnSelector',
|
||||
'Frame.evalOnSelectorAll',
|
||||
'Frame.addScriptTag',
|
||||
|
|
|
|||
|
|
@ -337,13 +337,11 @@ scheme.PlaywrightNewRequestParams = tObject({
|
|||
ignoreHTTPSErrors: tOptional(tBoolean),
|
||||
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
||||
clientCertificates: tOptional(tArray(tObject({
|
||||
url: tString,
|
||||
certs: tArray(tObject({
|
||||
cert: tOptional(tBinary),
|
||||
key: tOptional(tBinary),
|
||||
passphrase: tOptional(tString),
|
||||
pfx: tOptional(tBinary),
|
||||
})),
|
||||
origin: tString,
|
||||
cert: tOptional(tBinary),
|
||||
key: tOptional(tBinary),
|
||||
passphrase: tOptional(tString),
|
||||
pfx: tOptional(tBinary),
|
||||
}))),
|
||||
httpCredentials: tOptional(tObject({
|
||||
username: tString,
|
||||
|
|
@ -547,13 +545,11 @@ scheme.BrowserTypeLaunchPersistentContextParams = tObject({
|
|||
})),
|
||||
ignoreHTTPSErrors: tOptional(tBoolean),
|
||||
clientCertificates: tOptional(tArray(tObject({
|
||||
url: tString,
|
||||
certs: tArray(tObject({
|
||||
cert: tOptional(tBinary),
|
||||
key: tOptional(tBinary),
|
||||
passphrase: tOptional(tString),
|
||||
pfx: tOptional(tBinary),
|
||||
})),
|
||||
origin: tString,
|
||||
cert: tOptional(tBinary),
|
||||
key: tOptional(tBinary),
|
||||
passphrase: tOptional(tString),
|
||||
pfx: tOptional(tBinary),
|
||||
}))),
|
||||
javaScriptEnabled: tOptional(tBoolean),
|
||||
bypassCSP: tOptional(tBoolean),
|
||||
|
|
@ -635,13 +631,11 @@ scheme.BrowserNewContextParams = tObject({
|
|||
})),
|
||||
ignoreHTTPSErrors: tOptional(tBoolean),
|
||||
clientCertificates: tOptional(tArray(tObject({
|
||||
url: tString,
|
||||
certs: tArray(tObject({
|
||||
cert: tOptional(tBinary),
|
||||
key: tOptional(tBinary),
|
||||
passphrase: tOptional(tString),
|
||||
pfx: tOptional(tBinary),
|
||||
})),
|
||||
origin: tString,
|
||||
cert: tOptional(tBinary),
|
||||
key: tOptional(tBinary),
|
||||
passphrase: tOptional(tString),
|
||||
pfx: tOptional(tBinary),
|
||||
}))),
|
||||
javaScriptEnabled: tOptional(tBoolean),
|
||||
bypassCSP: tOptional(tBoolean),
|
||||
|
|
@ -706,13 +700,11 @@ scheme.BrowserNewContextForReuseParams = tObject({
|
|||
})),
|
||||
ignoreHTTPSErrors: tOptional(tBoolean),
|
||||
clientCertificates: tOptional(tArray(tObject({
|
||||
url: tString,
|
||||
certs: tArray(tObject({
|
||||
cert: tOptional(tBinary),
|
||||
key: tOptional(tBinary),
|
||||
passphrase: tOptional(tString),
|
||||
pfx: tOptional(tBinary),
|
||||
})),
|
||||
origin: tString,
|
||||
cert: tOptional(tBinary),
|
||||
key: tOptional(tBinary),
|
||||
passphrase: tOptional(tString),
|
||||
pfx: tOptional(tBinary),
|
||||
}))),
|
||||
javaScriptEnabled: tOptional(tBoolean),
|
||||
bypassCSP: tOptional(tBoolean),
|
||||
|
|
@ -1278,15 +1270,6 @@ scheme.PageTouchscreenTapParams = tObject({
|
|||
y: tNumber,
|
||||
});
|
||||
scheme.PageTouchscreenTapResult = tOptional(tObject({}));
|
||||
scheme.PageTouchscreenTouchParams = tObject({
|
||||
type: tEnum(['touchstart', 'touchend', 'touchmove', 'touchcancel']),
|
||||
touchPoints: tArray(tObject({
|
||||
x: tNumber,
|
||||
y: tNumber,
|
||||
id: tOptional(tNumber),
|
||||
})),
|
||||
});
|
||||
scheme.PageTouchscreenTouchResult = tOptional(tObject({}));
|
||||
scheme.PageAccessibilitySnapshotParams = tObject({
|
||||
interestingOnly: tOptional(tBoolean),
|
||||
root: tOptional(tChannel(['ElementHandle'])),
|
||||
|
|
@ -2535,13 +2518,11 @@ scheme.AndroidDeviceLaunchBrowserParams = tObject({
|
|||
})),
|
||||
ignoreHTTPSErrors: tOptional(tBoolean),
|
||||
clientCertificates: tOptional(tArray(tObject({
|
||||
url: tString,
|
||||
certs: tArray(tObject({
|
||||
cert: tOptional(tBinary),
|
||||
key: tOptional(tBinary),
|
||||
passphrase: tOptional(tString),
|
||||
pfx: tOptional(tBinary),
|
||||
})),
|
||||
origin: tString,
|
||||
cert: tOptional(tBinary),
|
||||
key: tOptional(tBinary),
|
||||
passphrase: tOptional(tString),
|
||||
pfx: tOptional(tBinary),
|
||||
}))),
|
||||
javaScriptEnabled: tOptional(tBoolean),
|
||||
bypassCSP: tOptional(tBoolean),
|
||||
|
|
|
|||
|
|
@ -725,21 +725,17 @@ export function verifyGeolocation(geolocation?: types.Geolocation) {
|
|||
export function verifyClientCertificates(clientCertificates?: channels.BrowserNewContextParams['clientCertificates']) {
|
||||
if (!clientCertificates)
|
||||
return;
|
||||
for (const { url, certs } of clientCertificates) {
|
||||
if (!url)
|
||||
throw new Error(`clientCertificates.url is required`);
|
||||
if (!certs.length)
|
||||
throw new Error('No certs specified for url: ' + url);
|
||||
for (const cert of certs) {
|
||||
if (!cert.cert && !cert.key && !cert.passphrase && !cert.pfx)
|
||||
throw new Error('None of cert, key, passphrase or pfx is specified');
|
||||
if (cert.cert && !cert.key)
|
||||
throw new Error('cert is specified without key');
|
||||
if (!cert.cert && cert.key)
|
||||
throw new Error('key is specified without cert');
|
||||
if (cert.pfx && (cert.cert || cert.key))
|
||||
throw new Error('pfx is specified together with cert, key or passphrase');
|
||||
}
|
||||
for (const cert of clientCertificates) {
|
||||
if (!cert.origin)
|
||||
throw new Error(`clientCertificates.origin is required`);
|
||||
if (!cert.cert && !cert.key && !cert.passphrase && !cert.pfx)
|
||||
throw new Error('None of cert, key, passphrase or pfx is specified');
|
||||
if (cert.cert && !cert.key)
|
||||
throw new Error('cert is specified without key');
|
||||
if (!cert.cert && cert.key)
|
||||
throw new Error('key is specified without cert');
|
||||
if (cert.pfx && (cert.cert || cert.key))
|
||||
throw new Error('pfx is specified together with cert, key or passphrase');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -179,19 +179,4 @@ export class RawTouchscreenImpl implements input.RawTouchscreen {
|
|||
}),
|
||||
]);
|
||||
}
|
||||
async touch(eventType: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[], modifiers: Set<types.KeyboardModifier>) {
|
||||
let type: 'touchStart' | 'touchMove' | 'touchEnd' | 'touchCancel';
|
||||
switch (eventType) {
|
||||
case 'touchstart': type = 'touchStart'; break;
|
||||
case 'touchmove': type = 'touchMove'; break;
|
||||
case 'touchend': type = 'touchEnd'; break;
|
||||
case 'touchcancel': type = 'touchCancel'; break;
|
||||
default: throw new Error('Invalid eventType: ' + eventType);
|
||||
}
|
||||
await this._client.send('Input.dispatchTouchEvent', {
|
||||
type,
|
||||
touchPoints,
|
||||
modifiers: toModifiersMask(modifiers)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,10 +265,6 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
|
|||
await this._page.touchscreen.tap(params.x, params.y, metadata);
|
||||
}
|
||||
|
||||
async touchscreenTouch(params: channels.PageTouchscreenTouchParams, metadata: CallMetadata) {
|
||||
await this._page.touchscreen.touch(params.type, params.touchPoints, metadata);
|
||||
}
|
||||
|
||||
async accessibilitySnapshot(params: channels.PageAccessibilitySnapshotParams, metadata: CallMetadata): Promise<channels.PageAccessibilitySnapshotResult> {
|
||||
const rootAXNode = await this._page.accessibility.snapshot({
|
||||
interestingOnly: params.interestingOnly,
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
maxRedirects: params.maxRedirects === 0 ? -1 : params.maxRedirects === undefined ? 20 : params.maxRedirects,
|
||||
timeout,
|
||||
deadline,
|
||||
...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, requestUrl.toString()),
|
||||
...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, requestUrl.origin),
|
||||
__testHookLookup: (params as any).__testHookLookup,
|
||||
};
|
||||
if (process.env.PWTEST_UNSUPPORTED_CUSTOM_CA && isUnderTest())
|
||||
|
|
@ -357,7 +357,7 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
maxRedirects: options.maxRedirects - 1,
|
||||
timeout: options.timeout,
|
||||
deadline: options.deadline,
|
||||
...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, url.toString()),
|
||||
...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, url.origin),
|
||||
__testHookLookup: options.__testHookLookup,
|
||||
};
|
||||
// rejectUnauthorized = undefined is treated as true in node 12.
|
||||
|
|
@ -422,7 +422,10 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
finishFlush: zlib.constants.Z_SYNC_FLUSH
|
||||
});
|
||||
} else if (encoding === 'br') {
|
||||
transform = zlib.createBrotliDecompress();
|
||||
transform = zlib.createBrotliDecompress({
|
||||
flush: zlib.constants.BROTLI_OPERATION_FLUSH,
|
||||
finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH
|
||||
});
|
||||
} else if (encoding === 'deflate') {
|
||||
transform = zlib.createInflate();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,8 +166,4 @@ export class RawTouchscreenImpl implements input.RawTouchscreen {
|
|||
modifiers: toModifiersMask(modifiers),
|
||||
});
|
||||
}
|
||||
|
||||
async touch(eventType: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[], modifiers: Set<types.KeyboardModifier>) {
|
||||
throw new Error('Not implemented yet.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -308,7 +308,6 @@ function buildLayoutClosure(layout: keyboardLayout.KeyboardLayout): Map<string,
|
|||
|
||||
export interface RawTouchscreen {
|
||||
tap(x: number, y: number, modifiers: Set<types.KeyboardModifier>): Promise<void>;
|
||||
touch(type: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[], modifiers: Set<types.KeyboardModifier>): Promise<void>;
|
||||
}
|
||||
|
||||
export class Touchscreen {
|
||||
|
|
@ -327,19 +326,4 @@ export class Touchscreen {
|
|||
throw new Error('hasTouch must be enabled on the browser context before using the touchscreen.');
|
||||
await this._raw.tap(x, y, this._page.keyboard._modifiers());
|
||||
}
|
||||
async touch(type: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[], metadata?: CallMetadata) {
|
||||
if (metadata && touchPoints.length === 1)
|
||||
metadata.point = { x: touchPoints[0].x, y: touchPoints[0].y };
|
||||
if (!this._page._browserContext._options.hasTouch)
|
||||
throw new Error('hasTouch must be enabled on the browser context before using the touchscreen.');
|
||||
const ids = new Set<number>();
|
||||
for (const point of touchPoints) {
|
||||
if (point.id !== undefined) {
|
||||
if (ids.has(point.id))
|
||||
throw new Error(`Duplicate touch point id: ${point.id}`);
|
||||
ids.add(point.id);
|
||||
}
|
||||
}
|
||||
await this._raw.touch(type, touchPoints, this._page.keyboard._modifiers());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,19 +21,44 @@ import fs from 'fs';
|
|||
import tls from 'tls';
|
||||
import stream from 'stream';
|
||||
import { createSocket } from '../utils/happy-eyeballs';
|
||||
import { globToRegex, isUnderTest } from '../utils';
|
||||
import { globToRegex, isUnderTest, ManualPromise } from '../utils';
|
||||
import type { SocksSocketClosedPayload, SocksSocketDataPayload, SocksSocketRequestedPayload } from '../common/socksProxy';
|
||||
import { SocksProxy } from '../common/socksProxy';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
|
||||
class SocksConnectionDuplex extends stream.Duplex {
|
||||
constructor(private readonly writeCallback: (data: Buffer) => void) {
|
||||
super();
|
||||
}
|
||||
override _read(): void { }
|
||||
override _write(chunk: Buffer, encoding: BufferEncoding, callback: (error?: Error | null | undefined) => void): void {
|
||||
this.writeCallback(chunk);
|
||||
callback();
|
||||
class ALPNCache {
|
||||
private _cache = new Map<string, ManualPromise<string>>();
|
||||
|
||||
get(host: string, port: number, success: (protocol: string) => void) {
|
||||
const cacheKey = `${host}:${port}`;
|
||||
{
|
||||
const result = this._cache.get(cacheKey);
|
||||
if (result) {
|
||||
result.then(success);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const result = new ManualPromise<string>();
|
||||
this._cache.set(cacheKey, result);
|
||||
result.then(success);
|
||||
const socket = tls.connect({
|
||||
host,
|
||||
port,
|
||||
servername: net.isIP(host) ? undefined : host,
|
||||
ALPNProtocols: ['h2', 'http/1.1'],
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
socket.on('secureConnect', () => {
|
||||
// The server may not respond with ALPN, in which case we default to http/1.1.
|
||||
result.resolve(socket.alpnProtocol || 'http/1.1');
|
||||
socket.end();
|
||||
});
|
||||
socket.on('error', error => {
|
||||
debugLogger.log('client-certificates', `ALPN error: ${error.message}`);
|
||||
result.resolve('http/1.1');
|
||||
socket.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -46,7 +71,6 @@ class SocksProxyConnection {
|
|||
target!: net.Socket;
|
||||
// In case of http, we just pipe data to the target socket and they are |undefined|.
|
||||
internal: stream.Duplex | undefined;
|
||||
internalTLS: tls.TLSSocket | undefined;
|
||||
|
||||
constructor(socksProxy: ClientCertificatesProxy, uid: string, host: string, port: number) {
|
||||
this.socksProxy = socksProxy;
|
||||
|
|
@ -56,7 +80,7 @@ class SocksProxyConnection {
|
|||
}
|
||||
|
||||
async connect() {
|
||||
this.target = await createSocket(this.host === 'local.playwright' ? 'localhost' : this.host, this.port);
|
||||
this.target = await createSocket(rewriteToLocalhostIfNeeded(this.host), this.port);
|
||||
this.target.on('close', () => this.socksProxy._socksProxy.sendSocketEnd({ uid: this.uid }));
|
||||
this.target.on('error', error => this.socksProxy._socksProxy.sendSocketError({ uid: this.uid, error: error.message }));
|
||||
this.socksProxy._socksProxy.socketConnected({
|
||||
|
|
@ -89,51 +113,70 @@ class SocksProxyConnection {
|
|||
}
|
||||
|
||||
private _attachTLSListeners() {
|
||||
this.internal = new SocksConnectionDuplex(data => this.socksProxy._socksProxy.sendSocketData({ uid: this.uid, data }));
|
||||
const internalTLS = new tls.TLSSocket(this.internal, {
|
||||
isServer: true,
|
||||
key: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/key.pem')),
|
||||
cert: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/cert.pem')),
|
||||
this.internal = new stream.Duplex({
|
||||
read: () => {},
|
||||
write: (data, encoding, callback) => {
|
||||
this.socksProxy._socksProxy.sendSocketData({ uid: this.uid, data });
|
||||
callback();
|
||||
}
|
||||
});
|
||||
this.internalTLS = internalTLS;
|
||||
internalTLS.on('close', () => this.socksProxy._socksProxy.sendSocketEnd({ uid: this.uid }));
|
||||
this.socksProxy.alpnCache.get(rewriteToLocalhostIfNeeded(this.host), this.port, alpnProtocolChosenByServer => {
|
||||
debugLogger.log('client-certificates', `Proxy->Target ${this.host}:${this.port} chooses ALPN ${alpnProtocolChosenByServer}`);
|
||||
const dummyServer = tls.createServer({
|
||||
key: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/key.pem')),
|
||||
cert: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/cert.pem')),
|
||||
ALPNProtocols: alpnProtocolChosenByServer === 'h2' ? ['h2', 'http/1.1'] : ['http/1.1'],
|
||||
});
|
||||
this.internal?.on('close', () => dummyServer.close());
|
||||
dummyServer.emit('connection', this.internal);
|
||||
dummyServer.on('secureConnection', internalTLS => {
|
||||
debugLogger.log('client-certificates', `Browser->Proxy ${this.host}:${this.port} chooses ALPN ${internalTLS.alpnProtocol}`);
|
||||
internalTLS.on('close', () => this.socksProxy._socksProxy.sendSocketEnd({ uid: this.uid }));
|
||||
const tlsOptions: tls.ConnectionOptions = {
|
||||
socket: this.target,
|
||||
host: this.host,
|
||||
port: this.port,
|
||||
rejectUnauthorized: !this.socksProxy.ignoreHTTPSErrors,
|
||||
ALPNProtocols: [internalTLS.alpnProtocol || 'http/1.1'],
|
||||
...clientCertificatesToTLSOptions(this.socksProxy.clientCertificates, `https://${this.host}:${this.port}`),
|
||||
};
|
||||
if (!net.isIP(this.host))
|
||||
tlsOptions.servername = this.host;
|
||||
if (process.env.PWTEST_UNSUPPORTED_CUSTOM_CA && isUnderTest())
|
||||
tlsOptions.ca = [fs.readFileSync(process.env.PWTEST_UNSUPPORTED_CUSTOM_CA)];
|
||||
const targetTLS = tls.connect(tlsOptions);
|
||||
|
||||
const tlsOptions: tls.ConnectionOptions = {
|
||||
socket: this.target,
|
||||
host: this.host,
|
||||
port: this.port,
|
||||
rejectUnauthorized: !this.socksProxy.ignoreHTTPSErrors,
|
||||
...clientCertificatesToTLSOptions(this.socksProxy.clientCertificates, `https://${this.host}:${this.port}/`),
|
||||
};
|
||||
if (!net.isIP(this.host))
|
||||
tlsOptions.servername = this.host;
|
||||
if (process.env.PWTEST_UNSUPPORTED_CUSTOM_CA && isUnderTest())
|
||||
tlsOptions.ca = [fs.readFileSync(process.env.PWTEST_UNSUPPORTED_CUSTOM_CA)];
|
||||
const targetTLS = tls.connect(tlsOptions);
|
||||
internalTLS.pipe(targetTLS);
|
||||
targetTLS.pipe(internalTLS);
|
||||
|
||||
internalTLS.pipe(targetTLS);
|
||||
targetTLS.pipe(internalTLS);
|
||||
// Handle close and errors
|
||||
const closeBothSockets = () => {
|
||||
internalTLS.end();
|
||||
targetTLS.end();
|
||||
};
|
||||
|
||||
// Handle close and errors
|
||||
const closeBothSockets = () => {
|
||||
internalTLS.end();
|
||||
targetTLS.end();
|
||||
};
|
||||
internalTLS.on('end', () => closeBothSockets());
|
||||
targetTLS.on('end', () => closeBothSockets());
|
||||
|
||||
internalTLS.on('end', () => closeBothSockets());
|
||||
targetTLS.on('end', () => closeBothSockets());
|
||||
|
||||
internalTLS.on('error', () => closeBothSockets());
|
||||
targetTLS.on('error', error => {
|
||||
const responseBody = 'Playwright client-certificate error: ' + error.message;
|
||||
internalTLS.end([
|
||||
'HTTP/1.1 503 Internal Server Error',
|
||||
'Content-Type: text/html; charset=utf-8',
|
||||
'Content-Length: ' + Buffer.byteLength(responseBody),
|
||||
'\r\n',
|
||||
responseBody,
|
||||
].join('\r\n'));
|
||||
closeBothSockets();
|
||||
internalTLS.on('error', () => closeBothSockets());
|
||||
targetTLS.on('error', error => {
|
||||
debugLogger.log('client-certificates', `error when connecting to target: ${error.message}`);
|
||||
if (internalTLS?.alpnProtocol === 'h2') {
|
||||
// https://github.com/nodejs/node/issues/46152
|
||||
// TODO: http2.performServerHandshake does not work here for some reason.
|
||||
} else {
|
||||
const responseBody = 'Playwright client-certificate error: ' + error.message;
|
||||
internalTLS.end([
|
||||
'HTTP/1.1 503 Internal Server Error',
|
||||
'Content-Type: text/html; charset=utf-8',
|
||||
'Content-Length: ' + Buffer.byteLength(responseBody),
|
||||
'\r\n',
|
||||
responseBody,
|
||||
].join('\r\n'));
|
||||
}
|
||||
closeBothSockets();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -143,10 +186,12 @@ export class ClientCertificatesProxy {
|
|||
private _connections: Map<string, SocksProxyConnection> = new Map();
|
||||
ignoreHTTPSErrors: boolean | undefined;
|
||||
clientCertificates: channels.BrowserNewContextOptions['clientCertificates'];
|
||||
alpnCache: ALPNCache;
|
||||
|
||||
constructor(
|
||||
contextOptions: Pick<channels.BrowserNewContextOptions, 'clientCertificates' | 'ignoreHTTPSErrors'>
|
||||
) {
|
||||
this.alpnCache = new ALPNCache();
|
||||
this.ignoreHTTPSErrors = contextOptions.ignoreHTTPSErrors;
|
||||
this.clientCertificates = contextOptions.clientCertificates;
|
||||
this._socksProxy = new SocksProxy();
|
||||
|
|
@ -183,16 +228,16 @@ const kClientCertificatesGlobRegex = Symbol('kClientCertificatesGlobRegex');
|
|||
|
||||
export function clientCertificatesToTLSOptions(
|
||||
clientCertificates: channels.BrowserNewContextOptions['clientCertificates'],
|
||||
requestURL: string
|
||||
origin: string
|
||||
): Pick<https.RequestOptions, 'pfx' | 'key' | 'cert'> | undefined {
|
||||
const matchingCerts = clientCertificates?.filter(c => {
|
||||
let regex: RegExp | undefined = (c as any)[kClientCertificatesGlobRegex];
|
||||
if (!regex) {
|
||||
regex = globToRegex(c.url);
|
||||
regex = globToRegex(c.origin);
|
||||
(c as any)[kClientCertificatesGlobRegex] = regex;
|
||||
}
|
||||
regex.lastIndex = 0;
|
||||
return regex.test(requestURL);
|
||||
return regex.test(origin);
|
||||
});
|
||||
if (!matchingCerts || !matchingCerts.length)
|
||||
return;
|
||||
|
|
@ -201,15 +246,17 @@ export function clientCertificatesToTLSOptions(
|
|||
key: [] as { pem: Buffer, passphrase?: string }[],
|
||||
cert: [] as Buffer[],
|
||||
};
|
||||
for (const { certs } of matchingCerts) {
|
||||
for (const cert of certs) {
|
||||
if (cert.cert)
|
||||
tlsOptions.cert.push(cert.cert);
|
||||
if (cert.key)
|
||||
tlsOptions.key.push({ pem: cert.key, passphrase: cert.passphrase });
|
||||
if (cert.pfx)
|
||||
tlsOptions.pfx.push({ buf: cert.pfx, passphrase: cert.passphrase });
|
||||
}
|
||||
for (const cert of matchingCerts) {
|
||||
if (cert.cert)
|
||||
tlsOptions.cert.push(cert.cert);
|
||||
if (cert.key)
|
||||
tlsOptions.key.push({ pem: cert.key, passphrase: cert.passphrase });
|
||||
if (cert.pfx)
|
||||
tlsOptions.pfx.push({ buf: cert.pfx, passphrase: cert.passphrase });
|
||||
}
|
||||
return tlsOptions;
|
||||
}
|
||||
|
||||
function rewriteToLocalhostIfNeeded(host: string): string {
|
||||
return host === 'local.playwright' ? 'localhost' : host;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,20 +182,4 @@ export class RawTouchscreenImpl implements input.RawTouchscreen {
|
|||
modifiers: toModifiersMask(modifiers),
|
||||
});
|
||||
}
|
||||
|
||||
async touch(eventType: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[], modifiers: Set<types.KeyboardModifier>) {
|
||||
let type: 'touchStart' | 'touchMove' | 'touchEnd' | 'touchCancel';
|
||||
switch (eventType) {
|
||||
case 'touchstart': type = 'touchStart'; break;
|
||||
case 'touchmove': type = 'touchMove'; break;
|
||||
case 'touchend': type = 'touchEnd'; break;
|
||||
case 'touchcancel': type = 'touchCancel'; break;
|
||||
default: throw new Error('Invalid eventType: ' + eventType);
|
||||
}
|
||||
await this._pageProxySession.send('Input.dispatchTouchEvent', {
|
||||
type,
|
||||
touchPoints: touchPoints.map(p => ({ ...p, id: p.id || 0 })),
|
||||
modifiers: toModifiersMask(modifiers)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,9 +40,9 @@ const errorReasons: { [reason: string]: Protocol.Network.ResourceErrorType } = {
|
|||
};
|
||||
|
||||
export class WKInterceptableRequest {
|
||||
private readonly _session: WKSession;
|
||||
private _session: WKSession;
|
||||
private _requestId: string;
|
||||
readonly request: network.Request;
|
||||
private readonly _requestId: string;
|
||||
_timestamp: number;
|
||||
_wallTime: number;
|
||||
|
||||
|
|
@ -59,6 +59,11 @@ export class WKInterceptableRequest {
|
|||
resourceType, event.request.method, postDataBuffer, headersObjectToArray(event.request.headers));
|
||||
}
|
||||
|
||||
adoptRequestFromNewProcess(newSession: WKSession, requestId: string) {
|
||||
this._session = newSession;
|
||||
this._requestId = requestId;
|
||||
}
|
||||
|
||||
createResponse(responsePayload: Protocol.Network.Response): network.Response {
|
||||
const getResponseBody = async () => {
|
||||
const response = await this._session.send('Network.getResponseBody', { requestId: this._requestId });
|
||||
|
|
|
|||
|
|
@ -250,6 +250,7 @@ export class WKPage implements PageDelegate {
|
|||
private _onTargetDestroyed(event: Protocol.Target.targetDestroyedPayload) {
|
||||
const { targetId, crashed } = event;
|
||||
if (this._provisionalPage && this._provisionalPage._session.sessionId === targetId) {
|
||||
this._maybeCancelCoopNavigationRequest(this._provisionalPage);
|
||||
this._provisionalPage._session.dispose();
|
||||
this._provisionalPage.dispose();
|
||||
this._provisionalPage = null;
|
||||
|
|
@ -1015,6 +1016,33 @@ export class WKPage implements PageDelegate {
|
|||
return context.createHandle(result.object) as dom.ElementHandle;
|
||||
}
|
||||
|
||||
private _maybeCancelCoopNavigationRequest(provisionalPage: WKProvisionalPage) {
|
||||
const navigationRequest = provisionalPage.coopNavigationRequest();
|
||||
for (const [requestId, request] of this._requestIdToRequest) {
|
||||
if (request.request === navigationRequest) {
|
||||
// Make sure the request completes if the provisional navigation is canceled.
|
||||
this._onLoadingFailed(provisionalPage._session, {
|
||||
requestId: requestId,
|
||||
errorText: 'Provisiolal navigation canceled.',
|
||||
timestamp: request._timestamp,
|
||||
canceled: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_adoptRequestFromNewProcess(navigationRequest: network.Request, newSession: WKSession, newRequestId: string) {
|
||||
for (const [requestId, request] of this._requestIdToRequest) {
|
||||
if (request.request === navigationRequest) {
|
||||
this._requestIdToRequest.delete(requestId);
|
||||
request.adoptRequestFromNewProcess(newSession, newRequestId);
|
||||
this._requestIdToRequest.set(newRequestId, request);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onRequestWillBeSent(session: WKSession, event: Protocol.Network.requestWillBeSentPayload) {
|
||||
if (event.request.url.startsWith('data:'))
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -20,10 +20,12 @@ import type { RegisteredListener } from '../../utils/eventsHelper';
|
|||
import { eventsHelper } from '../../utils/eventsHelper';
|
||||
import type { Protocol } from './protocol';
|
||||
import { assert } from '../../utils';
|
||||
import type * as network from '../network';
|
||||
|
||||
export class WKProvisionalPage {
|
||||
readonly _session: WKSession;
|
||||
private readonly _wkPage: WKPage;
|
||||
private _coopNavigationRequest: network.Request | undefined;
|
||||
private _sessionListeners: RegisteredListener[] = [];
|
||||
private _mainFrameId: string | null = null;
|
||||
readonly initializationPromise: Promise<void>;
|
||||
|
|
@ -31,6 +33,16 @@ export class WKProvisionalPage {
|
|||
constructor(session: WKSession, page: WKPage) {
|
||||
this._session = session;
|
||||
this._wkPage = page;
|
||||
// Cross-Origin-Opener-Policy (COOP) request starts in one process and once response headers
|
||||
// have been received, continues in another.
|
||||
//
|
||||
// Network.requestWillBeSent and requestIntercepted (if intercepting) from the original web process
|
||||
// will always come before a provisional page is created based on the response COOP headers.
|
||||
// Thereafter we'll receive targetCreated (provisional) and later on in some order loadingFailed from the
|
||||
// original process and requestWillBeSent from the provisional one. We should ignore loadingFailed
|
||||
// as the original request continues in the provisional process. But if the provisional load is later
|
||||
// canceled we should dispatch loadingFailed to the client.
|
||||
this._coopNavigationRequest = page._page.mainFrame().pendingDocument()?.request;
|
||||
|
||||
const overrideFrameId = (handler: (p: any) => void) => {
|
||||
return (payload: any) => {
|
||||
|
|
@ -43,16 +55,20 @@ export class WKProvisionalPage {
|
|||
const wkPage = this._wkPage;
|
||||
|
||||
this._sessionListeners = [
|
||||
eventsHelper.addEventListener(session, 'Network.requestWillBeSent', overrideFrameId(e => wkPage._onRequestWillBeSent(session, e))),
|
||||
eventsHelper.addEventListener(session, 'Network.requestWillBeSent', overrideFrameId(e => this._onRequestWillBeSent(e))),
|
||||
eventsHelper.addEventListener(session, 'Network.requestIntercepted', overrideFrameId(e => wkPage._onRequestIntercepted(session, e))),
|
||||
eventsHelper.addEventListener(session, 'Network.responseReceived', overrideFrameId(e => wkPage._onResponseReceived(session, e))),
|
||||
eventsHelper.addEventListener(session, 'Network.loadingFinished', overrideFrameId(e => wkPage._onLoadingFinished(e))),
|
||||
eventsHelper.addEventListener(session, 'Network.loadingFailed', overrideFrameId(e => wkPage._onLoadingFailed(session, e))),
|
||||
eventsHelper.addEventListener(session, 'Network.loadingFinished', overrideFrameId(e => this._onLoadingFinished(e))),
|
||||
eventsHelper.addEventListener(session, 'Network.loadingFailed', overrideFrameId(e => this._onLoadingFailed(e))),
|
||||
];
|
||||
|
||||
this.initializationPromise = this._wkPage._initializeSession(session, true, ({ frameTree }) => this._handleFrameTree(frameTree));
|
||||
}
|
||||
|
||||
coopNavigationRequest(): network.Request | undefined {
|
||||
return this._coopNavigationRequest;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
eventsHelper.removeEventListeners(this._sessionListeners);
|
||||
}
|
||||
|
|
@ -62,6 +78,29 @@ export class WKProvisionalPage {
|
|||
this._wkPage._onFrameAttached(this._mainFrameId, null);
|
||||
}
|
||||
|
||||
private _onRequestWillBeSent(event: Protocol.Network.requestWillBeSentPayload) {
|
||||
if (this._coopNavigationRequest && this._coopNavigationRequest.url() === event.request.url) {
|
||||
// If it's a continuation of the main frame navigation request after COOP headers were received,
|
||||
// take over original request, and replace its request id with the new one.
|
||||
this._wkPage._adoptRequestFromNewProcess(this._coopNavigationRequest, this._session, event.requestId);
|
||||
// Simply ignore this event as it has already been dispatched from the original process
|
||||
// and there will ne no requestIntercepted event from the provisional process as it resumes
|
||||
// existing network load (that has already received reponse headers).
|
||||
return;
|
||||
}
|
||||
this._wkPage._onRequestWillBeSent(this._session, event);
|
||||
}
|
||||
|
||||
private _onLoadingFinished(event: Protocol.Network.loadingFinishedPayload): void {
|
||||
this._coopNavigationRequest = undefined;
|
||||
this._wkPage._onLoadingFinished(event);
|
||||
}
|
||||
|
||||
private _onLoadingFailed(event: Protocol.Network.loadingFailedPayload) {
|
||||
this._coopNavigationRequest = undefined;
|
||||
this._wkPage._onLoadingFailed(this._session, event);
|
||||
}
|
||||
|
||||
private _handleFrameTree(frameTree: Protocol.Page.FrameResourceTree) {
|
||||
assert(!frameTree.frame.parentId);
|
||||
this._mainFrameId = frameTree.frame.id;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ const debugLoggerColorMap = {
|
|||
'download': 34, // green
|
||||
'browser': 0, // reset
|
||||
'socks': 92, // purple
|
||||
'client-certificates': 92, // purple
|
||||
'error': 160, // red,
|
||||
'channel': 33, // blue
|
||||
'server': 45, // cyan
|
||||
|
|
|
|||
227
packages/playwright-core/types/types.d.ts
vendored
227
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -13166,10 +13166,14 @@ export interface BrowserType<Unused = {}> {
|
|||
chromiumSandbox?: boolean;
|
||||
|
||||
/**
|
||||
* TLS Client Authentication allows the server to request a client certificate and verify it.
|
||||
*
|
||||
* **Details**
|
||||
*
|
||||
* An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a
|
||||
* single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the
|
||||
* private key is encrypted. If the certificate is valid only for specific URLs, the `url` property should be provided
|
||||
* with a glob pattern to match the URLs that the certificate is valid for.
|
||||
* certficiate is encrypted. If the certificate is valid only for specific origins, the `origin` property should be
|
||||
* provided with a glob pattern to match the origins that the certificate is valid for.
|
||||
*
|
||||
* **NOTE** Using Client Certificates in combination with Proxy Servers is not supported.
|
||||
*
|
||||
|
|
@ -13178,34 +13182,29 @@ export interface BrowserType<Unused = {}> {
|
|||
*/
|
||||
clientCertificates?: Array<{
|
||||
/**
|
||||
* Glob pattern to match the URLs that the certificate is valid for.
|
||||
* Glob pattern to match against the request origin that the certificate is valid for.
|
||||
*/
|
||||
url: string;
|
||||
origin: string;
|
||||
|
||||
/**
|
||||
* List of client certificates to be used.
|
||||
* Path to the file with the certificate in PEM format.
|
||||
*/
|
||||
certs: Array<{
|
||||
/**
|
||||
* Path to the file with the certificate in PEM format.
|
||||
*/
|
||||
certPath?: string;
|
||||
certPath?: string;
|
||||
|
||||
/**
|
||||
* Path to the file with the private key in PEM format.
|
||||
*/
|
||||
keyPath?: string;
|
||||
/**
|
||||
* Path to the file with the private key in PEM format.
|
||||
*/
|
||||
keyPath?: string;
|
||||
|
||||
/**
|
||||
* Path to the PFX or PKCS12 encoded private key and certificate chain.
|
||||
*/
|
||||
pfxPath?: string;
|
||||
/**
|
||||
* Path to the PFX or PKCS12 encoded private key and certificate chain.
|
||||
*/
|
||||
pfxPath?: string;
|
||||
|
||||
/**
|
||||
* Passphrase for the private key (PEM or PFX).
|
||||
*/
|
||||
passphrase?: string;
|
||||
}>;
|
||||
/**
|
||||
* Passphrase for the private key (PEM or PFX).
|
||||
*/
|
||||
passphrase?: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
|
|
@ -15578,10 +15577,14 @@ export interface APIRequest {
|
|||
baseURL?: string;
|
||||
|
||||
/**
|
||||
* TLS Client Authentication allows the server to request a client certificate and verify it.
|
||||
*
|
||||
* **Details**
|
||||
*
|
||||
* An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a
|
||||
* single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the
|
||||
* private key is encrypted. If the certificate is valid only for specific URLs, the `url` property should be provided
|
||||
* with a glob pattern to match the URLs that the certificate is valid for.
|
||||
* certficiate is encrypted. If the certificate is valid only for specific origins, the `origin` property should be
|
||||
* provided with a glob pattern to match the origins that the certificate is valid for.
|
||||
*
|
||||
* **NOTE** Using Client Certificates in combination with Proxy Servers is not supported.
|
||||
*
|
||||
|
|
@ -15590,34 +15593,29 @@ export interface APIRequest {
|
|||
*/
|
||||
clientCertificates?: Array<{
|
||||
/**
|
||||
* Glob pattern to match the URLs that the certificate is valid for.
|
||||
* Glob pattern to match against the request origin that the certificate is valid for.
|
||||
*/
|
||||
url: string;
|
||||
origin: string;
|
||||
|
||||
/**
|
||||
* List of client certificates to be used.
|
||||
* Path to the file with the certificate in PEM format.
|
||||
*/
|
||||
certs: Array<{
|
||||
/**
|
||||
* Path to the file with the certificate in PEM format.
|
||||
*/
|
||||
certPath?: string;
|
||||
certPath?: string;
|
||||
|
||||
/**
|
||||
* Path to the file with the private key in PEM format.
|
||||
*/
|
||||
keyPath?: string;
|
||||
/**
|
||||
* Path to the file with the private key in PEM format.
|
||||
*/
|
||||
keyPath?: string;
|
||||
|
||||
/**
|
||||
* Path to the PFX or PKCS12 encoded private key and certificate chain.
|
||||
*/
|
||||
pfxPath?: string;
|
||||
/**
|
||||
* Path to the PFX or PKCS12 encoded private key and certificate chain.
|
||||
*/
|
||||
pfxPath?: string;
|
||||
|
||||
/**
|
||||
* Passphrase for the private key (PEM or PFX).
|
||||
*/
|
||||
passphrase?: string;
|
||||
}>;
|
||||
/**
|
||||
* Passphrase for the private key (PEM or PFX).
|
||||
*/
|
||||
passphrase?: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
|
|
@ -15934,8 +15932,8 @@ export interface APIRequestContext {
|
|||
maxRedirects?: number;
|
||||
|
||||
/**
|
||||
* Maximum number of times socket errors should be retried. Currently only `ECONNRESET` error is retried. An error
|
||||
* will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||
* Maximum number of times network errors should be retried. Currently only `ECONNRESET` error is retried. Does not
|
||||
* retry based on HTTP response codes. An error will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||
*/
|
||||
maxRetries?: number;
|
||||
|
||||
|
|
@ -16040,8 +16038,8 @@ export interface APIRequestContext {
|
|||
maxRedirects?: number;
|
||||
|
||||
/**
|
||||
* Maximum number of times socket errors should be retried. Currently only `ECONNRESET` error is retried. An error
|
||||
* will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||
* Maximum number of times network errors should be retried. Currently only `ECONNRESET` error is retried. Does not
|
||||
* retry based on HTTP response codes. An error will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||
*/
|
||||
maxRetries?: number;
|
||||
|
||||
|
|
@ -16126,8 +16124,8 @@ export interface APIRequestContext {
|
|||
maxRedirects?: number;
|
||||
|
||||
/**
|
||||
* Maximum number of times socket errors should be retried. Currently only `ECONNRESET` error is retried. An error
|
||||
* will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||
* Maximum number of times network errors should be retried. Currently only `ECONNRESET` error is retried. Does not
|
||||
* retry based on HTTP response codes. An error will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||
*/
|
||||
maxRetries?: number;
|
||||
|
||||
|
|
@ -16212,8 +16210,8 @@ export interface APIRequestContext {
|
|||
maxRedirects?: number;
|
||||
|
||||
/**
|
||||
* Maximum number of times socket errors should be retried. Currently only `ECONNRESET` error is retried. An error
|
||||
* will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||
* Maximum number of times network errors should be retried. Currently only `ECONNRESET` error is retried. Does not
|
||||
* retry based on HTTP response codes. An error will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||
*/
|
||||
maxRetries?: number;
|
||||
|
||||
|
|
@ -16340,8 +16338,8 @@ export interface APIRequestContext {
|
|||
maxRedirects?: number;
|
||||
|
||||
/**
|
||||
* Maximum number of times socket errors should be retried. Currently only `ECONNRESET` error is retried. An error
|
||||
* will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||
* Maximum number of times network errors should be retried. Currently only `ECONNRESET` error is retried. Does not
|
||||
* retry based on HTTP response codes. An error will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||
*/
|
||||
maxRetries?: number;
|
||||
|
||||
|
|
@ -16426,8 +16424,8 @@ export interface APIRequestContext {
|
|||
maxRedirects?: number;
|
||||
|
||||
/**
|
||||
* Maximum number of times socket errors should be retried. Currently only `ECONNRESET` error is retried. An error
|
||||
* will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||
* Maximum number of times network errors should be retried. Currently only `ECONNRESET` error is retried. Does not
|
||||
* retry based on HTTP response codes. An error will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||
*/
|
||||
maxRetries?: number;
|
||||
|
||||
|
|
@ -16772,10 +16770,14 @@ export interface Browser extends EventEmitter {
|
|||
bypassCSP?: boolean;
|
||||
|
||||
/**
|
||||
* TLS Client Authentication allows the server to request a client certificate and verify it.
|
||||
*
|
||||
* **Details**
|
||||
*
|
||||
* An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a
|
||||
* single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the
|
||||
* private key is encrypted. If the certificate is valid only for specific URLs, the `url` property should be provided
|
||||
* with a glob pattern to match the URLs that the certificate is valid for.
|
||||
* certficiate is encrypted. If the certificate is valid only for specific origins, the `origin` property should be
|
||||
* provided with a glob pattern to match the origins that the certificate is valid for.
|
||||
*
|
||||
* **NOTE** Using Client Certificates in combination with Proxy Servers is not supported.
|
||||
*
|
||||
|
|
@ -16784,34 +16786,29 @@ export interface Browser extends EventEmitter {
|
|||
*/
|
||||
clientCertificates?: Array<{
|
||||
/**
|
||||
* Glob pattern to match the URLs that the certificate is valid for.
|
||||
* Glob pattern to match against the request origin that the certificate is valid for.
|
||||
*/
|
||||
url: string;
|
||||
origin: string;
|
||||
|
||||
/**
|
||||
* List of client certificates to be used.
|
||||
* Path to the file with the certificate in PEM format.
|
||||
*/
|
||||
certs: Array<{
|
||||
/**
|
||||
* Path to the file with the certificate in PEM format.
|
||||
*/
|
||||
certPath?: string;
|
||||
certPath?: string;
|
||||
|
||||
/**
|
||||
* Path to the file with the private key in PEM format.
|
||||
*/
|
||||
keyPath?: string;
|
||||
/**
|
||||
* Path to the file with the private key in PEM format.
|
||||
*/
|
||||
keyPath?: string;
|
||||
|
||||
/**
|
||||
* Path to the PFX or PKCS12 encoded private key and certificate chain.
|
||||
*/
|
||||
pfxPath?: string;
|
||||
/**
|
||||
* Path to the PFX or PKCS12 encoded private key and certificate chain.
|
||||
*/
|
||||
pfxPath?: string;
|
||||
|
||||
/**
|
||||
* Passphrase for the private key (PEM or PFX).
|
||||
*/
|
||||
passphrase?: string;
|
||||
}>;
|
||||
/**
|
||||
* Passphrase for the private key (PEM or PFX).
|
||||
*/
|
||||
passphrase?: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
|
|
@ -19755,29 +19752,6 @@ export interface Touchscreen {
|
|||
* @param y Y coordinate relative to the main frame's viewport in CSS pixels.
|
||||
*/
|
||||
tap(x: number, y: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* Synthesizes a touch event.
|
||||
* @param type Type of the touch event.
|
||||
* @param touchPoints List of touch points for this event. `id` is a unique identifier of a touch point that helps identify it between
|
||||
* touch events for the duration of its movement around the surface.
|
||||
*/
|
||||
touch(type: "touchstart"|"touchend"|"touchmove"|"touchcancel", touchPoints: ReadonlyArray<{
|
||||
/**
|
||||
* x coordinate of the event in CSS pixels.
|
||||
*/
|
||||
x: number;
|
||||
|
||||
/**
|
||||
* y coordinate of the event in CSS pixels.
|
||||
*/
|
||||
y: number;
|
||||
|
||||
/**
|
||||
* Identifier used to track the touch point between events, must be unique within an event. Optional.
|
||||
*/
|
||||
id?: number;
|
||||
}>): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -20246,10 +20220,14 @@ export interface BrowserContextOptions {
|
|||
bypassCSP?: boolean;
|
||||
|
||||
/**
|
||||
* TLS Client Authentication allows the server to request a client certificate and verify it.
|
||||
*
|
||||
* **Details**
|
||||
*
|
||||
* An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a
|
||||
* single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the
|
||||
* private key is encrypted. If the certificate is valid only for specific URLs, the `url` property should be provided
|
||||
* with a glob pattern to match the URLs that the certificate is valid for.
|
||||
* certficiate is encrypted. If the certificate is valid only for specific origins, the `origin` property should be
|
||||
* provided with a glob pattern to match the origins that the certificate is valid for.
|
||||
*
|
||||
* **NOTE** Using Client Certificates in combination with Proxy Servers is not supported.
|
||||
*
|
||||
|
|
@ -20258,34 +20236,29 @@ export interface BrowserContextOptions {
|
|||
*/
|
||||
clientCertificates?: Array<{
|
||||
/**
|
||||
* Glob pattern to match the URLs that the certificate is valid for.
|
||||
* Glob pattern to match against the request origin that the certificate is valid for.
|
||||
*/
|
||||
url: string;
|
||||
origin: string;
|
||||
|
||||
/**
|
||||
* List of client certificates to be used.
|
||||
* Path to the file with the certificate in PEM format.
|
||||
*/
|
||||
certs: Array<{
|
||||
/**
|
||||
* Path to the file with the certificate in PEM format.
|
||||
*/
|
||||
certPath?: string;
|
||||
certPath?: string;
|
||||
|
||||
/**
|
||||
* Path to the file with the private key in PEM format.
|
||||
*/
|
||||
keyPath?: string;
|
||||
/**
|
||||
* Path to the file with the private key in PEM format.
|
||||
*/
|
||||
keyPath?: string;
|
||||
|
||||
/**
|
||||
* Path to the PFX or PKCS12 encoded private key and certificate chain.
|
||||
*/
|
||||
pfxPath?: string;
|
||||
/**
|
||||
* Path to the PFX or PKCS12 encoded private key and certificate chain.
|
||||
*/
|
||||
pfxPath?: string;
|
||||
|
||||
/**
|
||||
* Passphrase for the private key (PEM or PFX).
|
||||
*/
|
||||
passphrase?: string;
|
||||
}>;
|
||||
/**
|
||||
* Passphrase for the private key (PEM or PFX).
|
||||
*/
|
||||
passphrase?: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
|
|
|
|||
9
packages/playwright-ct-core/index.d.ts
vendored
9
packages/playwright-ct-core/index.d.ts
vendored
|
|
@ -38,14 +38,13 @@ interface RequestHandler {
|
|||
run(args: { request: Request, requestId?: string, resolutionContext?: { baseUrl?: string } }): Promise<{ response?: Response } | null>;
|
||||
}
|
||||
|
||||
export interface RouteFixture {
|
||||
(...args: Parameters<BrowserContext['route']>): Promise<void>;
|
||||
(handlers: RequestHandler[]): Promise<void>;
|
||||
(handler: RequestHandler): Promise<void>;
|
||||
export interface RouterFixture {
|
||||
route(...args: Parameters<BrowserContext['route']>): Promise<void>;
|
||||
use(...handlers: RequestHandler[]): Promise<void>;
|
||||
}
|
||||
|
||||
export type TestType<ComponentFixtures> = BaseTestType<
|
||||
PlaywrightTestArgs & PlaywrightTestOptions & ComponentFixtures & { route: RouteFixture },
|
||||
PlaywrightTestArgs & PlaywrightTestOptions & ComponentFixtures & { router: RouterFixture },
|
||||
PlaywrightWorkerArgs & PlaywrightWorkerOptions
|
||||
>;
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import type { Component, JsxComponent, MountOptions, ObjectComponentOptions } fr
|
|||
import type { ContextReuseMode, FullConfigInternal } from '../../playwright/src/common/config';
|
||||
import type { ImportRef } from './injected/importRegistry';
|
||||
import { wrapObject } from './injected/serializers';
|
||||
import { Router } from './route';
|
||||
import type { RouteFixture } from '../index';
|
||||
import { Router } from './router';
|
||||
import type { RouterFixture } from '../index';
|
||||
|
||||
let boundCallbacksForMount: Function[] = [];
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ interface MountResult extends Locator {
|
|||
|
||||
type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
|
||||
mount: (component: any, options: any) => Promise<MountResult>;
|
||||
route: RouteFixture;
|
||||
router: RouterFixture;
|
||||
};
|
||||
type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions;
|
||||
type BaseTestFixtures = {
|
||||
|
|
@ -80,9 +80,9 @@ export const fixtures: Fixtures<TestFixtures, WorkerFixtures, BaseTestFixtures>
|
|||
boundCallbacksForMount = [];
|
||||
},
|
||||
|
||||
route: async ({ context, baseURL }, use) => {
|
||||
router: async ({ context, baseURL }, use) => {
|
||||
const router = new Router(context, baseURL);
|
||||
await use((...args) => router.handle(...args));
|
||||
await use(router);
|
||||
await router.dispose();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -134,25 +134,14 @@ export class Router {
|
|||
};
|
||||
}
|
||||
|
||||
async handle(...args: any[]) {
|
||||
// Multiple RequestHandlers.
|
||||
if (Array.isArray(args[0])) {
|
||||
const handlers = args[0] as RequestHandler[];
|
||||
this._requestHandlers = handlers.concat(this._requestHandlers);
|
||||
await this._updateRequestHandlersRoute();
|
||||
return;
|
||||
}
|
||||
// Single RequestHandler.
|
||||
if (args.length === 1 && typeof args[0] === 'object') {
|
||||
const handlers = [args[0] as RequestHandler];
|
||||
this._requestHandlers = handlers.concat(this._requestHandlers);
|
||||
await this._updateRequestHandlersRoute();
|
||||
return;
|
||||
}
|
||||
// Arguments of BrowserContext.route(url, handler, options?).
|
||||
const routeArgs = args as RouteArgs;
|
||||
async route(...routeArgs: RouteArgs) {
|
||||
this._routes.push(routeArgs);
|
||||
await this._context.route(...routeArgs);
|
||||
return await this._context.route(...routeArgs);
|
||||
}
|
||||
|
||||
async use(...handlers: RequestHandler[]) {
|
||||
this._requestHandlers = handlers.concat(this._requestHandlers);
|
||||
await this._updateRequestHandlersRoute();
|
||||
}
|
||||
|
||||
async dispose() {
|
||||
|
|
@ -69,6 +69,10 @@ export function createPlugin(): TestRunnerPlugin {
|
|||
if (stoppableServer)
|
||||
await new Promise(f => stoppableServer.stop(f));
|
||||
},
|
||||
|
||||
populateDependencies: async () => {
|
||||
await buildBundle(config, configDir);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -157,7 +161,7 @@ export async function buildBundle(config: FullConfig, configDir: string): Promis
|
|||
const viteConfig = await createConfig(dirs, config, frameworkPluginFactory, jsxInJS);
|
||||
|
||||
if (sourcesDirty) {
|
||||
// Only add out own plugin when we actually build / transform.
|
||||
// Only add our own plugin when we actually build / transform.
|
||||
log('build');
|
||||
const depsCollector = new Map<string, string[]>();
|
||||
const buildConfig = mergeConfig(viteConfig, {
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export class FullConfigInternal {
|
|||
cliArgs: string[] = [];
|
||||
cliGrep: string | undefined;
|
||||
cliGrepInvert: string | undefined;
|
||||
cliOnlyChanged: string | undefined;
|
||||
cliProjectFilter?: string[];
|
||||
cliListOnly = false;
|
||||
cliPassWithNoTests?: boolean;
|
||||
|
|
|
|||
|
|
@ -479,12 +479,10 @@ function resolveFileToConfig(file: string | undefined) {
|
|||
type ClientCertificates = NonNullable<PlaywrightTestOptions['clientCertificates']>;
|
||||
|
||||
function resolveClientCerticates(clientCertificates: ClientCertificates): ClientCertificates {
|
||||
for (const { certs } of clientCertificates) {
|
||||
for (const cert of certs) {
|
||||
cert.certPath = resolveFileToConfig(cert.certPath);
|
||||
cert.keyPath = resolveFileToConfig(cert.keyPath);
|
||||
cert.pfxPath = resolveFileToConfig(cert.pfxPath);
|
||||
}
|
||||
for (const cert of clientCertificates) {
|
||||
cert.certPath = resolveFileToConfig(cert.certPath);
|
||||
cert.keyPath = resolveFileToConfig(cert.keyPath);
|
||||
cert.pfxPath = resolveFileToConfig(cert.pfxPath);
|
||||
}
|
||||
return clientCertificates;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ export interface TestServerInterface {
|
|||
listTests(params: {
|
||||
projects?: string[];
|
||||
locations?: string[];
|
||||
grep?: string;
|
||||
grepInvert?: string;
|
||||
}): Promise<{
|
||||
report: ReportEntry[],
|
||||
status: reporterTypes.FullResult['status']
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import type { ReporterV2 } from '../reporters/reporterV2';
|
|||
export interface TestRunnerPlugin {
|
||||
name: string;
|
||||
setup?(config: FullConfig, configDir: string, reporter: ReporterV2): Promise<void>;
|
||||
populateDependencies?(): Promise<void>;
|
||||
begin?(suite: Suite): Promise<void>;
|
||||
end?(): Promise<void>;
|
||||
teardown?(): Promise<void>;
|
||||
|
|
|
|||
|
|
@ -153,12 +153,14 @@ Examples:
|
|||
$ npx playwright merge-reports playwright-report`);
|
||||
}
|
||||
|
||||
|
||||
async function runTests(args: string[], opts: { [key: string]: any }) {
|
||||
await startProfiling();
|
||||
const cliOverrides = overridesFromOptions(opts);
|
||||
|
||||
if (opts.ui || opts.uiHost || opts.uiPort) {
|
||||
if (opts.onlyChanged)
|
||||
throw new Error(`--only-changed is not supported in UI mode. If you'd like that to change, see https://github.com/microsoft/playwright/issues/15075 for more details.`);
|
||||
|
||||
const status = await testServer.runUIMode(opts.config, {
|
||||
host: opts.uiHost,
|
||||
port: opts.uiPort ? +opts.uiPort : undefined,
|
||||
|
|
@ -192,6 +194,7 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
|
|||
|
||||
config.cliArgs = args;
|
||||
config.cliGrep = opts.grep as string | undefined;
|
||||
config.cliOnlyChanged = opts.onlyChanged === true ? 'HEAD' : opts.onlyChanged;
|
||||
config.cliGrepInvert = opts.grepInvert as string | undefined;
|
||||
config.cliListOnly = !!opts.list;
|
||||
config.cliProjectFilter = opts.project || undefined;
|
||||
|
|
@ -352,6 +355,7 @@ const testOptions: [string, string][] = [
|
|||
['--max-failures <N>', `Stop after the first N failures`],
|
||||
['--no-deps', 'Do not run project dependencies'],
|
||||
['--output <dir>', `Folder for output artifacts (default: "test-results")`],
|
||||
['--only-changed [ref]', `Only run tests that have been changed between 'HEAD' and 'ref'. Defaults to running all uncommitted changes. Only supports Git.`],
|
||||
['--pass-with-no-tests', `Makes test run succeed even if no tests were found`],
|
||||
['--project <project-name...>', `Only run tests from the specified list of projects, supports '*' wildcard (default: run all projects)`],
|
||||
['--quiet', `Suppress stdio`],
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import { dependenciesForTestFile } from '../transform/compilationCache';
|
|||
import { sourceMapSupport } from '../utilsBundle';
|
||||
import type { RawSourceMap } from 'source-map';
|
||||
|
||||
|
||||
export async function collectProjectsAndTestFiles(testRun: TestRun, doNotRunTestsOutsideProjectFilter: boolean, additionalFileMatcher?: Matcher) {
|
||||
const config = testRun.config;
|
||||
const fsCache = new Map();
|
||||
|
|
@ -118,7 +119,7 @@ export async function loadFileSuites(testRun: TestRun, mode: 'out-of-process' |
|
|||
}
|
||||
}
|
||||
|
||||
export async function createRootSuite(testRun: TestRun, errors: TestError[], shouldFilterOnly: boolean): Promise<Suite> {
|
||||
export async function createRootSuite(testRun: TestRun, errors: TestError[], shouldFilterOnly: boolean, additionalFileMatcher?: Matcher): Promise<Suite> {
|
||||
const config = testRun.config;
|
||||
// Create root suite, where each child will be a project suite with cloned file suites inside it.
|
||||
const rootSuite = new Suite('', 'root');
|
||||
|
|
@ -135,7 +136,8 @@ export async function createRootSuite(testRun: TestRun, errors: TestError[], sho
|
|||
|
||||
// Filter file suites for all projects.
|
||||
for (const [project, fileSuites] of testRun.projectSuites) {
|
||||
const projectSuite = createProjectSuite(project, fileSuites);
|
||||
const filteredFileSuites = additionalFileMatcher ? fileSuites.filter(fileSuite => additionalFileMatcher(fileSuite.location!.file)) : fileSuites;
|
||||
const projectSuite = createProjectSuite(project, filteredFileSuites);
|
||||
projectSuites.set(project, projectSuite);
|
||||
const filteredProjectSuite = filterProjectSuite(projectSuite, { cliFileFilters, cliTitleMatcher, testIdMatcher: config.testIdMatcher });
|
||||
filteredProjectSuites.set(project, filteredProjectSuite);
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@ function computeCommandHash(config: FullConfigInternal) {
|
|||
command.cliGrep = config.cliGrep;
|
||||
if (config.cliGrepInvert)
|
||||
command.cliGrepInvert = config.cliGrepInvert;
|
||||
if (config.cliOnlyChanged)
|
||||
command.cliOnlyChanged = config.cliOnlyChanged;
|
||||
if (Object.keys(command).length)
|
||||
parts.push(calculateSha1(JSON.stringify(command)).substring(0, 7));
|
||||
return parts.join('-');
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@ import { createReporters } from './reporters';
|
|||
import { TestRun, createTaskRunner, createTaskRunnerForList } from './tasks';
|
||||
import type { FullConfigInternal } from '../common/config';
|
||||
import { runWatchModeLoop } from './watchMode';
|
||||
import { InternalReporter } from '../reporters/internalReporter';
|
||||
import { Multiplexer } from '../reporters/multiplexer';
|
||||
import type { Suite } from '../common/test';
|
||||
import { wrapReporterAsV2 } from '../reporters/reporterV2';
|
||||
import { affectedTestFiles } from '../transform/compilationCache';
|
||||
|
|
@ -79,25 +77,28 @@ export class Runner {
|
|||
// Legacy webServer support.
|
||||
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
|
||||
|
||||
const reporter = new InternalReporter(new Multiplexer(await createReporters(config, listOnly ? 'list' : 'test', false)));
|
||||
const taskRunner = listOnly ? createTaskRunnerForList(config, reporter, 'in-process', { failOnLoadErrors: true })
|
||||
: createTaskRunner(config, reporter);
|
||||
const reporters = await createReporters(config, listOnly ? 'list' : 'test', false);
|
||||
const taskRunner = listOnly ? createTaskRunnerForList(
|
||||
config,
|
||||
reporters,
|
||||
'in-process',
|
||||
{ failOnLoadErrors: true }) : createTaskRunner(config, reporters);
|
||||
|
||||
const testRun = new TestRun(config, reporter);
|
||||
reporter.onConfigure(config.config);
|
||||
const testRun = new TestRun(config);
|
||||
taskRunner.reporter.onConfigure(config.config);
|
||||
|
||||
const taskStatus = await taskRunner.run(testRun, deadline);
|
||||
let status: FullResult['status'] = testRun.failureTracker.result();
|
||||
if (status === 'passed' && taskStatus !== 'passed')
|
||||
status = taskStatus;
|
||||
const modifiedResult = await reporter.onEnd({ status });
|
||||
const modifiedResult = await taskRunner.reporter.onEnd({ status });
|
||||
if (modifiedResult && modifiedResult.status)
|
||||
status = modifiedResult.status;
|
||||
|
||||
if (!listOnly)
|
||||
await writeLastRunInfo(testRun, status);
|
||||
|
||||
await reporter.onExit();
|
||||
await taskRunner.reporter.onExit();
|
||||
|
||||
// Calling process.exit() might truncate large stdout/stderr output.
|
||||
// See https://github.com/nodejs/node/issues/6456.
|
||||
|
|
@ -110,23 +111,23 @@ export class Runner {
|
|||
async loadAllTests(mode: 'in-process' | 'out-of-process' = 'in-process'): Promise<{ status: FullResult['status'], suite?: Suite, errors: TestError[] }> {
|
||||
const config = this._config;
|
||||
const errors: TestError[] = [];
|
||||
const reporter = new InternalReporter(new Multiplexer([wrapReporterAsV2({
|
||||
const reporters = [wrapReporterAsV2({
|
||||
onError(error: TestError) {
|
||||
errors.push(error);
|
||||
}
|
||||
})]));
|
||||
const taskRunner = createTaskRunnerForList(config, reporter, mode, { failOnLoadErrors: true });
|
||||
const testRun = new TestRun(config, reporter);
|
||||
reporter.onConfigure(config.config);
|
||||
})];
|
||||
const taskRunner = createTaskRunnerForList(config, reporters, mode, { failOnLoadErrors: true });
|
||||
const testRun = new TestRun(config);
|
||||
taskRunner.reporter.onConfigure(config.config);
|
||||
|
||||
const taskStatus = await taskRunner.run(testRun, 0);
|
||||
let status: FullResult['status'] = testRun.failureTracker.result();
|
||||
if (status === 'passed' && taskStatus !== 'passed')
|
||||
status = taskStatus;
|
||||
const modifiedResult = await reporter.onEnd({ status });
|
||||
const modifiedResult = await taskRunner.reporter.onEnd({ status });
|
||||
if (modifiedResult && modifiedResult.status)
|
||||
status = modifiedResult.status;
|
||||
await reporter.onExit();
|
||||
await taskRunner.reporter.onExit();
|
||||
return { status, suite: testRun.rootSuite, errors };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,20 +20,26 @@ import type { FullResult, TestError } from '../../types/testReporter';
|
|||
import { SigIntWatcher } from './sigIntWatcher';
|
||||
import { serializeError } from '../util';
|
||||
import type { ReporterV2 } from '../reporters/reporterV2';
|
||||
import { InternalReporter } from '../reporters/internalReporter';
|
||||
import { Multiplexer } from '../reporters/multiplexer';
|
||||
|
||||
type TaskPhase<Context> = (context: Context, errors: TestError[], softErrors: TestError[]) => Promise<void> | void;
|
||||
type TaskPhase<Context> = (reporter: ReporterV2, context: Context, errors: TestError[], softErrors: TestError[]) => Promise<void> | void;
|
||||
export type Task<Context> = { setup?: TaskPhase<Context>, teardown?: TaskPhase<Context> };
|
||||
|
||||
export class TaskRunner<Context> {
|
||||
private _tasks: { name: string, task: Task<Context> }[] = [];
|
||||
private _reporter: ReporterV2;
|
||||
readonly reporter: InternalReporter;
|
||||
private _hasErrors = false;
|
||||
private _interrupted = false;
|
||||
private _isTearDown = false;
|
||||
private _globalTimeoutForError: number;
|
||||
|
||||
constructor(reporter: ReporterV2, globalTimeoutForError: number) {
|
||||
this._reporter = reporter;
|
||||
static create<Context>(reporters: ReporterV2[], globalTimeoutForError: number = 0) {
|
||||
return new TaskRunner<Context>(createInternalReporter(reporters), globalTimeoutForError);
|
||||
}
|
||||
|
||||
private constructor(reporter: InternalReporter, globalTimeoutForError: number) {
|
||||
this.reporter = reporter;
|
||||
this._globalTimeoutForError = globalTimeoutForError;
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +56,7 @@ export class TaskRunner<Context> {
|
|||
async runDeferCleanup(context: Context, deadline: number, cancelPromise = new ManualPromise<void>()): Promise<{ status: FullResult['status'], cleanup: () => Promise<FullResult['status']> }> {
|
||||
const sigintWatcher = new SigIntWatcher();
|
||||
const timeoutWatcher = new TimeoutWatcher(deadline);
|
||||
const teardownRunner = new TaskRunner<Context>(this._reporter, this._globalTimeoutForError);
|
||||
const teardownRunner = new TaskRunner<Context>(this.reporter, this._globalTimeoutForError);
|
||||
teardownRunner._isTearDown = true;
|
||||
|
||||
let currentTaskName: string | undefined;
|
||||
|
|
@ -65,13 +71,13 @@ export class TaskRunner<Context> {
|
|||
const softErrors: TestError[] = [];
|
||||
try {
|
||||
teardownRunner._tasks.unshift({ name: `teardown for ${name}`, task: { setup: task.teardown } });
|
||||
await task.setup?.(context, errors, softErrors);
|
||||
await task.setup?.(this.reporter, context, errors, softErrors);
|
||||
} catch (e) {
|
||||
debug('pw:test:task')(`error in "${name}": `, e);
|
||||
errors.push(serializeError(e));
|
||||
} finally {
|
||||
for (const error of [...softErrors, ...errors])
|
||||
this._reporter.onError?.(error);
|
||||
this.reporter.onError?.(error);
|
||||
if (errors.length) {
|
||||
if (!this._isTearDown)
|
||||
this._interrupted = true;
|
||||
|
|
@ -99,7 +105,7 @@ export class TaskRunner<Context> {
|
|||
if (sigintWatcher.hadSignal() || cancelPromise?.isDone()) {
|
||||
status = 'interrupted';
|
||||
} else if (timeoutWatcher.timedOut()) {
|
||||
this._reporter.onError?.({ message: colors.red(`Timed out waiting ${this._globalTimeoutForError / 1000}s for the ${currentTaskName} to run`) });
|
||||
this.reporter.onError?.({ message: colors.red(`Timed out waiting ${this._globalTimeoutForError / 1000}s for the ${currentTaskName} to run`) });
|
||||
status = 'timedout';
|
||||
} else if (this._hasErrors) {
|
||||
status = 'failed';
|
||||
|
|
@ -140,3 +146,7 @@ class TimeoutWatcher {
|
|||
clearTimeout(this._timer);
|
||||
}
|
||||
}
|
||||
|
||||
function createInternalReporter(reporters: ReporterV2[]): InternalReporter {
|
||||
return new InternalReporter(new Multiplexer(reporters));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import type { Matcher } from '../util';
|
|||
import { Suite } from '../common/test';
|
||||
import { buildDependentProjects, buildTeardownToSetupsMap, filterProjects } from './projectUtils';
|
||||
import { FailureTracker } from './failureTracker';
|
||||
import { detectChangedTests } from './vcs';
|
||||
|
||||
const readDirAsync = promisify(fs.readdir);
|
||||
|
||||
|
|
@ -46,7 +47,6 @@ export type Phase = {
|
|||
};
|
||||
|
||||
export class TestRun {
|
||||
readonly reporter: ReporterV2;
|
||||
readonly config: FullConfigInternal;
|
||||
readonly failureTracker: FailureTracker;
|
||||
rootSuite: Suite | undefined = undefined;
|
||||
|
|
@ -54,37 +54,36 @@ export class TestRun {
|
|||
projectFiles: Map<FullProjectInternal, string[]> = new Map();
|
||||
projectSuites: Map<FullProjectInternal, Suite[]> = new Map();
|
||||
|
||||
constructor(config: FullConfigInternal, reporter: ReporterV2) {
|
||||
constructor(config: FullConfigInternal) {
|
||||
this.config = config;
|
||||
this.reporter = reporter;
|
||||
this.failureTracker = new FailureTracker(config);
|
||||
}
|
||||
}
|
||||
|
||||
export function createTaskRunner(config: FullConfigInternal, reporter: ReporterV2): TaskRunner<TestRun> {
|
||||
const taskRunner = new TaskRunner<TestRun>(reporter, config.config.globalTimeout);
|
||||
export function createTaskRunner(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner<TestRun> {
|
||||
const taskRunner = TaskRunner.create<TestRun>(reporters, config.config.globalTimeout);
|
||||
addGlobalSetupTasks(taskRunner, config);
|
||||
taskRunner.addTask('load tests', createLoadTask('in-process', { filterOnly: true, failOnLoadErrors: true }));
|
||||
taskRunner.addTask('load tests', createLoadTask('in-process', { filterOnly: true, filterOnlyChanged: true, failOnLoadErrors: true }));
|
||||
addRunTasks(taskRunner, config);
|
||||
return taskRunner;
|
||||
}
|
||||
|
||||
export function createTaskRunnerForWatchSetup(config: FullConfigInternal, reporter: ReporterV2): TaskRunner<TestRun> {
|
||||
const taskRunner = new TaskRunner<TestRun>(reporter, 0);
|
||||
export function createTaskRunnerForWatchSetup(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner<TestRun> {
|
||||
const taskRunner = TaskRunner.create<TestRun>(reporters);
|
||||
addGlobalSetupTasks(taskRunner, config);
|
||||
return taskRunner;
|
||||
}
|
||||
|
||||
export function createTaskRunnerForWatch(config: FullConfigInternal, reporter: ReporterV2, additionalFileMatcher?: Matcher): TaskRunner<TestRun> {
|
||||
const taskRunner = new TaskRunner<TestRun>(reporter, 0);
|
||||
taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true, additionalFileMatcher }));
|
||||
export function createTaskRunnerForWatch(config: FullConfigInternal, reporters: ReporterV2[], additionalFileMatcher?: Matcher): TaskRunner<TestRun> {
|
||||
const taskRunner = TaskRunner.create<TestRun>(reporters);
|
||||
taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, filterOnlyChanged: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true, additionalFileMatcher }));
|
||||
addRunTasks(taskRunner, config);
|
||||
return taskRunner;
|
||||
}
|
||||
|
||||
export function createTaskRunnerForTestServer(config: FullConfigInternal, reporter: ReporterV2): TaskRunner<TestRun> {
|
||||
const taskRunner = new TaskRunner<TestRun>(reporter, 0);
|
||||
taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true }));
|
||||
export function createTaskRunnerForTestServer(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner<TestRun> {
|
||||
const taskRunner = TaskRunner.create<TestRun>(reporters);
|
||||
taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, filterOnlyChanged: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true }));
|
||||
addRunTasks(taskRunner, config);
|
||||
return taskRunner;
|
||||
}
|
||||
|
|
@ -107,15 +106,15 @@ function addRunTasks(taskRunner: TaskRunner<TestRun>, config: FullConfigInternal
|
|||
return taskRunner;
|
||||
}
|
||||
|
||||
export function createTaskRunnerForList(config: FullConfigInternal, reporter: ReporterV2, mode: 'in-process' | 'out-of-process', options: { failOnLoadErrors: boolean }): TaskRunner<TestRun> {
|
||||
const taskRunner = new TaskRunner<TestRun>(reporter, config.config.globalTimeout);
|
||||
taskRunner.addTask('load tests', createLoadTask(mode, { ...options, filterOnly: false }));
|
||||
export function createTaskRunnerForList(config: FullConfigInternal, reporters: ReporterV2[], mode: 'in-process' | 'out-of-process', options: { failOnLoadErrors: boolean }): TaskRunner<TestRun> {
|
||||
const taskRunner = TaskRunner.create<TestRun>(reporters, config.config.globalTimeout);
|
||||
taskRunner.addTask('load tests', createLoadTask(mode, { ...options, filterOnly: false, filterOnlyChanged: false }));
|
||||
taskRunner.addTask('report begin', createReportBeginTask());
|
||||
return taskRunner;
|
||||
}
|
||||
|
||||
export function createTaskRunnerForListFiles(config: FullConfigInternal, reporter: ReporterV2): TaskRunner<TestRun> {
|
||||
const taskRunner = new TaskRunner<TestRun>(reporter, config.config.globalTimeout);
|
||||
export function createTaskRunnerForListFiles(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner<TestRun> {
|
||||
const taskRunner = TaskRunner.create<TestRun>(reporters, config.config.globalTimeout);
|
||||
taskRunner.addTask('load tests', createListFilesTask());
|
||||
taskRunner.addTask('report begin', createReportBeginTask());
|
||||
return taskRunner;
|
||||
|
|
@ -123,7 +122,7 @@ export function createTaskRunnerForListFiles(config: FullConfigInternal, reporte
|
|||
|
||||
function createReportBeginTask(): Task<TestRun> {
|
||||
return {
|
||||
setup: async ({ reporter, rootSuite }) => {
|
||||
setup: async (reporter, { rootSuite }) => {
|
||||
reporter.onBegin(rootSuite!);
|
||||
},
|
||||
teardown: async ({}) => {},
|
||||
|
|
@ -132,7 +131,7 @@ function createReportBeginTask(): Task<TestRun> {
|
|||
|
||||
function createPluginSetupTask(plugin: TestRunnerPluginRegistration): Task<TestRun> {
|
||||
return {
|
||||
setup: async ({ config, reporter }) => {
|
||||
setup: async (reporter, { config }) => {
|
||||
if (typeof plugin.factory === 'function')
|
||||
plugin.instance = await plugin.factory();
|
||||
else
|
||||
|
|
@ -147,7 +146,7 @@ function createPluginSetupTask(plugin: TestRunnerPluginRegistration): Task<TestR
|
|||
|
||||
function createPluginBeginTask(plugin: TestRunnerPluginRegistration): Task<TestRun> {
|
||||
return {
|
||||
setup: async ({ rootSuite }) => {
|
||||
setup: async (reporter, { rootSuite }) => {
|
||||
await plugin.instance?.begin?.(rootSuite!);
|
||||
},
|
||||
teardown: async () => {
|
||||
|
|
@ -161,13 +160,13 @@ function createGlobalSetupTask(): Task<TestRun> {
|
|||
let globalSetupFinished = false;
|
||||
let teardownHook: any;
|
||||
return {
|
||||
setup: async ({ config }) => {
|
||||
setup: async (reporter, { config }) => {
|
||||
const setupHook = config.config.globalSetup ? await loadGlobalHook(config, config.config.globalSetup) : undefined;
|
||||
teardownHook = config.config.globalTeardown ? await loadGlobalHook(config, config.config.globalTeardown) : undefined;
|
||||
globalSetupResult = setupHook ? await setupHook(config.config) : undefined;
|
||||
globalSetupFinished = true;
|
||||
},
|
||||
teardown: async ({ config }) => {
|
||||
teardown: async (reporter, { config }) => {
|
||||
if (typeof globalSetupResult === 'function')
|
||||
await globalSetupResult();
|
||||
if (globalSetupFinished)
|
||||
|
|
@ -178,7 +177,7 @@ function createGlobalSetupTask(): Task<TestRun> {
|
|||
|
||||
function createRemoveOutputDirsTask(): Task<TestRun> {
|
||||
return {
|
||||
setup: async ({ config }) => {
|
||||
setup: async (reporter, { config }) => {
|
||||
const outputDirs = new Set<string>();
|
||||
const projects = filterProjects(config.projects, config.cliProjectFilter);
|
||||
projects.forEach(p => outputDirs.add(p.project.outputDir));
|
||||
|
|
@ -202,7 +201,7 @@ function createRemoveOutputDirsTask(): Task<TestRun> {
|
|||
|
||||
function createListFilesTask(): Task<TestRun> {
|
||||
return {
|
||||
setup: async (testRun, errors) => {
|
||||
setup: async (reporter, testRun, errors) => {
|
||||
testRun.rootSuite = await createRootSuite(testRun, errors, false);
|
||||
testRun.failureTracker.onRootSuite(testRun.rootSuite);
|
||||
await collectProjectsAndTestFiles(testRun, false);
|
||||
|
|
@ -223,12 +222,21 @@ function createListFilesTask(): Task<TestRun> {
|
|||
};
|
||||
}
|
||||
|
||||
function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, failOnLoadErrors: boolean, doNotRunDepsOutsideProjectFilter?: boolean, additionalFileMatcher?: Matcher }): Task<TestRun> {
|
||||
function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, filterOnlyChanged: boolean, failOnLoadErrors: boolean, doNotRunDepsOutsideProjectFilter?: boolean, additionalFileMatcher?: Matcher }): Task<TestRun> {
|
||||
return {
|
||||
setup: async (testRun, errors, softErrors) => {
|
||||
setup: async (reporter, testRun, errors, softErrors) => {
|
||||
await collectProjectsAndTestFiles(testRun, !!options.doNotRunDepsOutsideProjectFilter, options.additionalFileMatcher);
|
||||
await loadFileSuites(testRun, mode, options.failOnLoadErrors ? errors : softErrors);
|
||||
testRun.rootSuite = await createRootSuite(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly);
|
||||
|
||||
let cliOnlyChangedMatcher: Matcher | undefined = undefined;
|
||||
if (testRun.config.cliOnlyChanged && options.filterOnlyChanged) {
|
||||
for (const plugin of testRun.config.plugins)
|
||||
await plugin.instance?.populateDependencies?.();
|
||||
const changedFiles = await detectChangedTests(testRun.config.cliOnlyChanged, testRun.config.configDir);
|
||||
cliOnlyChangedMatcher = file => changedFiles.has(file);
|
||||
}
|
||||
|
||||
testRun.rootSuite = await createRootSuite(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly, cliOnlyChangedMatcher);
|
||||
testRun.failureTracker.onRootSuite(testRun.rootSuite);
|
||||
// Fail when no tests.
|
||||
if (options.failOnLoadErrors && !testRun.rootSuite.allTests().length && !testRun.config.cliPassWithNoTests && !testRun.config.config.shard) {
|
||||
|
|
@ -247,7 +255,7 @@ function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filter
|
|||
|
||||
function createPhasesTask(): Task<TestRun> {
|
||||
return {
|
||||
setup: async testRun => {
|
||||
setup: async (reporter, testRun) => {
|
||||
let maxConcurrentTestGroups = 0;
|
||||
|
||||
const processed = new Set<FullProjectInternal>();
|
||||
|
|
@ -278,7 +286,7 @@ function createPhasesTask(): Task<TestRun> {
|
|||
processed.add(project);
|
||||
if (phaseProjects.length) {
|
||||
let testGroupsInPhase = 0;
|
||||
const phase: Phase = { dispatcher: new Dispatcher(testRun.config, testRun.reporter, testRun.failureTracker), projects: [] };
|
||||
const phase: Phase = { dispatcher: new Dispatcher(testRun.config, reporter, testRun.failureTracker), projects: [] };
|
||||
testRun.phases.push(phase);
|
||||
for (const project of phaseProjects) {
|
||||
const projectSuite = projectToSuite.get(project)!;
|
||||
|
|
@ -298,7 +306,7 @@ function createPhasesTask(): Task<TestRun> {
|
|||
|
||||
function createRunTestsTask(): Task<TestRun> {
|
||||
return {
|
||||
setup: async ({ phases, failureTracker }) => {
|
||||
setup: async (reporter, { phases, failureTracker }) => {
|
||||
const successfulProjects = new Set<FullProjectInternal>();
|
||||
const extraEnvByProjectId: EnvByProjectId = new Map();
|
||||
const teardownToSetups = buildTeardownToSetupsMap(phases.map(phase => phase.projects.map(p => p.project)).flat());
|
||||
|
|
@ -342,7 +350,7 @@ function createRunTestsTask(): Task<TestRun> {
|
|||
}
|
||||
}
|
||||
},
|
||||
teardown: async ({ phases }) => {
|
||||
teardown: async (reporter, { phases }) => {
|
||||
for (const { dispatcher } of phases.reverse())
|
||||
await dispatcher.stop();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -22,12 +22,10 @@ import type { Transport, HttpServer } from 'playwright-core/lib/utils';
|
|||
import type * as reporterTypes from '../../types/testReporter';
|
||||
import { collectAffectedTestFiles, dependenciesForTestFile } from '../transform/compilationCache';
|
||||
import type { ConfigLocation, FullConfigInternal } from '../common/config';
|
||||
import { InternalReporter } from '../reporters/internalReporter';
|
||||
import { createReporterForTestServer, createReporters } from './reporters';
|
||||
import { TestRun, createTaskRunnerForList, createTaskRunnerForTestServer, createTaskRunnerForWatchSetup, createTaskRunnerForListFiles } from './tasks';
|
||||
import { open } from 'playwright-core/lib/utilsBundle';
|
||||
import ListReporter from '../reporters/list';
|
||||
import { Multiplexer } from '../reporters/multiplexer';
|
||||
import { SigIntWatcher } from './sigIntWatcher';
|
||||
import { Watcher } from '../fsWatcher';
|
||||
import type { ReportEntry, TestServerInterface, TestServerInterfaceEventEmitters } from '../isomorphic/testServerInterface';
|
||||
|
|
@ -40,6 +38,7 @@ import type { TestRunnerPluginRegistration } from '../plugins';
|
|||
import { serializeError } from '../util';
|
||||
import { cacheDir } from '../transform/compilationCache';
|
||||
import { baseFullConfig } from '../isomorphic/teleReceiver';
|
||||
import { InternalReporter } from '../reporters/internalReporter';
|
||||
|
||||
const originalStdoutWrite = process.stdout.write;
|
||||
const originalStderrWrite = process.stderr.write;
|
||||
|
|
@ -102,9 +101,13 @@ class TestServerDispatcher implements TestServerInterface {
|
|||
|
||||
private async _collectingReporter() {
|
||||
const report: ReportEntry[] = [];
|
||||
const wireReporter = await createReporterForTestServer(this._serializer, e => report.push(e));
|
||||
const reporter = new InternalReporter(wireReporter);
|
||||
return { reporter, report };
|
||||
const collectingReporter = await createReporterForTestServer(this._serializer, e => report.push(e));
|
||||
return { collectingReporter, report };
|
||||
}
|
||||
|
||||
private async _collectingInternalReporter() {
|
||||
const { collectingReporter, report } = await this._collectingReporter();
|
||||
return { reporter: new InternalReporter(collectingReporter), report };
|
||||
}
|
||||
|
||||
async initialize(params: Parameters<TestServerInterface['initialize']>[0]): ReturnType<TestServerInterface['initialize']> {
|
||||
|
|
@ -145,9 +148,9 @@ class TestServerDispatcher implements TestServerInterface {
|
|||
async runGlobalSetup(params: Parameters<TestServerInterface['runGlobalSetup']>[0]): ReturnType<TestServerInterface['runGlobalSetup']> {
|
||||
await this.runGlobalTeardown();
|
||||
|
||||
const { reporter, report } = await this._collectingReporter();
|
||||
const { config, error } = await this._loadConfig();
|
||||
if (!config) {
|
||||
const { reporter, report } = await this._collectingInternalReporter();
|
||||
// Produce dummy config when it has an error.
|
||||
reporter.onConfigure(baseFullConfig);
|
||||
reporter.onError(error!);
|
||||
|
|
@ -156,13 +159,14 @@ class TestServerDispatcher implements TestServerInterface {
|
|||
}
|
||||
|
||||
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
|
||||
const listReporter = new InternalReporter(new ListReporter());
|
||||
const taskRunner = createTaskRunnerForWatchSetup(config, new Multiplexer([reporter, listReporter]));
|
||||
reporter.onConfigure(config.config);
|
||||
const testRun = new TestRun(config, reporter);
|
||||
const { collectingReporter, report } = await this._collectingReporter();
|
||||
const listReporter = new ListReporter();
|
||||
const taskRunner = createTaskRunnerForWatchSetup(config, [collectingReporter, listReporter]);
|
||||
taskRunner.reporter.onConfigure(config.config);
|
||||
const testRun = new TestRun(config);
|
||||
const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(testRun, 0);
|
||||
await reporter.onEnd({ status });
|
||||
await reporter.onExit();
|
||||
await taskRunner.reporter.onEnd({ status });
|
||||
await taskRunner.reporter.onExit();
|
||||
if (status !== 'passed') {
|
||||
await globalCleanup();
|
||||
return { report, status };
|
||||
|
|
@ -181,7 +185,7 @@ class TestServerDispatcher implements TestServerInterface {
|
|||
async startDevServer(params: Parameters<TestServerInterface['startDevServer']>[0]): ReturnType<TestServerInterface['startDevServer']> {
|
||||
if (this._devServerHandle)
|
||||
return { status: 'failed', report: [] };
|
||||
const { reporter, report } = await this._collectingReporter();
|
||||
const { reporter, report } = await this._collectingInternalReporter();
|
||||
const { config, error } = await this._loadConfig();
|
||||
if (!config) {
|
||||
reporter.onError(error!);
|
||||
|
|
@ -209,7 +213,7 @@ class TestServerDispatcher implements TestServerInterface {
|
|||
this._devServerHandle = undefined;
|
||||
return { status: 'passed', report: [] };
|
||||
} catch (e) {
|
||||
const { reporter, report } = await this._collectingReporter();
|
||||
const { reporter, report } = await this._collectingInternalReporter();
|
||||
reporter.onError(serializeError(e));
|
||||
return { status: 'failed', report };
|
||||
}
|
||||
|
|
@ -222,20 +226,21 @@ class TestServerDispatcher implements TestServerInterface {
|
|||
}
|
||||
|
||||
async listFiles(params: Parameters<TestServerInterface['listFiles']>[0]): ReturnType<TestServerInterface['listFiles']> {
|
||||
const { reporter, report } = await this._collectingReporter();
|
||||
const { config, error } = await this._loadConfig();
|
||||
if (!config) {
|
||||
const { reporter, report } = await this._collectingInternalReporter();
|
||||
reporter.onError(error!);
|
||||
return { status: 'failed', report };
|
||||
}
|
||||
|
||||
const { collectingReporter, report } = await this._collectingReporter();
|
||||
config.cliProjectFilter = params.projects?.length ? params.projects : undefined;
|
||||
const taskRunner = createTaskRunnerForListFiles(config, reporter);
|
||||
reporter.onConfigure(config.config);
|
||||
const testRun = new TestRun(config, reporter);
|
||||
const taskRunner = createTaskRunnerForListFiles(config, [collectingReporter]);
|
||||
taskRunner.reporter.onConfigure(config.config);
|
||||
const testRun = new TestRun(config);
|
||||
const status = await taskRunner.run(testRun, 0);
|
||||
await reporter.onEnd({ status });
|
||||
await reporter.onExit();
|
||||
await taskRunner.reporter.onEnd({ status });
|
||||
await taskRunner.reporter.onExit();
|
||||
return { report, status };
|
||||
}
|
||||
|
||||
|
|
@ -253,23 +258,26 @@ class TestServerDispatcher implements TestServerInterface {
|
|||
repeatEach: 1,
|
||||
retries: 0,
|
||||
};
|
||||
const { reporter, report } = await this._collectingReporter();
|
||||
const { config, error } = await this._loadConfig(overrides);
|
||||
if (!config) {
|
||||
const { reporter, report } = await this._collectingInternalReporter();
|
||||
reporter.onError(error!);
|
||||
return { report: [], status: 'failed' };
|
||||
return { report, status: 'failed' };
|
||||
}
|
||||
|
||||
config.cliArgs = params.locations || [];
|
||||
config.cliGrep = params.grep;
|
||||
config.cliGrepInvert = params.grepInvert;
|
||||
config.cliProjectFilter = params.projects?.length ? params.projects : undefined;
|
||||
config.cliListOnly = true;
|
||||
|
||||
const taskRunner = createTaskRunnerForList(config, reporter, 'out-of-process', { failOnLoadErrors: false });
|
||||
const testRun = new TestRun(config, reporter);
|
||||
reporter.onConfigure(config.config);
|
||||
const { collectingReporter, report } = await this._collectingReporter();
|
||||
const taskRunner = createTaskRunnerForList(config, [collectingReporter], 'out-of-process', { failOnLoadErrors: false });
|
||||
const testRun = new TestRun(config);
|
||||
taskRunner.reporter.onConfigure(config.config);
|
||||
const status = await taskRunner.run(testRun, 0);
|
||||
await reporter.onEnd({ status });
|
||||
await reporter.onExit();
|
||||
await taskRunner.reporter.onEnd({ status });
|
||||
await taskRunner.reporter.onExit();
|
||||
|
||||
const projectDirs = new Set<string>();
|
||||
const projectOutputs = new Set<string>();
|
||||
|
|
@ -341,14 +349,13 @@ class TestServerDispatcher implements TestServerInterface {
|
|||
const reporters = await createReporters(config, 'test', true);
|
||||
const wireReporter = await this._wireReporter(e => this._dispatchEvent('report', e));
|
||||
reporters.push(wireReporter);
|
||||
const reporter = new InternalReporter(new Multiplexer(reporters));
|
||||
const taskRunner = createTaskRunnerForTestServer(config, reporter);
|
||||
const testRun = new TestRun(config, reporter);
|
||||
reporter.onConfigure(config.config);
|
||||
const taskRunner = createTaskRunnerForTestServer(config, reporters);
|
||||
const testRun = new TestRun(config);
|
||||
taskRunner.reporter.onConfigure(config.config);
|
||||
const stop = new ManualPromise();
|
||||
const run = taskRunner.run(testRun, 0, stop).then(async status => {
|
||||
await reporter.onEnd({ status });
|
||||
await reporter.onExit();
|
||||
await taskRunner.reporter.onEnd({ status });
|
||||
await taskRunner.reporter.onExit();
|
||||
this._testRun = undefined;
|
||||
return status;
|
||||
});
|
||||
|
|
|
|||
45
packages/playwright/src/runner/vcs.ts
Normal file
45
packages/playwright/src/runner/vcs.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* Copyright Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* 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 childProcess from 'child_process';
|
||||
import { affectedTestFiles } from '../transform/compilationCache';
|
||||
import path from 'path';
|
||||
|
||||
export async function detectChangedTests(baseCommit: string, configDir: string): Promise<Set<string>> {
|
||||
function gitFileList(command: string) {
|
||||
try {
|
||||
return childProcess.execSync(
|
||||
`git ${command}`,
|
||||
{ encoding: 'utf-8', stdio: 'pipe', cwd: configDir }
|
||||
).split('\n').filter(Boolean);
|
||||
} catch (_error) {
|
||||
const error = _error as childProcess.SpawnSyncReturns<string>;
|
||||
throw new Error([
|
||||
`Cannot detect changed files for --only-changed mode:`,
|
||||
`git ${command}`,
|
||||
'',
|
||||
...error.output,
|
||||
].join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
const untrackedFiles = gitFileList(`ls-files --others --exclude-standard`).map(file => path.join(configDir, file));
|
||||
|
||||
const [gitRoot] = gitFileList('rev-parse --show-toplevel');
|
||||
const trackedFilesWithChanges = gitFileList(`diff ${baseCommit} --name-only`).map(file => path.join(gitRoot, file));
|
||||
|
||||
return new Set(affectedTestFiles([...untrackedFiles, ...trackedFilesWithChanges]));
|
||||
}
|
||||
|
|
@ -17,7 +17,6 @@
|
|||
import readline from 'readline';
|
||||
import { createGuid, getPackageManagerExecCommand, ManualPromise } from 'playwright-core/lib/utils';
|
||||
import type { FullConfigInternal, FullProjectInternal } from '../common/config';
|
||||
import { InternalReporter } from '../reporters/internalReporter';
|
||||
import { createFileMatcher, createFileMatcherFromArguments } from '../util';
|
||||
import type { Matcher } from '../util';
|
||||
import { TestRun, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
|
||||
|
|
@ -112,15 +111,14 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise<Full
|
|||
p.project.retries = 0;
|
||||
|
||||
// Perform global setup.
|
||||
const reporter = new InternalReporter(new ListReporter());
|
||||
const testRun = new TestRun(config, reporter);
|
||||
const taskRunner = createTaskRunnerForWatchSetup(config, reporter);
|
||||
reporter.onConfigure(config.config);
|
||||
const testRun = new TestRun(config);
|
||||
const taskRunner = createTaskRunnerForWatchSetup(config, [new ListReporter()]);
|
||||
taskRunner.reporter.onConfigure(config.config);
|
||||
const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(testRun, 0);
|
||||
if (status !== 'passed')
|
||||
await globalCleanup();
|
||||
await reporter.onEnd({ status });
|
||||
await reporter.onExit();
|
||||
await taskRunner.reporter.onEnd({ status });
|
||||
await taskRunner.reporter.onExit();
|
||||
if (status !== 'passed')
|
||||
return status;
|
||||
|
||||
|
|
@ -280,10 +278,9 @@ async function runTests(config: FullConfigInternal, failedTestIdCollector: Set<s
|
|||
title?: string,
|
||||
}) {
|
||||
printConfiguration(config, options?.title);
|
||||
const reporter = new InternalReporter(new ListReporter());
|
||||
const taskRunner = createTaskRunnerForWatch(config, reporter, options?.additionalFileMatcher);
|
||||
const testRun = new TestRun(config, reporter);
|
||||
reporter.onConfigure(config.config);
|
||||
const taskRunner = createTaskRunnerForWatch(config, [new ListReporter()], options?.additionalFileMatcher);
|
||||
const testRun = new TestRun(config);
|
||||
taskRunner.reporter.onConfigure(config.config);
|
||||
const taskStatus = await taskRunner.run(testRun, 0);
|
||||
let status: FullResult['status'] = 'passed';
|
||||
|
||||
|
|
@ -301,8 +298,8 @@ async function runTests(config: FullConfigInternal, failedTestIdCollector: Set<s
|
|||
status = 'failed';
|
||||
if (status === 'passed' && taskStatus !== 'passed')
|
||||
status = taskStatus;
|
||||
await reporter.onEnd({ status });
|
||||
await reporter.onExit();
|
||||
await taskRunner.reporter.onEnd({ status });
|
||||
await taskRunner.reporter.onExit();
|
||||
}
|
||||
|
||||
function affectedProjectsClosure(projectClosure: FullProjectInternal[], affected: FullProjectInternal[]): Set<FullProjectInternal> {
|
||||
|
|
|
|||
32
packages/playwright/types/test.d.ts
vendored
32
packages/playwright/types/test.d.ts
vendored
|
|
@ -5202,10 +5202,14 @@ export interface PlaywrightTestOptions {
|
|||
*/
|
||||
colorScheme: ColorScheme;
|
||||
/**
|
||||
* TLS Client Authentication allows the server to request a client certificate and verify it.
|
||||
*
|
||||
* **Details**
|
||||
*
|
||||
* An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a
|
||||
* single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the
|
||||
* private key is encrypted. If the certificate is valid only for specific URLs, the `url` property should be provided
|
||||
* with a glob pattern to match the URLs that the certificate is valid for.
|
||||
* certficiate is encrypted. If the certificate is valid only for specific origins, the `origin` property should be
|
||||
* provided with a glob pattern to match the origins that the certificate is valid for.
|
||||
*
|
||||
* **NOTE** Using Client Certificates in combination with Proxy Servers is not supported.
|
||||
*
|
||||
|
|
@ -5219,22 +5223,14 @@ export interface PlaywrightTestOptions {
|
|||
* import { defineConfig } from '@playwright/test';
|
||||
*
|
||||
* export default defineConfig({
|
||||
* projects: [
|
||||
* {
|
||||
* name: 'Microsoft Edge',
|
||||
* use: {
|
||||
* ...devices['Desktop Edge'],
|
||||
* clientCertificates: [{
|
||||
* url: 'https://example.com/**',
|
||||
* certs: [{
|
||||
* certPath: './cert.pem',
|
||||
* keyPath: './key.pem',
|
||||
* passphrase: 'mysecretpassword',
|
||||
* }],
|
||||
* }],
|
||||
* },
|
||||
* },
|
||||
* ]
|
||||
* use: {
|
||||
* clientCertificates: [{
|
||||
* origin: 'https://example.com',
|
||||
* certPath: './cert.pem',
|
||||
* keyPath: './key.pem',
|
||||
* passphrase: 'mysecretpassword',
|
||||
* }],
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
|
|
|
|||
|
|
@ -582,13 +582,11 @@ export type PlaywrightNewRequestParams = {
|
|||
ignoreHTTPSErrors?: boolean,
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
clientCertificates?: {
|
||||
url: string,
|
||||
certs: {
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
origin: string,
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
|
|
@ -615,13 +613,11 @@ export type PlaywrightNewRequestOptions = {
|
|||
ignoreHTTPSErrors?: boolean,
|
||||
extraHTTPHeaders?: NameValue[],
|
||||
clientCertificates?: {
|
||||
url: string,
|
||||
certs: {
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
origin: string,
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
httpCredentials?: {
|
||||
username: string,
|
||||
|
|
@ -968,13 +964,11 @@ export type BrowserTypeLaunchPersistentContextParams = {
|
|||
},
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
clientCertificates?: {
|
||||
url: string,
|
||||
certs: {
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
origin: string,
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
javaScriptEnabled?: boolean,
|
||||
bypassCSP?: boolean,
|
||||
|
|
@ -1050,13 +1044,11 @@ export type BrowserTypeLaunchPersistentContextOptions = {
|
|||
},
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
clientCertificates?: {
|
||||
url: string,
|
||||
certs: {
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
origin: string,
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
javaScriptEnabled?: boolean,
|
||||
bypassCSP?: boolean,
|
||||
|
|
@ -1167,13 +1159,11 @@ export type BrowserNewContextParams = {
|
|||
},
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
clientCertificates?: {
|
||||
url: string,
|
||||
certs: {
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
origin: string,
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
javaScriptEnabled?: boolean,
|
||||
bypassCSP?: boolean,
|
||||
|
|
@ -1235,13 +1225,11 @@ export type BrowserNewContextOptions = {
|
|||
},
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
clientCertificates?: {
|
||||
url: string,
|
||||
certs: {
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
origin: string,
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
javaScriptEnabled?: boolean,
|
||||
bypassCSP?: boolean,
|
||||
|
|
@ -1306,13 +1294,11 @@ export type BrowserNewContextForReuseParams = {
|
|||
},
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
clientCertificates?: {
|
||||
url: string,
|
||||
certs: {
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
origin: string,
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
javaScriptEnabled?: boolean,
|
||||
bypassCSP?: boolean,
|
||||
|
|
@ -1374,13 +1360,11 @@ export type BrowserNewContextForReuseOptions = {
|
|||
},
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
clientCertificates?: {
|
||||
url: string,
|
||||
certs: {
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
origin: string,
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
javaScriptEnabled?: boolean,
|
||||
bypassCSP?: boolean,
|
||||
|
|
@ -1967,7 +1951,6 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel {
|
|||
mouseClick(params: PageMouseClickParams, metadata?: CallMetadata): Promise<PageMouseClickResult>;
|
||||
mouseWheel(params: PageMouseWheelParams, metadata?: CallMetadata): Promise<PageMouseWheelResult>;
|
||||
touchscreenTap(params: PageTouchscreenTapParams, metadata?: CallMetadata): Promise<PageTouchscreenTapResult>;
|
||||
touchscreenTouch(params: PageTouchscreenTouchParams, metadata?: CallMetadata): Promise<PageTouchscreenTouchResult>;
|
||||
accessibilitySnapshot(params: PageAccessibilitySnapshotParams, metadata?: CallMetadata): Promise<PageAccessibilitySnapshotResult>;
|
||||
pdf(params: PagePdfParams, metadata?: CallMetadata): Promise<PagePdfResult>;
|
||||
startJSCoverage(params: PageStartJSCoverageParams, metadata?: CallMetadata): Promise<PageStartJSCoverageResult>;
|
||||
|
|
@ -2335,18 +2318,6 @@ export type PageTouchscreenTapOptions = {
|
|||
|
||||
};
|
||||
export type PageTouchscreenTapResult = void;
|
||||
export type PageTouchscreenTouchParams = {
|
||||
type: 'touchstart' | 'touchend' | 'touchmove' | 'touchcancel',
|
||||
touchPoints: {
|
||||
x: number,
|
||||
y: number,
|
||||
id?: number,
|
||||
}[],
|
||||
};
|
||||
export type PageTouchscreenTouchOptions = {
|
||||
|
||||
};
|
||||
export type PageTouchscreenTouchResult = void;
|
||||
export type PageAccessibilitySnapshotParams = {
|
||||
interestingOnly?: boolean,
|
||||
root?: ElementHandleChannel,
|
||||
|
|
@ -4595,13 +4566,11 @@ export type AndroidDeviceLaunchBrowserParams = {
|
|||
},
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
clientCertificates?: {
|
||||
url: string,
|
||||
certs: {
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
origin: string,
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
javaScriptEnabled?: boolean,
|
||||
bypassCSP?: boolean,
|
||||
|
|
@ -4661,13 +4630,11 @@ export type AndroidDeviceLaunchBrowserOptions = {
|
|||
},
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
clientCertificates?: {
|
||||
url: string,
|
||||
certs: {
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
origin: string,
|
||||
cert?: Binary,
|
||||
key?: Binary,
|
||||
passphrase?: string,
|
||||
pfx?: Binary,
|
||||
}[],
|
||||
javaScriptEnabled?: boolean,
|
||||
bypassCSP?: boolean,
|
||||
|
|
|
|||
|
|
@ -445,16 +445,11 @@ ContextOptions:
|
|||
items:
|
||||
type: object
|
||||
properties:
|
||||
url: string
|
||||
certs:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
cert: binary?
|
||||
key: binary?
|
||||
passphrase: string?
|
||||
pfx: binary?
|
||||
origin: string
|
||||
cert: binary?
|
||||
key: binary?
|
||||
passphrase: string?
|
||||
pfx: binary?
|
||||
javaScriptEnabled: boolean?
|
||||
bypassCSP: boolean?
|
||||
userAgent: string?
|
||||
|
|
@ -700,16 +695,11 @@ Playwright:
|
|||
items:
|
||||
type: object
|
||||
properties:
|
||||
url: string
|
||||
certs:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
cert: binary?
|
||||
key: binary?
|
||||
passphrase: string?
|
||||
pfx: binary?
|
||||
origin: string
|
||||
cert: binary?
|
||||
key: binary?
|
||||
passphrase: string?
|
||||
pfx: binary?
|
||||
httpCredentials:
|
||||
type: object?
|
||||
properties:
|
||||
|
|
@ -1640,27 +1630,6 @@ Page:
|
|||
slowMo: true
|
||||
snapshot: true
|
||||
|
||||
touchscreenTouch:
|
||||
parameters:
|
||||
type:
|
||||
type: enum
|
||||
literals:
|
||||
- touchstart
|
||||
- touchend
|
||||
- touchmove
|
||||
- touchcancel
|
||||
touchPoints:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
x: number
|
||||
y: number
|
||||
id: number?
|
||||
flags:
|
||||
slowMo: true
|
||||
snapshot: true
|
||||
|
||||
accessibilitySnapshot:
|
||||
parameters:
|
||||
interestingOnly: boolean?
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ export const UIModeView: React.FC<{}> = ({
|
|||
if (status !== 'passed')
|
||||
return;
|
||||
|
||||
const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args });
|
||||
const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert });
|
||||
teleSuiteUpdater.processListReport(result.report);
|
||||
|
||||
testServerConnection.onReport(params => {
|
||||
|
|
@ -199,7 +199,7 @@ export const UIModeView: React.FC<{}> = ({
|
|||
commandQueue.current = commandQueue.current.then(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args });
|
||||
const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert });
|
||||
teleSuiteUpdater.processListReport(result.report);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
|
|
|||
|
|
@ -26,14 +26,14 @@ test('should load font with routes', async ({ mount, page }) => {
|
|||
});
|
||||
|
||||
test.describe('request handlers', () => {
|
||||
test('should handle requests', async ({ page, mount, route }) => {
|
||||
test('should handle requests', async ({ page, mount, router }) => {
|
||||
let respond: (() => void) = () => {};
|
||||
const promise = new Promise<void>(f => respond = f);
|
||||
|
||||
let postReceived: ((body: string) => void) = () => {};
|
||||
const postBody = new Promise<string>(f => postReceived = f);
|
||||
|
||||
await route([
|
||||
await router.use(
|
||||
http.get('/data.json', async () => {
|
||||
await promise;
|
||||
return HttpResponse.json({ name: 'John Doe' });
|
||||
|
|
@ -42,7 +42,7 @@ test.describe('request handlers', () => {
|
|||
postReceived(await request.text());
|
||||
return HttpResponse.text('ok');
|
||||
}),
|
||||
]);
|
||||
);
|
||||
|
||||
const component = await mount(<Fetcher />);
|
||||
await expect(component.getByTestId('name')).toHaveText('<none>');
|
||||
|
|
@ -54,15 +54,15 @@ test.describe('request handlers', () => {
|
|||
expect(await postBody).toBe('hello from the page');
|
||||
});
|
||||
|
||||
test('should add dynamically', async ({ page, mount, route }) => {
|
||||
await route('**/data.json', async route => {
|
||||
test('should add dynamically', async ({ page, mount, router }) => {
|
||||
await router.route('**/data.json', async route => {
|
||||
await route.fulfill({ body: JSON.stringify({ name: '<original>' }) });
|
||||
});
|
||||
|
||||
const component = await mount(<Fetcher />);
|
||||
await expect(component.getByTestId('name')).toHaveText('<original>');
|
||||
|
||||
await route(
|
||||
await router.use(
|
||||
http.get('/data.json', async () => {
|
||||
return HttpResponse.json({ name: 'John Doe' });
|
||||
}),
|
||||
|
|
@ -72,12 +72,12 @@ test.describe('request handlers', () => {
|
|||
await expect(component.getByTestId('name')).toHaveText('John Doe');
|
||||
});
|
||||
|
||||
test('should passthrough', async ({ page, mount, route }) => {
|
||||
await route('**/data.json', async route => {
|
||||
test('should passthrough', async ({ page, mount, router }) => {
|
||||
await router.route('**/data.json', async route => {
|
||||
await route.fulfill({ body: JSON.stringify({ name: '<original>' }) });
|
||||
});
|
||||
|
||||
await route(
|
||||
await router.use(
|
||||
http.get('/data.json', async () => {
|
||||
return passthrough();
|
||||
}),
|
||||
|
|
@ -87,13 +87,13 @@ test.describe('request handlers', () => {
|
|||
await expect(component.getByTestId('name')).toHaveText('<error>');
|
||||
});
|
||||
|
||||
test('should fallback when nothing is returned', async ({ page, mount, route }) => {
|
||||
await route('**/data.json', async route => {
|
||||
test('should fallback when nothing is returned', async ({ page, mount, router }) => {
|
||||
await router.route('**/data.json', async route => {
|
||||
await route.fulfill({ body: JSON.stringify({ name: '<original>' }) });
|
||||
});
|
||||
|
||||
let called = false;
|
||||
await route(
|
||||
await router.use(
|
||||
http.get('/data.json', async () => {
|
||||
called = true;
|
||||
}),
|
||||
|
|
@ -104,12 +104,12 @@ test.describe('request handlers', () => {
|
|||
expect(called).toBe(true);
|
||||
});
|
||||
|
||||
test('should bypass(request)', async ({ page, mount, route }) => {
|
||||
await route('**/data.json', async route => {
|
||||
test('should bypass(request)', async ({ page, mount, router }) => {
|
||||
await router.route('**/data.json', async route => {
|
||||
await route.fulfill({ body: JSON.stringify({ name: `<original>` }) });
|
||||
});
|
||||
|
||||
await route(
|
||||
await router.use(
|
||||
http.get('/data.json', async ({ request }) => {
|
||||
return await fetch(bypass(request));
|
||||
}),
|
||||
|
|
@ -119,7 +119,7 @@ test.describe('request handlers', () => {
|
|||
await expect(component.getByTestId('name')).toHaveText('<error>');
|
||||
});
|
||||
|
||||
test('should bypass(url) and get cookies', async ({ page, mount, route, browserName }) => {
|
||||
test('should bypass(url) and get cookies', async ({ page, mount, router, browserName }) => {
|
||||
let cookie = '';
|
||||
const server = new httpServer.Server();
|
||||
server.on('request', (req, res) => {
|
||||
|
|
@ -129,7 +129,7 @@ test.describe('request handlers', () => {
|
|||
await new Promise<void>(f => server.listen(0, f));
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
await route('**/data.json', async route => {
|
||||
await router.route('**/data.json', async route => {
|
||||
await route.fulfill({ body: JSON.stringify({ name: `<original>` }) });
|
||||
});
|
||||
|
||||
|
|
@ -137,7 +137,7 @@ test.describe('request handlers', () => {
|
|||
await expect(component.getByTestId('name')).toHaveText('<original>');
|
||||
|
||||
await page.evaluate(() => document.cookie = 'foo=bar');
|
||||
await route(
|
||||
await router.use(
|
||||
http.get('/data.json', async ({ request }) => {
|
||||
if (browserName !== 'webkit') {
|
||||
// WebKit does not have cookies while intercepting.
|
||||
|
|
@ -153,12 +153,12 @@ test.describe('request handlers', () => {
|
|||
await new Promise(f => server.close(f));
|
||||
});
|
||||
|
||||
test('should ignore navigation requests', async ({ page, mount, route }) => {
|
||||
await route('**/newpage', async route => {
|
||||
test('should ignore navigation requests', async ({ page, mount, router }) => {
|
||||
await router.route('**/newpage', async route => {
|
||||
await route.fulfill({ body: `<div>original</div>`, contentType: 'text/html' });
|
||||
});
|
||||
|
||||
await route(
|
||||
await router.use(
|
||||
http.get('/newpage', async ({ request }) => {
|
||||
return new Response(`<div>intercepted</div>`, {
|
||||
headers: new Headers({ 'Content-Type': 'text/html' }),
|
||||
|
|
@ -171,11 +171,8 @@ test.describe('request handlers', () => {
|
|||
await expect(page.locator('div')).toHaveText('original');
|
||||
});
|
||||
|
||||
test('should throw when calling fetch(bypass) outside of a handler', async ({ page, route, baseURL }) => {
|
||||
await route(
|
||||
http.get('/data.json', async () => {
|
||||
}),
|
||||
);
|
||||
test('should throw when calling fetch(bypass) outside of a handler', async ({ page, router, baseURL }) => {
|
||||
await router.use(http.get('/data.json', async () => {}));
|
||||
|
||||
const error = await fetch(bypass(baseURL + '/hello')).catch(e => e);
|
||||
expect(error.message).toContain(`Cannot call fetch(bypass()) outside of a request handler`);
|
||||
|
|
|
|||
98
tests/library/browsercontext-fetch-algorithms.spec.ts
Normal file
98
tests/library/browsercontext-fetch-algorithms.spec.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* Copyright (c) 2014-present Matt Zabriskie
|
||||
* Modifications copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { contextTest as it, expect } from '../config/browserTest';
|
||||
import util from 'util';
|
||||
import zlib from 'zlib';
|
||||
|
||||
const gzip = util.promisify(zlib.gzip);
|
||||
const deflate = util.promisify(zlib.deflate);
|
||||
const brotliCompress = util.promisify(zlib.brotliCompress);
|
||||
|
||||
it.skip(({ mode }) => mode !== 'default');
|
||||
|
||||
it.describe('algorithms', () => {
|
||||
const responseBody = 'str';
|
||||
|
||||
for (const [type, zipped] of Object.entries({
|
||||
gzip: gzip(responseBody),
|
||||
deflate: deflate(responseBody),
|
||||
br: brotliCompress(responseBody)
|
||||
})) {
|
||||
it.describe(`${type} decompression`, () => {
|
||||
it(`should support decompression`, async ({ context, server }) => {
|
||||
server.setRoute('/compressed', async (req, res) => {
|
||||
res.setHeader('Content-Encoding', type);
|
||||
res.end(await zipped);
|
||||
});
|
||||
|
||||
const response = await context.request.get(server.PREFIX + '/compressed');
|
||||
expect(await response.text()).toEqual(responseBody);
|
||||
});
|
||||
|
||||
it(`should not fail if response content-length header is missing (${type})`, async ({ context, server }) => {
|
||||
server.setRoute('/compressed', async (req, res) => {
|
||||
res.setHeader('Content-Encoding', type);
|
||||
res.removeHeader('Content-Length');
|
||||
res.end(await zipped);
|
||||
});
|
||||
|
||||
const response = await context.request.get(server.PREFIX + '/compressed');
|
||||
expect(await response.text()).toEqual(responseBody);
|
||||
});
|
||||
|
||||
it('should not fail with chunked responses (without Content-Length header)', async ({ context, server }) => {
|
||||
server.setRoute('/compressed', async (req, res) => {
|
||||
res.setHeader('Content-Encoding', type);
|
||||
res.setHeader('Transfer-Encoding', 'chunked');
|
||||
res.removeHeader('Content-Length');
|
||||
res.write(await zipped);
|
||||
res.end();
|
||||
});
|
||||
|
||||
const response = await context.request.get(server.PREFIX + '/compressed');
|
||||
expect(await response.text()).toEqual(responseBody);
|
||||
});
|
||||
|
||||
it('should not fail with an empty response without content-length header (Z_BUF_ERROR)', async ({ context, server }) => {
|
||||
server.setRoute('/compressed', async (req, res) => {
|
||||
res.setHeader('Content-Encoding', type);
|
||||
res.removeHeader('Content-Length');
|
||||
res.end();
|
||||
});
|
||||
|
||||
const response = await context.request.get(server.PREFIX + '/compressed');
|
||||
expect(await response.text()).toEqual('');
|
||||
});
|
||||
|
||||
it('should not fail with an empty response with content-length header (Z_BUF_ERROR)', async ({ context, server }) => {
|
||||
server.setRoute('/compressed', async (req, res) => {
|
||||
res.setHeader('Content-Encoding', type);
|
||||
res.end();
|
||||
});
|
||||
|
||||
await context.request.get(server.PREFIX + '/compressed');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -19,8 +19,7 @@ import url from 'url';
|
|||
import { contextTest as it, expect } from '../config/browserTest';
|
||||
import { hostPlatform } from '../../packages/playwright-core/src/utils/hostPlatform';
|
||||
|
||||
it('SharedArrayBuffer should work @smoke', async function({ contextFactory, httpsServer, browserName }) {
|
||||
it.fail(browserName === 'webkit', 'no shared array buffer on webkit');
|
||||
it('SharedArrayBuffer should work @smoke', async function({ contextFactory, httpsServer }) {
|
||||
const context = await contextFactory({ ignoreHTTPSErrors: true });
|
||||
const page = await context.newPage();
|
||||
httpsServer.setRoute('/sharedarraybuffer', (req, res) => {
|
||||
|
|
|
|||
|
|
@ -15,48 +15,58 @@
|
|||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import http2 from 'http2';
|
||||
import type http from 'http';
|
||||
import { expect, playwrightTest as base } from '../config/browserTest';
|
||||
import type net from 'net';
|
||||
import type { BrowserContextOptions } from 'packages/playwright-test';
|
||||
const { createHttpsServer } = require('../../packages/playwright-core/lib/utils');
|
||||
|
||||
const test = base.extend<{ serverURL: string, serverURLRewrittenToLocalhost: string }>({
|
||||
serverURL: async ({ asset }, use) => {
|
||||
const server = createHttpsServer({
|
||||
key: fs.readFileSync(asset('client-certificates/server/server_key.pem')),
|
||||
cert: fs.readFileSync(asset('client-certificates/server/server_cert.pem')),
|
||||
ca: [
|
||||
fs.readFileSync(asset('client-certificates/server/server_cert.pem')),
|
||||
],
|
||||
requestCert: true,
|
||||
rejectUnauthorized: false,
|
||||
}, (req, res) => {
|
||||
const tlsSocket = req.socket as import('tls').TLSSocket;
|
||||
// @ts-expect-error
|
||||
expect(['localhost', 'local.playwright'].includes((tlsSocket).servername)).toBe(true);
|
||||
const cert = tlsSocket.getPeerCertificate();
|
||||
if ((req as any).client.authorized) {
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(`Hello ${cert.subject.CN}, your certificate was issued by ${cert.issuer.CN}!`);
|
||||
} else if (cert.subject) {
|
||||
res.writeHead(403, { 'Content-Type': 'text/html' });
|
||||
res.end(`Sorry ${cert.subject.CN}, certificates from ${cert.issuer.CN} are not welcome here.`);
|
||||
} else {
|
||||
res.writeHead(401, { 'Content-Type': 'text/html' });
|
||||
res.end(`Sorry, but you need to provide a client certificate to continue.`);
|
||||
}
|
||||
});
|
||||
type TestOptions = {
|
||||
startCCServer(options?: {
|
||||
http2?: boolean;
|
||||
useFakeLocalhost?: boolean;
|
||||
}): Promise<string>,
|
||||
};
|
||||
|
||||
const test = base.extend<TestOptions>({
|
||||
startCCServer: async ({ asset, browserName }, use) => {
|
||||
process.env.PWTEST_UNSUPPORTED_CUSTOM_CA = asset('client-certificates/server/server_cert.pem');
|
||||
await new Promise<void>(f => server.listen(0, 'localhost', () => f()));
|
||||
await use(`https://localhost:${(server.address() as net.AddressInfo).port}/`);
|
||||
await new Promise<void>(resolve => server.close(() => resolve()));
|
||||
let server: http.Server | http2.Http2Server | undefined;
|
||||
await use(async options => {
|
||||
server = (options?.http2 ? http2.createSecureServer : createHttpsServer)({
|
||||
key: fs.readFileSync(asset('client-certificates/server/server_key.pem')),
|
||||
cert: fs.readFileSync(asset('client-certificates/server/server_cert.pem')),
|
||||
ca: [
|
||||
fs.readFileSync(asset('client-certificates/server/server_cert.pem')),
|
||||
],
|
||||
requestCert: true,
|
||||
rejectUnauthorized: false,
|
||||
allowHTTP1: true,
|
||||
}, (req: (http2.Http2ServerRequest | http.IncomingMessage), res: http2.Http2ServerResponse | http.ServerResponse) => {
|
||||
const tlsSocket = req.socket as import('tls').TLSSocket;
|
||||
// @ts-expect-error https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/62336
|
||||
expect(['localhost', 'local.playwright'].includes((tlsSocket).servername)).toBe(true);
|
||||
const prefix = `ALPN protocol: ${tlsSocket.alpnProtocol}\n`;
|
||||
const cert = tlsSocket.getPeerCertificate();
|
||||
if (tlsSocket.authorized) {
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(prefix + `Hello ${cert.subject.CN}, your certificate was issued by ${cert.issuer.CN}!`);
|
||||
} else if (cert.subject) {
|
||||
res.writeHead(403, { 'Content-Type': 'text/html' });
|
||||
res.end(prefix + `Sorry ${cert.subject.CN}, certificates from ${cert.issuer.CN} are not welcome here.`);
|
||||
} else {
|
||||
res.writeHead(401, { 'Content-Type': 'text/html' });
|
||||
res.end(prefix + `Sorry, but you need to provide a client certificate to continue.`);
|
||||
}
|
||||
});
|
||||
await new Promise<void>(f => server.listen(0, 'localhost', () => f()));
|
||||
const host = options?.useFakeLocalhost ? 'local.playwright' : 'localhost';
|
||||
return `https://${host}:${(server.address() as net.AddressInfo).port}/`;
|
||||
});
|
||||
if (server)
|
||||
await new Promise<void>(resolve => server.close(() => resolve()));
|
||||
},
|
||||
serverURLRewrittenToLocalhost: async ({ serverURL, browserName }, use) => {
|
||||
const parsed = new URL(serverURL);
|
||||
parsed.hostname = 'local.playwright';
|
||||
const shouldRewriteToLocalhost = browserName === 'webkit' && process.platform === 'darwin';
|
||||
await use(shouldRewriteToLocalhost ? parsed.toString() : serverURL);
|
||||
}
|
||||
});
|
||||
|
||||
test.use({
|
||||
|
|
@ -72,27 +82,22 @@ test.skip(({ mode }) => mode !== 'default');
|
|||
|
||||
const kDummyFileName = __filename;
|
||||
const kValidationSubTests: [BrowserContextOptions, string][] = [
|
||||
[{ clientCertificates: [{ url: 'test', certs: [] }] }, 'No certs specified for url: test'],
|
||||
[{ clientCertificates: [{ url: 'test', certs: [{}] }] }, 'None of cert, key, passphrase or pfx is specified'],
|
||||
[{ clientCertificates: [{ origin: 'test' }] }, 'None of cert, key, passphrase or pfx is specified'],
|
||||
[{
|
||||
clientCertificates: [{
|
||||
url: 'test',
|
||||
certs: [{
|
||||
certPath: kDummyFileName,
|
||||
keyPath: kDummyFileName,
|
||||
pfxPath: kDummyFileName,
|
||||
passphrase: kDummyFileName,
|
||||
}]
|
||||
origin: 'test',
|
||||
certPath: kDummyFileName,
|
||||
keyPath: kDummyFileName,
|
||||
pfxPath: kDummyFileName,
|
||||
passphrase: kDummyFileName,
|
||||
}]
|
||||
}, 'pfx is specified together with cert, key or passphrase'],
|
||||
[{
|
||||
proxy: { server: 'http://localhost:8080' },
|
||||
clientCertificates: [{
|
||||
url: 'test',
|
||||
certs: [{
|
||||
certPath: kDummyFileName,
|
||||
keyPath: kDummyFileName,
|
||||
}]
|
||||
origin: 'test',
|
||||
certPath: kDummyFileName,
|
||||
keyPath: kDummyFileName,
|
||||
}]
|
||||
}, 'Cannot specify both proxy and clientCertificates'],
|
||||
];
|
||||
|
|
@ -103,22 +108,21 @@ test.describe('fetch', () => {
|
|||
await expect(playwright.request.newContext(contextOptions)).rejects.toThrow(expected);
|
||||
});
|
||||
|
||||
test('should fail with no client certificates provided', async ({ playwright, serverURL }) => {
|
||||
test('should fail with no client certificates provided', async ({ playwright, startCCServer }) => {
|
||||
const serverURL = await startCCServer();
|
||||
const request = await playwright.request.newContext();
|
||||
const response = await request.get(serverURL);
|
||||
expect(response.status()).toBe(401);
|
||||
expect(await response.text()).toBe('Sorry, but you need to provide a client certificate to continue.');
|
||||
expect(await response.text()).toContain('Sorry, but you need to provide a client certificate to continue.');
|
||||
await request.dispose();
|
||||
});
|
||||
|
||||
test('should keep supporting http', async ({ playwright, server, asset }) => {
|
||||
const request = await playwright.request.newContext({
|
||||
clientCertificates: [{
|
||||
url: server.PREFIX,
|
||||
certs: [{
|
||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||
}],
|
||||
origin: new URL(server.PREFIX).origin,
|
||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||
}],
|
||||
});
|
||||
const response = await request.get(server.PREFIX + '/one-style.html');
|
||||
|
|
@ -128,48 +132,45 @@ test.describe('fetch', () => {
|
|||
await request.dispose();
|
||||
});
|
||||
|
||||
test('should throw with untrusted client certs', async ({ playwright, serverURL, asset }) => {
|
||||
test('should throw with untrusted client certs', async ({ playwright, startCCServer, asset }) => {
|
||||
const serverURL = await startCCServer();
|
||||
const request = await playwright.request.newContext({
|
||||
clientCertificates: [{
|
||||
url: serverURL,
|
||||
certs: [{
|
||||
certPath: asset('client-certificates/client/self-signed/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/self-signed/key.pem'),
|
||||
}],
|
||||
origin: new URL(serverURL).origin,
|
||||
certPath: asset('client-certificates/client/self-signed/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/self-signed/key.pem'),
|
||||
}],
|
||||
});
|
||||
const response = await request.get(serverURL);
|
||||
expect(response.url()).toBe(serverURL);
|
||||
expect(response.status()).toBe(403);
|
||||
expect(await response.text()).toBe('Sorry Bob, certificates from Bob are not welcome here.');
|
||||
expect(await response.text()).toContain('Sorry Bob, certificates from Bob are not welcome here.');
|
||||
await request.dispose();
|
||||
});
|
||||
|
||||
test('pass with trusted client certificates', async ({ playwright, serverURL, asset }) => {
|
||||
test('pass with trusted client certificates', async ({ playwright, startCCServer, asset }) => {
|
||||
const serverURL = await startCCServer();
|
||||
const request = await playwright.request.newContext({
|
||||
clientCertificates: [{
|
||||
url: serverURL,
|
||||
certs: [{
|
||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||
}],
|
||||
origin: new URL(serverURL).origin,
|
||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||
}],
|
||||
});
|
||||
const response = await request.get(serverURL);
|
||||
expect(response.url()).toBe(serverURL);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(await response.text()).toBe('Hello Alice, your certificate was issued by localhost!');
|
||||
expect(await response.text()).toContain('Hello Alice, your certificate was issued by localhost!');
|
||||
await request.dispose();
|
||||
});
|
||||
|
||||
test('should work in the browser with request interception', async ({ browser, playwright, serverURL, asset }) => {
|
||||
test('should work in the browser with request interception', async ({ browser, playwright, startCCServer, asset }) => {
|
||||
const serverURL = await startCCServer();
|
||||
const request = await playwright.request.newContext({
|
||||
clientCertificates: [{
|
||||
url: serverURL,
|
||||
certs: [{
|
||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||
}],
|
||||
origin: new URL(serverURL).origin,
|
||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||
}],
|
||||
});
|
||||
const page = await browser.newPage({ ignoreHTTPSErrors: true });
|
||||
|
|
@ -194,11 +195,9 @@ test.describe('browser', () => {
|
|||
test('should keep supporting http', async ({ browser, server, asset }) => {
|
||||
const page = await browser.newPage({
|
||||
clientCertificates: [{
|
||||
url: server.PREFIX,
|
||||
certs: [{
|
||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||
}],
|
||||
origin: new URL(server.PREFIX).origin,
|
||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||
}],
|
||||
});
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
|
|
@ -207,47 +206,44 @@ test.describe('browser', () => {
|
|||
await page.close();
|
||||
});
|
||||
|
||||
test('should fail with no client certificates', async ({ browser, serverURLRewrittenToLocalhost, asset }) => {
|
||||
test('should fail with no client certificates', async ({ browser, startCCServer, asset, browserName }) => {
|
||||
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
||||
const page = await browser.newPage({
|
||||
clientCertificates: [{
|
||||
url: 'https://not-matching.com',
|
||||
certs: [{
|
||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||
}],
|
||||
origin: 'https://not-matching.com',
|
||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||
}],
|
||||
});
|
||||
await page.goto(serverURLRewrittenToLocalhost);
|
||||
await page.goto(serverURL);
|
||||
await expect(page.getByText('Sorry, but you need to provide a client certificate to continue.')).toBeVisible();
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('should fail with self-signed client certificates', async ({ browser, serverURLRewrittenToLocalhost, asset }) => {
|
||||
test('should fail with self-signed client certificates', async ({ browser, startCCServer, asset, browserName }) => {
|
||||
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
||||
const page = await browser.newPage({
|
||||
clientCertificates: [{
|
||||
url: serverURLRewrittenToLocalhost,
|
||||
certs: [{
|
||||
certPath: asset('client-certificates/client/self-signed/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/self-signed/key.pem'),
|
||||
}],
|
||||
origin: new URL(serverURL).origin,
|
||||
certPath: asset('client-certificates/client/self-signed/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/self-signed/key.pem'),
|
||||
}],
|
||||
});
|
||||
await page.goto(serverURLRewrittenToLocalhost);
|
||||
await page.goto(serverURL);
|
||||
await expect(page.getByText('Sorry Bob, certificates from Bob are not welcome here')).toBeVisible();
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('should pass with matching certificates', async ({ browser, serverURLRewrittenToLocalhost, asset }) => {
|
||||
test('should pass with matching certificates', async ({ browser, startCCServer, asset, browserName }) => {
|
||||
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
||||
const page = await browser.newPage({
|
||||
clientCertificates: [{
|
||||
url: serverURLRewrittenToLocalhost,
|
||||
certs: [{
|
||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||
}],
|
||||
origin: new URL(serverURL).origin,
|
||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||
}],
|
||||
});
|
||||
await page.goto(serverURLRewrittenToLocalhost);
|
||||
await page.goto(serverURL);
|
||||
await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible();
|
||||
await page.close();
|
||||
});
|
||||
|
|
@ -255,11 +251,9 @@ test.describe('browser', () => {
|
|||
test('should have ignoreHTTPSErrors=false by default', async ({ browser, httpsServer, asset, browserName, platform }) => {
|
||||
const page = await browser.newPage({
|
||||
clientCertificates: [{
|
||||
url: 'https://just-there-that-the-client-certificates-proxy-server-is-getting-launched.com',
|
||||
certs: [{
|
||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||
}],
|
||||
origin: 'https://just-there-that-the-client-certificates-proxy-server-is-getting-launched.com',
|
||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||
}],
|
||||
});
|
||||
await page.goto(browserName === 'webkit' && platform === 'darwin' ? httpsServer.EMPTY_PAGE.replace('localhost', 'local.playwright') : httpsServer.EMPTY_PAGE);
|
||||
|
|
@ -267,6 +261,56 @@ test.describe('browser', () => {
|
|||
await page.close();
|
||||
});
|
||||
|
||||
test('support http2', async ({ browser, startCCServer, asset, browserName }) => {
|
||||
test.skip(browserName === 'webkit' && process.platform === 'darwin', 'WebKit on macOS doesn\n proxy localhost');
|
||||
const serverURL = await startCCServer({ http2: true });
|
||||
const page = await browser.newPage({
|
||||
clientCertificates: [{
|
||||
origin: new URL(serverURL).origin,
|
||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||
}],
|
||||
});
|
||||
// TODO: We should investigate why http2 is not supported in WebKit on Linux.
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=276990
|
||||
const expectedProtocol = browserName === 'webkit' && process.platform === 'linux' ? 'http/1.1' : 'h2';
|
||||
{
|
||||
await page.goto(serverURL.replace('localhost', 'local.playwright'));
|
||||
await expect(page.getByText('Sorry, but you need to provide a client certificate to continue.')).toBeVisible();
|
||||
await expect(page.getByText(`ALPN protocol: ${expectedProtocol}`)).toBeVisible();
|
||||
}
|
||||
{
|
||||
await page.goto(serverURL);
|
||||
await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible();
|
||||
await expect(page.getByText(`ALPN protocol: ${expectedProtocol}`)).toBeVisible();
|
||||
}
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('support http2 if the browser only supports http1.1', async ({ browserType, browserName, startCCServer, asset }) => {
|
||||
test.skip(browserName !== 'chromium');
|
||||
const serverURL = await startCCServer({ http2: true });
|
||||
const browser = await browserType.launch({ args: ['--disable-http2'] });
|
||||
const page = await browser.newPage({
|
||||
clientCertificates: [{
|
||||
origin: new URL(serverURL).origin,
|
||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||
}],
|
||||
});
|
||||
{
|
||||
await page.goto(serverURL.replace('localhost', 'local.playwright'));
|
||||
await expect(page.getByText('Sorry, but you need to provide a client certificate to continue.')).toBeVisible();
|
||||
await expect(page.getByText('ALPN protocol: http/1.1')).toBeVisible();
|
||||
}
|
||||
{
|
||||
await page.goto(serverURL);
|
||||
await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible();
|
||||
await expect(page.getByText('ALPN protocol: http/1.1')).toBeVisible();
|
||||
}
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
test.describe('persistentContext', () => {
|
||||
test('validate input', async ({ launchPersistent }) => {
|
||||
test.slow();
|
||||
|
|
@ -274,17 +318,16 @@ test.describe('browser', () => {
|
|||
await expect(launchPersistent(contextOptions)).rejects.toThrow(expected);
|
||||
});
|
||||
|
||||
test('should pass with matching certificates', async ({ launchPersistent, serverURLRewrittenToLocalhost, asset }) => {
|
||||
test('should pass with matching certificates', async ({ launchPersistent, startCCServer, asset, browserName }) => {
|
||||
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
||||
const { page } = await launchPersistent({
|
||||
clientCertificates: [{
|
||||
url: serverURLRewrittenToLocalhost,
|
||||
certs: [{
|
||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||
}],
|
||||
origin: new URL(serverURL).origin,
|
||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||
}],
|
||||
});
|
||||
await page.goto(serverURLRewrittenToLocalhost);
|
||||
await page.goto(serverURL);
|
||||
await expect(page.getByText('Hello Alice, your certificate was issued by localhost!')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
/**
|
||||
* 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 { contextTest as it, expect } from '../config/browserTest';
|
||||
import type { Locator } from 'playwright-core';
|
||||
|
||||
it.use({ hasTouch: true });
|
||||
|
||||
it.fixme(({ browserName }) => browserName === 'firefox');
|
||||
|
||||
it('slow swipe events @smoke', async ({ page }) => {
|
||||
it.fixme();
|
||||
await page.setContent(`<div id="a" style="background: lightblue; width: 200px; height: 200px">a</div>`);
|
||||
const eventsHandle = await trackEvents(await page.locator('#a'));
|
||||
const center = await centerPoint(page.locator('#a'));
|
||||
await page.touchscreen.touch('touchstart', [{ ...center, id: 1 }]);
|
||||
expect.soft(await eventsHandle.jsonValue()).toEqual([
|
||||
'pointerover',
|
||||
'pointerenter',
|
||||
'pointerdown',
|
||||
'touchstart',
|
||||
]);
|
||||
|
||||
await eventsHandle.evaluate(events => events.length = 0);
|
||||
await page.touchscreen.touch('touchmove', [{ x: center.x + 10, y: center.y + 10, id: 1 }]);
|
||||
await page.touchscreen.touch('touchmove', [{ x: center.x + 20, y: center.y + 20, id: 1 }]);
|
||||
expect.soft(await eventsHandle.jsonValue()).toEqual([
|
||||
'pointermove',
|
||||
'touchmove',
|
||||
'pointermove',
|
||||
'touchmove',
|
||||
]);
|
||||
|
||||
await eventsHandle.evaluate(events => events.length = 0);
|
||||
await page.touchscreen.touch('touchend', [{ x: center.x + 20, y: center.y + 20, id: 1 }]);
|
||||
expect.soft(await eventsHandle.jsonValue()).toEqual([
|
||||
'pointerup',
|
||||
'pointerout',
|
||||
'pointerleave',
|
||||
'touchend',
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
async function trackEvents(target: Locator) {
|
||||
const eventsHandle = await target.evaluateHandle(target => {
|
||||
const events: string[] = [];
|
||||
for (const event of [
|
||||
'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'click',
|
||||
'pointercancel', 'pointerdown', 'pointerenter', 'pointerleave', 'pointermove', 'pointerout', 'pointerover', 'pointerup',
|
||||
'touchstart', 'touchend', 'touchmove', 'touchcancel',])
|
||||
target.addEventListener(event, () => events.push(event), { passive: false });
|
||||
return events;
|
||||
});
|
||||
return eventsHandle;
|
||||
}
|
||||
|
||||
async function centerPoint(e: Locator) {
|
||||
const box = await e.boundingBox();
|
||||
if (!box)
|
||||
throw new Error('Element is not visible');
|
||||
return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
||||
}
|
||||
|
|
@ -78,7 +78,7 @@ it('should work with cross-process that fails before committing', async ({ page,
|
|||
expect(error instanceof Error).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should work with Cross-Origin-Opener-Policy', async ({ page, server, browserName }) => {
|
||||
it('should work with Cross-Origin-Opener-Policy', async ({ page, server }) => {
|
||||
server.setRoute('/empty.html', (req, res) => {
|
||||
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
||||
res.end();
|
||||
|
|
@ -109,7 +109,42 @@ it('should work with Cross-Origin-Opener-Policy', async ({ page, server, browser
|
|||
expect(response.request().failure()).toBeNull();
|
||||
});
|
||||
|
||||
it('should work with Cross-Origin-Opener-Policy after redirect', async ({ page, server, browserName }) => {
|
||||
it('should work with Cross-Origin-Opener-Policy and interception', async ({ page, server }) => {
|
||||
server.setRoute('/empty.html', (req, res) => {
|
||||
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
||||
res.end();
|
||||
});
|
||||
const requests = new Set();
|
||||
const events = [];
|
||||
page.on('request', r => {
|
||||
events.push('request');
|
||||
requests.add(r);
|
||||
});
|
||||
page.on('requestfailed', r => {
|
||||
events.push('requestfailed');
|
||||
requests.add(r);
|
||||
});
|
||||
page.on('requestfinished', r => {
|
||||
events.push('requestfinished');
|
||||
requests.add(r);
|
||||
});
|
||||
page.on('response', r => {
|
||||
events.push('response');
|
||||
requests.add(r.request());
|
||||
});
|
||||
await page.route('**/*', async route => {
|
||||
await new Promise(f => setTimeout(f, 100));
|
||||
await route.continue();
|
||||
});
|
||||
const response = await page.goto(server.EMPTY_PAGE);
|
||||
expect(page.url()).toBe(server.EMPTY_PAGE);
|
||||
await response.finished();
|
||||
expect(events).toEqual(['request', 'response', 'requestfinished']);
|
||||
expect(requests.size).toBe(1);
|
||||
expect(response.request().failure()).toBeNull();
|
||||
});
|
||||
|
||||
it('should work with Cross-Origin-Opener-Policy after redirect', async ({ page, server }) => {
|
||||
server.setRedirect('/redirect', '/empty.html');
|
||||
server.setRoute('/empty.html', (req, res) => {
|
||||
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
||||
|
|
@ -144,6 +179,23 @@ it('should work with Cross-Origin-Opener-Policy after redirect', async ({ page,
|
|||
expect(firstRequest.url()).toBe(server.PREFIX + '/redirect');
|
||||
});
|
||||
|
||||
it('should properly cancel Cross-Origin-Opener-Policy navigation', async ({ page, server }) => {
|
||||
server.setRoute('/empty.html', (req, res) => {
|
||||
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
||||
res.end();
|
||||
});
|
||||
const requestPromise = page.waitForRequest(server.EMPTY_PAGE);
|
||||
page.goto(server.EMPTY_PAGE).catch(() => {});
|
||||
await new Promise(f => setTimeout(f, 50));
|
||||
// Non COOP response.
|
||||
await page.goto(server.CROSS_PROCESS_PREFIX + '/error.html');
|
||||
const req = await requestPromise;
|
||||
const response = await Promise.race([req.response(), new Promise(f => setTimeout(() => f('timeout'), 5_000))]);
|
||||
// First navigation request should either receive response or be canceled by the second
|
||||
// navigation, but never hang unresolved.
|
||||
expect(response).not.toBe('timeout');
|
||||
});
|
||||
|
||||
it('should capture iframe navigation request', async ({ page, server }) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
expect(page.url()).toBe(server.EMPTY_PAGE);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
const http = require('http');
|
||||
|
||||
console.log('output from server');
|
||||
console.error('error from server');
|
||||
|
||||
const port = process.argv[2] || 3000;
|
||||
|
|
|
|||
|
|
@ -99,3 +99,8 @@ test('should be case sensitive by default with a regex', async ({ runInlineTest
|
|||
const result = await runInlineTest(files, { 'grep': '/TesT Cc/' });
|
||||
expect(result.passed).toBe(0);
|
||||
});
|
||||
|
||||
test('excluded tests should not be shown in UI', async ({ runInlineTest, runTSC }) => {
|
||||
const result = await runInlineTest(files, { 'grep': 'Test AA' });
|
||||
expect(result.passed).toBe(3);
|
||||
});
|
||||
|
|
|
|||
367
tests/playwright-test/only-changed.spec.ts
Normal file
367
tests/playwright-test/only-changed.spec.ts
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
/**
|
||||
* 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 { test as baseTest, expect, playwrightCtConfigText } from './playwright-test-fixtures';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const test = baseTest.extend<{ git(command: string): void }>({
|
||||
git: async ({}, use, testInfo) => {
|
||||
const baseDir = testInfo.outputPath();
|
||||
|
||||
const git = (command: string) => execSync(`git ${command}`, { cwd: baseDir });
|
||||
|
||||
git(`init --initial-branch=main`);
|
||||
git(`config --local user.name "Robert Botman"`);
|
||||
git(`config --local user.email "botty@mcbotface.com"`);
|
||||
|
||||
await use((command: string) => git(command));
|
||||
},
|
||||
});
|
||||
|
||||
test.slow();
|
||||
|
||||
test('should detect untracked files', async ({ runInlineTest, git, writeFiles }) => {
|
||||
await writeFiles({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
'b.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
});
|
||||
|
||||
git(`add .`);
|
||||
git(`commit -m init`);
|
||||
|
||||
const result = await runInlineTest({
|
||||
'c.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`
|
||||
}, { 'only-changed': true });
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(result.passed).toBe(0);
|
||||
expect(result.output).toContain('c.spec.ts');
|
||||
});
|
||||
|
||||
|
||||
test('should detect changed files', async ({ runInlineTest, git, writeFiles }) => {
|
||||
await writeFiles({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
'b.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
});
|
||||
|
||||
git(`add .`);
|
||||
git(`commit -m init`);
|
||||
|
||||
const result = await runInlineTest({
|
||||
'b.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(3); });
|
||||
`,
|
||||
}, { 'only-changed': true });
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(result.passed).toBe(0);
|
||||
expect(result.output).toContain('b.spec.ts');
|
||||
});
|
||||
|
||||
test('should diff based on base commit', async ({ runInlineTest, git, writeFiles }) => {
|
||||
await writeFiles({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
'b.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
});
|
||||
|
||||
git(`add .`);
|
||||
git(`commit -m init`);
|
||||
|
||||
await writeFiles({
|
||||
'b.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(3); });
|
||||
`,
|
||||
});
|
||||
git('commit -a -m update');
|
||||
const result = await runInlineTest({}, { 'only-changed': `HEAD~1` });
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(result.passed).toBe(0);
|
||||
expect(result.output).toContain('b.spec.ts');
|
||||
});
|
||||
|
||||
test('should understand dependency structure', async ({ runInlineTest, git, writeFiles }) => {
|
||||
await writeFiles({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { answer, question } from './utils';
|
||||
test('fails', () => { expect(question).toBe(answer); });
|
||||
`,
|
||||
'b.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { answer, question } from './utils';
|
||||
test('fails', () => { expect(question).toBe(answer); });
|
||||
`,
|
||||
'c.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
'utils.ts': `
|
||||
export * from './answer';
|
||||
export * from './question';
|
||||
`,
|
||||
'answer.ts': `
|
||||
export const answer = 42;
|
||||
`,
|
||||
'question.ts': `
|
||||
export const question = "???";
|
||||
`,
|
||||
});
|
||||
|
||||
git(`add .`);
|
||||
git(`commit -m init`);
|
||||
|
||||
await writeFiles({
|
||||
'question.ts': `
|
||||
export const question = "what is the answer to life the universe and everything";
|
||||
`,
|
||||
});
|
||||
const result = await runInlineTest({}, { 'only-changed': true });
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.failed).toBe(2);
|
||||
expect(result.passed).toBe(0);
|
||||
expect(result.output).toContain('a.spec.ts');
|
||||
expect(result.output).toContain('b.spec.ts');
|
||||
expect(result.output).not.toContain('c.spec.ts');
|
||||
});
|
||||
|
||||
test('should support watch mode', async ({ git, writeFiles, runWatchTest }) => {
|
||||
await writeFiles({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
'b.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
});
|
||||
|
||||
git(`add .`);
|
||||
git(`commit -m init`);
|
||||
|
||||
await writeFiles({
|
||||
'b.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(3); });
|
||||
`,
|
||||
});
|
||||
git(`commit -a -m update`);
|
||||
|
||||
const testProcess = await runWatchTest({}, { 'only-changed': `HEAD~1` });
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('r');
|
||||
|
||||
await testProcess.waitForOutput('b.spec.ts:3:13 › fails');
|
||||
expect(testProcess.output).not.toContain('a.spec');
|
||||
});
|
||||
|
||||
test('should throw nice error message if git doesnt work', async ({ git, runInlineTest }) => {
|
||||
const result = await runInlineTest({}, { 'only-changed': `this-commit-does-not-exist` });
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.output, 'contains our error message').toContain('Cannot detect changed files for --only-changed mode');
|
||||
expect(result.output, 'contains command').toContain('git diff this-commit-does-not-exist --name-only');
|
||||
expect(result.output, 'contains git command output').toContain('unknown revision or path not in the working tree');
|
||||
});
|
||||
|
||||
test('should suppport component tests', async ({ runInlineTest, git, writeFiles }) => {
|
||||
await writeFiles({
|
||||
'playwright.config.ts': playwrightCtConfigText,
|
||||
'playwright/index.html': `<script type="module" src="./index.ts"></script>`,
|
||||
'playwright/index.ts': `
|
||||
`,
|
||||
'src/contents.ts': `
|
||||
export const content = "Button";
|
||||
`,
|
||||
'src/button.tsx': `
|
||||
import {content} from './contents';
|
||||
export const Button = () => <button>{content}</button>;
|
||||
`,
|
||||
'src/helper.ts': `
|
||||
export { Button } from "./button";
|
||||
`,
|
||||
'src/button.test.tsx': `
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import { Button } from './helper';
|
||||
|
||||
test('pass', async ({ mount }) => {
|
||||
const component = await mount(<Button></Button>);
|
||||
await expect(component).toHaveText('Button');
|
||||
});
|
||||
`,
|
||||
'src/button2.test.tsx': `
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import { Button } from './helper';
|
||||
|
||||
test('pass', async ({ mount }) => {
|
||||
const component = await mount(<Button></Button>);
|
||||
await expect(component).toHaveText('Button');
|
||||
});
|
||||
`,
|
||||
'src/button3.test.tsx': `
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
|
||||
test('pass', async ({ mount }) => {
|
||||
const component = await mount(<p>Hello World</p>);
|
||||
await expect(component).toHaveText('Hello World');
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
git(`add .`);
|
||||
git(`commit -m "init"`);
|
||||
|
||||
const result = await runInlineTest({}, { 'workers': 1, 'only-changed': true });
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.passed).toBe(0);
|
||||
expect(result.failed).toBe(0);
|
||||
expect(result.output).toContain('No tests found');
|
||||
|
||||
const result2 = await runInlineTest({
|
||||
'src/button2.test.tsx': `
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import { Button } from './helper';
|
||||
|
||||
test('pass', async ({ mount }) => {
|
||||
const component = await mount(<Button></Button>);
|
||||
await expect(component).toHaveText('Different Button');
|
||||
});
|
||||
`
|
||||
}, { 'workers': 1, 'only-changed': true });
|
||||
|
||||
expect(result2.exitCode).toBe(1);
|
||||
expect(result2.failed).toBe(1);
|
||||
expect(result2.passed).toBe(0);
|
||||
expect(result2.output).toContain('button2.test.tsx');
|
||||
expect(result2.output).not.toContain('button.test.tsx');
|
||||
expect(result2.output).not.toContain('button3.test.tsx');
|
||||
|
||||
git(`commit -am "update button2 test"`);
|
||||
|
||||
const result3 = await runInlineTest({
|
||||
'src/contents.ts': `
|
||||
export const content = 'Changed Content';
|
||||
`
|
||||
}, { 'workers': 1, 'only-changed': true });
|
||||
|
||||
expect(result3.exitCode).toBe(1);
|
||||
expect(result3.failed).toBe(2);
|
||||
expect(result3.passed).toBe(0);
|
||||
});
|
||||
|
||||
test.describe('should work the same if being called in subdirectory', () => {
|
||||
test('tracked file', async ({ runInlineTest, git, writeFiles }) => {
|
||||
await writeFiles({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
'b.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
});
|
||||
|
||||
git(`add .`);
|
||||
git(`commit -m init`);
|
||||
|
||||
await writeFiles({
|
||||
'tests/c.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`
|
||||
});
|
||||
git(`add .`);
|
||||
git(`commit -a -m "add test"`);
|
||||
|
||||
const result = await runInlineTest({
|
||||
'tests/c.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(3); });
|
||||
`
|
||||
}, { 'only-changed': true }, {}, { cwd: 'tests' });
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(result.passed).toBe(0);
|
||||
expect(result.output).toContain('c.spec.ts');
|
||||
});
|
||||
|
||||
test('untracked file', async ({ runInlineTest, git, writeFiles }) => {
|
||||
await writeFiles({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
'b.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
});
|
||||
|
||||
git(`add .`);
|
||||
git(`commit -m init`);
|
||||
|
||||
const result = await runInlineTest({
|
||||
'tests/c.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(3); });
|
||||
`
|
||||
}, { 'only-changed': true }, {}, { cwd: 'tests' });
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(result.passed).toBe(0);
|
||||
expect(result.output).toContain('c.spec.ts');
|
||||
});
|
||||
});
|
||||
|
||||
test('UI mode is not supported', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({}, { 'only-changed': true, 'ui': true });
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.output).toContain('--only-changed is not supported in UI mode');
|
||||
});
|
||||
|
|
@ -246,7 +246,7 @@ type Fixtures = {
|
|||
deleteFile: (file: string) => Promise<void>;
|
||||
runInlineTest: (files: Files, params?: Params, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise<RunResult>;
|
||||
runCLICommand: (files: Files, command: string, args?: string[], entryPoint?: string) => Promise<{ stdout: string, stderr: string, exitCode: number }>;
|
||||
runWatchTest: (files: Files, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise<TestChildProcess>;
|
||||
runWatchTest: (files: Files, params?: Params, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise<TestChildProcess>;
|
||||
interactWithTestRunner: (files: Files, params?: Params, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise<TestChildProcess>;
|
||||
runTSC: (files: Files) => Promise<TSCResult>;
|
||||
mergeReports: (reportFolder: string, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise<CliRunResult>
|
||||
|
|
@ -288,7 +288,7 @@ export const test = base
|
|||
},
|
||||
|
||||
runWatchTest: async ({ interactWithTestRunner }, use, testInfo: TestInfo) => {
|
||||
await use((files, env, options) => interactWithTestRunner(files, {}, { ...env, PWTEST_WATCH: '1' }, options));
|
||||
await use((files, params, env, options) => interactWithTestRunner(files, params, { ...env, PWTEST_WATCH: '1' }, options));
|
||||
},
|
||||
|
||||
interactWithTestRunner: async ({ childProcess }, use, testInfo: TestInfo) => {
|
||||
|
|
|
|||
|
|
@ -229,3 +229,18 @@ test('should filter skipped', async ({ runUITest, createLatch }) => {
|
|||
⊘ fails
|
||||
`);
|
||||
});
|
||||
|
||||
test('should only show tests selected with --grep', async ({ runUITest }) => {
|
||||
const { page } = await runUITest(basicTestTree, undefined, {
|
||||
additionalArgs: ['--grep', 'fails'],
|
||||
});
|
||||
await expect.poll(dumpTestTree(page)).not.toContain('passes');
|
||||
});
|
||||
|
||||
test('should not show tests filtered with --grep-invert', async ({ runUITest }) => {
|
||||
const { page } = await runUITest(basicTestTree, undefined, {
|
||||
additionalArgs: ['--grep-invert', 'fails'],
|
||||
});
|
||||
await expect.poll(dumpTestTree(page)).toContain('passes');
|
||||
await expect.poll(dumpTestTree(page)).not.toContain('fails');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
|
||||
import { test, expect, retries } from './ui-mode-fixtures';
|
||||
import path from 'path';
|
||||
|
||||
test.describe.configure({ mode: 'parallel', retries });
|
||||
|
||||
|
|
@ -202,3 +203,29 @@ test('should print beforeAll console messages once', async ({ runUITest }, testI
|
|||
'test log',
|
||||
]);
|
||||
});
|
||||
|
||||
test('should print web server output', async ({ runUITest }, { workerIndex }) => {
|
||||
const port = workerIndex * 2 + 10500;
|
||||
const serverPath = path.join(__dirname, 'assets', 'simple-server.js');
|
||||
const { page } = await runUITest({
|
||||
'test.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('connect to the server', async ({baseURL, page}) => {
|
||||
expect(baseURL).toBe('http://localhost:${port}');
|
||||
});
|
||||
`,
|
||||
'playwright.config.ts': `
|
||||
module.exports = {
|
||||
webServer: {
|
||||
command: 'node ${JSON.stringify(serverPath)} ${port}',
|
||||
port: ${port},
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
}
|
||||
};
|
||||
`,
|
||||
});
|
||||
await page.getByTitle('Toggle output').click();
|
||||
await expect(page.getByTestId('output')).toContainText('output from server');
|
||||
await expect(page.getByTestId('output')).toContainText('error from server');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -179,20 +179,20 @@ test('should perform initial run', async ({ runWatchTest }) => {
|
|||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
}, {});
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
});
|
||||
|
||||
test('should quit on Q', async ({ runWatchTest }) => {
|
||||
const testProcess = await runWatchTest({}, {});
|
||||
const testProcess = await runWatchTest({});
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.write('q');
|
||||
await testProcess!.exited;
|
||||
});
|
||||
|
||||
test('should print help on H', async ({ runWatchTest }) => {
|
||||
const testProcess = await runWatchTest({}, {});
|
||||
const testProcess = await runWatchTest({});
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.write('h');
|
||||
await testProcess.waitForOutput('to quit');
|
||||
|
|
@ -204,7 +204,7 @@ test('should run tests on Enter', async ({ runWatchTest }) => {
|
|||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
}, {});
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
|
|
@ -220,7 +220,7 @@ test('should run tests on R', async ({ runWatchTest }) => {
|
|||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
}, {});
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
|
|
@ -244,7 +244,7 @@ test('should run failed tests on F', async ({ runWatchTest }) => {
|
|||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
}, {});
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('c.test.ts:3:11 › fails');
|
||||
|
|
@ -267,7 +267,7 @@ test('should respect file filter P', async ({ runWatchTest }) => {
|
|||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
}, {});
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
|
|
@ -291,7 +291,7 @@ test('should respect project filter C', async ({ runWatchTest }) => {
|
|||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
}, {});
|
||||
});
|
||||
await testProcess.waitForOutput('[foo] › a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('[bar] › a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
|
|
@ -317,7 +317,7 @@ test('should respect file filter P and split files', async ({ runWatchTest }) =>
|
|||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
}, {});
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
|
|
@ -341,7 +341,7 @@ test('should respect title filter T', async ({ runWatchTest }) => {
|
|||
import { test, expect } from '@playwright/test';
|
||||
test('title 2', () => {});
|
||||
`,
|
||||
}, {});
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › title 1');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › title 2');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
|
|
@ -369,7 +369,7 @@ test('should re-run failed tests on F > R', async ({ runWatchTest }) => {
|
|||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
}, {});
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('c.test.ts:3:11 › fails');
|
||||
|
|
@ -401,7 +401,7 @@ test('should run on changed files', async ({ runWatchTest, writeFiles }) => {
|
|||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
}, {});
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('c.test.ts:3:11 › fails');
|
||||
|
|
@ -434,7 +434,7 @@ test('should run on changed deps', async ({ runWatchTest, writeFiles }) => {
|
|||
'helper.ts': `
|
||||
console.log('old helper');
|
||||
`,
|
||||
}, {});
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:4:11 › passes');
|
||||
await testProcess.waitForOutput('old helper');
|
||||
|
|
@ -467,7 +467,7 @@ test('should run on changed deps in ESM', async ({ runWatchTest, writeFiles }) =
|
|||
'helper.ts': `
|
||||
console.log('old helper');
|
||||
`,
|
||||
}, {});
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:7 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:4:7 › passes');
|
||||
await testProcess.waitForOutput('old helper');
|
||||
|
|
@ -498,7 +498,7 @@ test('should re-run changed files on R', async ({ runWatchTest, writeFiles }) =>
|
|||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
}, {});
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('c.test.ts:3:11 › fails');
|
||||
|
|
@ -533,7 +533,7 @@ test('should not trigger on changes to non-tests', async ({ runWatchTest, writeF
|
|||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
}, {});
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
|
|
@ -559,7 +559,7 @@ test('should only watch selected projects', async ({ runWatchTest, writeFiles })
|
|||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
}, {}, { additionalArgs: ['--project=foo'] });
|
||||
}, undefined, undefined, { additionalArgs: ['--project=foo'] });
|
||||
await testProcess.waitForOutput('npx playwright test --project foo');
|
||||
await testProcess.waitForOutput('[foo] › a.test.ts:3:11 › passes');
|
||||
expect(testProcess.output).not.toContain('[bar]');
|
||||
|
|
@ -589,7 +589,7 @@ test('should watch filtered files', async ({ runWatchTest, writeFiles }) => {
|
|||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
}, {}, { additionalArgs: ['a.test.ts'] });
|
||||
}, undefined, undefined, { additionalArgs: ['a.test.ts'] });
|
||||
await testProcess.waitForOutput('npx playwright test a.test.ts');
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
expect(testProcess.output).not.toContain('b.test');
|
||||
|
|
@ -617,7 +617,7 @@ test('should not watch unfiltered files', async ({ runWatchTest, writeFiles }) =
|
|||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
}, {}, { additionalArgs: ['a.test.ts'] });
|
||||
}, undefined, undefined, { additionalArgs: ['a.test.ts'] });
|
||||
await testProcess.waitForOutput('npx playwright test a.test.ts');
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
expect(testProcess.output).not.toContain('b.test');
|
||||
|
|
@ -661,7 +661,7 @@ test('should run CT on changed deps', async ({ runWatchTest, writeFiles }) => {
|
|||
await expect(component).toHaveText('hello');
|
||||
});
|
||||
`,
|
||||
}, {});
|
||||
});
|
||||
await testProcess.waitForOutput('button.spec.tsx:4:11 › pass');
|
||||
await testProcess.waitForOutput('link.spec.tsx:3:11 › pass');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
|
|
@ -709,7 +709,7 @@ test('should run CT on indirect deps change', async ({ runWatchTest, writeFiles
|
|||
await expect(component).toHaveText('hello');
|
||||
});
|
||||
`,
|
||||
}, {});
|
||||
});
|
||||
await testProcess.waitForOutput('button.spec.tsx:4:11 › pass');
|
||||
await testProcess.waitForOutput('link.spec.tsx:3:11 › pass');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
|
|
@ -753,7 +753,7 @@ test('should run CT on indirect deps change ESM mode', async ({ runWatchTest, wr
|
|||
await expect(component).toHaveText('hello');
|
||||
});
|
||||
`,
|
||||
}, {});
|
||||
});
|
||||
await testProcess.waitForOutput('button.spec.tsx:4:7 › pass');
|
||||
await testProcess.waitForOutput('link.spec.tsx:3:7 › pass');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
|
|
@ -786,7 +786,7 @@ test('should run global teardown before exiting', async ({ runWatchTest }) => {
|
|||
test('passes', async () => {
|
||||
});
|
||||
`,
|
||||
}, {});
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.write('\x1B');
|
||||
|
|
|
|||
Loading…
Reference in a new issue