Skip to content

Commit

Permalink
api(networkidle): remove networkidle2
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman committed Apr 20, 2020
1 parent 621df5d commit 3e4e17e
Show file tree
Hide file tree
Showing 12 changed files with 164 additions and 255 deletions.
17 changes: 10 additions & 7 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,20 @@
1. Scraping and verification
- Screenshots
- Evaluation
1. Selector engines
- Built-in engines
- Custom engines
1. [Continuous integration](./ci.md)
- [Docker](./ci.md#docker)
- [GitHub Actions](./ci.md#github-actions)
- [Azure Pipelines](./ci.md#azure-pipelines)
- [Travis CI](./ci.md#travis-ci)
- [CircleCI](./ci.md#circleci)
- [AppVeyor](./ci.md#appveyor)
1. Test runners
- Jest
- Mocha
- Karma
- Jasmine
- Jasmine
- Storybooks
1. Continuous integration
- Git Hub Action
- Docker images
- Troubleshooting
1. [Extensibility](./extensibility.md)
- [Custom selector engines](./extensibility.md#custom-selector-engines)

55 changes: 22 additions & 33 deletions docs/api.md

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions docs/ci.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
# Getting started on CI
# Continuous integration.md

Playwright tests can be executed to run on your CI environments. To simplify this, we have created sample configurations for common CI providers that can be used to bootstrap your setup.

#### Contents
- [Docker](#docker)
- [GitHub](#github-actions)
- [GitHub Actions](#github-actions)
- [Azure Pipelines](#azure-pipelines)
- [Travis CI](#travis-ci)
- [CircleCI](#circleci)
- [AppVeyor](#appveyor)

Broadly, configuration on CI involves **ensuring system dependencies** are in place, **installing Playwright and browsers** (typically with `npm install`), and **running tests** (typically with `npm test`). Windows and macOS build agents do not require any additional system dependencies. Linux build agents can require additional dependencies, depending on the Linux distribution.

<br/>

## Docker

We have a [pre-built Docker image](docker/README.md) which can either be used directly, or as a reference to update your existing Docker definitions.
Expand Down
73 changes: 57 additions & 16 deletions docs/core-concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,31 +109,57 @@ await frame.fill('#username-input', 'John');

## Selectors

Playwright APIs that interact with elements accept selectors as the first argument, used to search for the element. Playwright can search for elements with CSS selectors, XPath, HTML attributes like `id`, `data-test-id` and text content.
Playwright can search for elements using CSS selectors, XPath selectors, HTML attributes like `id`, `data-test-id` and even text content.

Note that all selectors except for XPath pierce shadow DOM automatically.
You can explicitly specify the selector engine you are using or let Playwright detect it.

All selector engines except for XPath pierce shadow DOM by default. If you want to enforce regular DOM selection, you can use the `*:light` versions of the selectors. You don't typically need to though.

Learn more about selectors and selector engines [here](./selectors.md).

Some examples below:

```js
// Using data-test-id= selector engine
await page.click('data-test-id=foo');
```

```js
// Auto-detected CSS notation
// CSS and XPath selector engines are automatically detected
await page.click('div');
await page.click('//html/body/div');
```

// Explicit CSS notation
await page.click('css=div');
```js
// Find node by text substring
await page.click('text=Hello w');
```

// Auto-detected XPath notation
```js
// Explicit CSS and XPath notation
await page.click('css=div');
await page.click('xpath=//html/body/div');
```

// Explicit XPath notation
await page.click('//html/body/div');
```js
// Only search light DOM, outside WebComponent shadow DOM:
await page.click('css:light=div');
```

// Auto-detected text notation
await page.click('"Login"');
Selectors using the same or different engines can be combined using the `>>` separator. For example,

// Explicit text notation
await page.click('text="Login"');
```js
await page.click('css=article >> css=.bar > .baz >> css=span[attr=value]');
```

Selectors using different engines can be combined using the `>>` separator. Learn more about selectors and selector engines [here](./selectors.md).
is equivalent to

```js
document
.querySelector('article')
.querySelector('.bar > .baz')
.querySelector('span[attr=value]')
```

<br/>

Expand All @@ -148,17 +174,32 @@ Actions like `click` and `fill` auto-wait for the element to be visible and acti


```js
// Will wait for #search element to be in DOM
// Playwright waits for #search element to be in DOM
await page.fill('#search', 'query');

// Will wait for it to stop animating and accept clicks
```
```js
// Playwright waits for element to stop animating
// and accept clicks.
await page.click('#search');
```

You can explicitly wait for element to become available in DOM and to become visible:

```js
await page.waitForSelector('#search', { waitFor: 'visible' });
```

... or to become hidden or detached

```js
await page.waitForSelector('#search', { waitFor: 'detached' });
```

#### API reference

- [page.click(selector[, options])](./api.md#pageclickselector-options)
- [page.fill(selector, value[, options])](./api.md#pagefillselector-value-options)
- [page.waitForSelector(selector[, options])](./api.md#pagewaitforselectorselector-options)

<br/>

Expand Down
2 changes: 1 addition & 1 deletion docs/loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Page load takes time retrieving the response body over the network, parsing, exe
- page executes some scripts and loads resources like stylesheets and images
- [`load`](api.md#event-load) event is fired
- page executes dynamically loaded scripts
- `networkidle0` is fired - no new network requests made for at least `500` ms
- `networkidle` is fired - no new network requests made for at least `500` ms

### Common scenarios

Expand Down
46 changes: 0 additions & 46 deletions docs/selectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,49 +120,3 @@ Malformed selector starting with `"` is assumed to be a text selector. For examp
### id, data-testid, data-test-id, data-test and their :light counterparts

Attribute engines are selecting based on the corresponding atrribute value. For example: `data-test-id=foo` is equivalent to `css=[data-test-id="foo"]`, and `id:light=foo` is equivalent to `css:light=[id="foo"]`.

## Custom selector engines

Playwright supports custom selector engines, registered with [selectors.register(name, script[, options])](api.md#selectorsregistername-script-options).

Selector engine should have the following properties:

- `create` function to create a relative selector from `root` (root is either a `Document`, `ShadowRoot` or `Element`) to a `target` element.
- `query` function to query first element matching `selector` relative to the `root`.
- `queryAll` function to query all elements matching `selector` relative to the `root`.

By default the engine is run directly in the frame's JavaScript context and, for example, can call an application-defined function. To isolate the engine from any JavaScript in the frame, but leave access to the DOM, resgister the engine with `{contentScript: true}` option. Content script engine is safer because it is protected from any tampering with the global objects, for example altering `Node.prototype` methods. All built-in selector engines run as content scripts. Note that running as a content script is not guaranteed when the engine is used together with other custom engines.

An example of registering selector engine that queries elements based on a tag name:
```js
// Must be a function that evaluates to a selector engine instance.
const createTagNameEngine = () => ({
// Creates a selector that matches given target when queried at the root.
// Can return undefined if unable to create one.
create(root, target) {
return root.querySelector(target.tagName) === target ? target.tagName : undefined;
},

// Returns the first element matching given selector in the root's subtree.
query(root, selector) {
return root.querySelector(selector);
},

// Returns all elements matching given selector in the root's subtree.
queryAll(root, selector) {
return Array.from(root.querySelectorAll(selector));
}
});

// Register the engine. Selectors will be prefixed with "tag=".
await selectors.register('tag', createTagNameEngine);

// Now we can use 'tag=' selectors.
const button = await page.$('tag=button');

// We can combine it with other selector engines using `>>` combinator.
await page.click('tag=div >> span >> "Click me"');

// We can use it in any methods supporting selectors.
const buttonCount = await page.$$eval('tag=button', buttons => buttons.length);
```
46 changes: 18 additions & 28 deletions src/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,9 @@ export class FrameManager {
frame._firedLifecycleEvents.clear();
// Keep the current navigation request if any.
frame._inflightRequests = new Set(Array.from(frame._inflightRequests).filter(request => request._documentId === frame._lastDocumentId));
this._stopNetworkIdleTimer(frame, 'networkidle0');
frame._stopNetworkIdleTimer();
if (frame._inflightRequests.size === 0)
this._startNetworkIdleTimer(frame, 'networkidle0');
this._stopNetworkIdleTimer(frame, 'networkidle2');
if (frame._inflightRequests.size <= 2)
this._startNetworkIdleTimer(frame, 'networkidle2');
frame._startNetworkIdleTimer();
}

requestStarted(request: network.Request) {
Expand Down Expand Up @@ -282,9 +279,7 @@ export class FrameManager {
return;
frame._inflightRequests.delete(request);
if (frame._inflightRequests.size === 0)
this._startNetworkIdleTimer(frame, 'networkidle0');
if (frame._inflightRequests.size === 2)
this._startNetworkIdleTimer(frame, 'networkidle2');
frame._startNetworkIdleTimer();
}

private _inflightRequestStarted(request: network.Request) {
Expand All @@ -293,25 +288,7 @@ export class FrameManager {
return;
frame._inflightRequests.add(request);
if (frame._inflightRequests.size === 1)
this._stopNetworkIdleTimer(frame, 'networkidle0');
if (frame._inflightRequests.size === 3)
this._stopNetworkIdleTimer(frame, 'networkidle2');
}

private _startNetworkIdleTimer(frame: Frame, event: types.LifecycleEvent) {
assert(!frame._networkIdleTimers.has(event));
if (frame._firedLifecycleEvents.has(event))
return;
frame._networkIdleTimers.set(event, setTimeout(() => {
this.frameLifecycleEvent(frame._id, event);
}, 500));
}

private _stopNetworkIdleTimer(frame: Frame, event: types.LifecycleEvent) {
const timeoutId = frame._networkIdleTimers.get(event);
if (timeoutId)
clearTimeout(timeoutId);
frame._networkIdleTimers.delete(event);
frame._stopNetworkIdleTimer();
}

interceptConsoleMessage(message: ConsoleMessage): boolean {
Expand Down Expand Up @@ -341,7 +318,7 @@ export class Frame {
private _childFrames = new Set<Frame>();
_name = '';
_inflightRequests = new Set<network.Request>();
readonly _networkIdleTimers = new Map<types.LifecycleEvent, NodeJS.Timer>();
private _networkIdleTimer: NodeJS.Timer | undefined;
private _setContentCounter = 0;
readonly _detachedPromise: Promise<void>;
private _detachedCallback = () => {};
Expand Down Expand Up @@ -859,6 +836,19 @@ export class Frame {
this._setContext(contextType, null);
}
}

_startNetworkIdleTimer() {
assert(!this._networkIdleTimer);
if (this._firedLifecycleEvents.has('networkidle'))
return;
this._networkIdleTimer = setTimeout(() => { this._page._frameManager.frameLifecycleEvent(this._id, 'networkidle'); }, 500);
}

_stopNetworkIdleTimer() {
if (this._networkIdleTimer)
clearTimeout(this._networkIdleTimer);
this._networkIdleTimer = undefined;
}
}

type Task = (context: dom.FrameExecutionContext) => Promise<js.JSHandle>;
Expand Down
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ export type WaitForElementOptions = TimeoutOptions & { waitFor?: 'attached' | 'd
export type Polling = 'raf' | 'mutation' | number;
export type WaitForFunctionOptions = TimeoutOptions & { polling?: Polling };

export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2';
export const kLifecycleEvents: Set<LifecycleEvent> = new Set(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']);
export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle';
export const kLifecycleEvents: Set<LifecycleEvent> = new Set(['load', 'domcontentloaded', 'networkidle']);

export type NavigateOptions = TimeoutOptions & {
waitUntil?: LifecycleEvent,
Expand Down
5 changes: 3 additions & 2 deletions test/assets/networkidle.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ async function sleep(delay) {
}

async function main() {
window.ws = new WebSocket('ws://localhost:' + window.location.port + '/ws');
window.ws.addEventListener('message', message => {});

const roundOne = Promise.all([
fetch('fetch-request-a.js'),
fetch('fetch-request-b.js'),
fetch('fetch-request-c.js'),
]);

await roundOne;
Expand Down
2 changes: 1 addition & 1 deletion test/autowaiting.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe('Auto waiting', () => {
]);
expect(messages.join('|')).toBe('popup|click');
});
it('should await download when clicking anchor', async function({page, server}) {
it.fail(CHROMIUM)('should await download when clicking anchor', async function({page, server}) {
server.setRoute('/download', (req, res) => {
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Disposition', 'attachment');
Expand Down
Loading

0 comments on commit 3e4e17e

Please sign in to comment.