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 4d8c057 commit 2a3e09e
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 184 deletions.
57 changes: 22 additions & 35 deletions docs/api.md

Large diffs are not rendered by default.

51 changes: 51 additions & 0 deletions docs/extensibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Extensibility

#### Contents

- [Custom selector engines](#custom-selector-engines)

## 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);
```
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: 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 2a3e09e

Please sign in to comment.