Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: move more injected code to injected to reduce evaluation size #1093

Merged
merged 1 commit into from
Feb 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1277,7 +1277,7 @@ await browser.close();

#### page.select(selector, value, options)
- `selector` <[string]> A selector to query frame for.
- `value` <[string]|[ElementHandle]|[Array]<[string]>|[Object]|[Array]<[ElementHandle]>|[Array]<[Object]>> Options to select. If the `<select>` has the `multiple` attribute, all matching options are selected, otherwise only the first option matching one of the passed options is selected. String values are equivalent to `{value:'string'}`. Option is considered matching if all specified properties match.
- `value` <[string]|[ElementHandle]|[Object]|[Array]<[string]>|[Array]<[ElementHandle]>|[Array]<[Object]>> Options to select. If the `<select>` has the `multiple` attribute, all matching options are selected, otherwise only the first option matching one of the passed options is selected. String values are equivalent to `{value:'string'}`. Option is considered matching if all specified properties match.
- `value` <[string]> Matches by `option.value`.
- `label` <[string]> Matches by `option.label`.
- `index` <[number]> Matches by the index.
Expand Down Expand Up @@ -2006,7 +2006,7 @@ If the name is empty, returns the id attribute instead.

#### frame.select(selector, value, options)
- `selector` <[string]> A selector to query frame for.
- `value` <[string]|[ElementHandle]|[Array]<[string]>|[Object]|[Array]<[ElementHandle]>|[Array]<[Object]>> Options to select. If the `<select>` has the `multiple` attribute, all matching options are selected, otherwise only the first option matching one of the passed options is selected. String values are equivalent to `{value:'string'}`. Option is considered matching if all specified properties match.
- `value` <[string]|[ElementHandle]|[Object]|[Array]<[string]>|[Array]<[ElementHandle]>|[Array]<[Object]>> Options to select. If the `<select>` has the `multiple` attribute, all matching options are selected, otherwise only the first option matching one of the passed options is selected. String values are equivalent to `{value:'string'}`. Option is considered matching if all specified properties match.
- `value` <[string]> Matches by `option.value`.
- `label` <[string]> Matches by `option.label`.
- `index` <[number]> Matches by the index.
Expand Down
175 changes: 18 additions & 157 deletions src/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return this;
}

_evaluateInUtility: types.EvaluateOn<T> = async (pageFunction, ...args) => {
_evaluateInUtility: types.EvaluateWithInjected<T> = async (pageFunction, ...args) => {
const utility = await this._context.frame._utilityContext();
return utility.evaluate(pageFunction as any, this, ...args);
return utility.evaluate(pageFunction as any, await utility._injected(), this, ...args);
}

async ownerFrame(): Promise<frames.Frame | null> {
Expand All @@ -154,7 +154,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}

async contentFrame(): Promise<frames.Frame | null> {
const isFrameElement = await this._evaluateInUtility(node => node && (node.nodeName === 'IFRAME' || node.nodeName === 'FRAME'));
const isFrameElement = await this._evaluateInUtility((injected, node) => node && (node.nodeName === 'IFRAME' || node.nodeName === 'FRAME'));
if (!isFrameElement)
return null;
return this._page._delegate.getContentFrame(this);
Expand Down Expand Up @@ -210,12 +210,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
private async _relativePoint(relativePoint: types.Point): Promise<types.Point> {
const [box, border] = await Promise.all([
this.boundingBox(),
this._evaluateInUtility((node: Node) => {
if (node.nodeType !== Node.ELEMENT_NODE || !node.ownerDocument || !node.ownerDocument.defaultView)
return { x: 0, y: 0 };
const style = node.ownerDocument.defaultView.getComputedStyle(node as Element);
return { x: parseInt(style.borderLeftWidth || '', 10), y: parseInt(style.borderTopWidth || '', 10) };
}).catch(debugError),
this._evaluateInUtility((injected, node) => injected.getElementBorderWidth(node)).catch(debugError),
]);
const point = { x: relativePoint.x, y: relativePoint.y };
if (box) {
Expand All @@ -224,8 +219,8 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
if (border) {
// Make point relative to the padding box to align with offsetX/offsetY.
point.x += border.x;
point.y += border.y;
point.x += border.left;
point.y += border.top;
}
return point;
}
Expand Down Expand Up @@ -277,92 +272,12 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (option.index !== undefined)
assert(helper.isNumber(option.index), 'Indices must be numbers. Found index "' + option.index + '" of type "' + (typeof option.index) + '"');
}
return this._evaluateInUtility((node: Node, ...optionsToSelect: (Node | types.SelectOption)[]) => {
if (node.nodeName.toLowerCase() !== 'select')
throw new Error('Element is not a <select> element.');
const element = node as HTMLSelectElement;

const options = Array.from(element.options);
element.value = undefined as any;
for (let index = 0; index < options.length; index++) {
const option = options[index];
option.selected = optionsToSelect.some(optionToSelect => {
if (optionToSelect instanceof Node)
return option === optionToSelect;
let matches = true;
if (optionToSelect.value !== undefined)
matches = matches && optionToSelect.value === option.value;
if (optionToSelect.label !== undefined)
matches = matches && optionToSelect.label === option.label;
if (optionToSelect.index !== undefined)
matches = matches && optionToSelect.index === index;
return matches;
});
if (option.selected && !element.multiple)
break;
}
element.dispatchEvent(new Event('input', { 'bubbles': true }));
element.dispatchEvent(new Event('change', { 'bubbles': true }));
return options.filter(option => option.selected).map(option => option.value);
}, ...options);
return this._evaluateInUtility((injected, node, ...optionsToSelect) => injected.selectOptions(node, optionsToSelect), ...options);
}

async fill(value: string): Promise<void> {
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
const error = await this._evaluateInUtility((node: Node, value: string) => {
if (node.nodeType !== Node.ELEMENT_NODE)
return 'Node is not of type HTMLElement';
const element = node as HTMLElement;
if (!element.isConnected)
return 'Element is not attached to the DOM';
if (!element.ownerDocument || !element.ownerDocument.defaultView)
return 'Element does not belong to a window';

const style = element.ownerDocument.defaultView.getComputedStyle(element);
if (!style || style.visibility === 'hidden')
return 'Element is hidden';
if (!element.offsetParent && element.tagName !== 'BODY')
return 'Element is not visible';
if (element.nodeName.toLowerCase() === 'input') {
const input = element as HTMLInputElement;
const type = input.getAttribute('type') || '';
const kTextInputTypes = new Set(['', 'email', 'number', 'password', 'search', 'tel', 'text', 'url']);
if (!kTextInputTypes.has(type.toLowerCase()))
return 'Cannot fill input of type "' + type + '".';
if (type.toLowerCase() === 'number') {
value = value.trim();
if (!value || isNaN(Number(value)))
return 'Cannot type text into input[type=number].';
}
if (input.disabled)
return 'Cannot fill a disabled input.';
if (input.readOnly)
return 'Cannot fill a readonly input.';
input.select();
input.focus();
} else if (element.nodeName.toLowerCase() === 'textarea') {
const textarea = element as HTMLTextAreaElement;
if (textarea.disabled)
return 'Cannot fill a disabled textarea.';
if (textarea.readOnly)
return 'Cannot fill a readonly textarea.';
textarea.selectionStart = 0;
textarea.selectionEnd = textarea.value.length;
textarea.focus();
} else if (element.isContentEditable) {
const range = element.ownerDocument.createRange();
range.selectNodeContents(element);
const selection = element.ownerDocument.defaultView.getSelection();
if (!selection)
return 'Element belongs to invisible iframe.';
selection.removeAllRanges();
selection.addRange(range);
element.focus();
} else {
return 'Element is not an <input>, <textarea> or [contenteditable] element.';
}
return false;
}, value);
const error = await this._evaluateInUtility((injected, node, value) => injected.fill(node, value), value);
if (error)
throw new Error(error);
if (value)
Expand All @@ -372,7 +287,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}

async setInputFiles(...files: (string | types.FilePayload)[]) {
const multiple = await this._evaluateInUtility((node: Node) => {
const multiple = await this._evaluateInUtility((injected: Injected, node: Node) => {
if (node.nodeType !== Node.ELEMENT_NODE || (node as Element).tagName !== 'INPUT')
throw new Error('Node is not an HTMLInputElement');
const input = node as HTMLInputElement;
Expand All @@ -394,7 +309,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}

async focus() {
const errorMessage = await this._evaluateInUtility((element: Node) => {
const errorMessage = await this._evaluateInUtility((injected: Injected, element: Node) => {
if (!(element as any)['focus'])
return 'Node is not an HTML or SVG element.';
(element as HTMLElement|SVGElement).focus();
Expand Down Expand Up @@ -423,35 +338,10 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}

private async _setChecked(state: boolean, options?: types.WaitForOptions) {
const isCheckboxChecked = async (): Promise<boolean> => {
return this._evaluateInUtility((node: Node) => {
if (node.nodeType !== Node.ELEMENT_NODE)
throw new Error('Not a checkbox or radio button');

let element: Element | undefined = node as Element;
if (element.getAttribute('role') === 'checkbox')
return element.getAttribute('aria-checked') === 'true';

if (element.nodeName === 'LABEL') {
const forId = element.getAttribute('for');
if (forId && element.ownerDocument)
element = element.ownerDocument.querySelector(`input[id="${forId}"]`) || undefined;
else
element = element.querySelector('input[type=checkbox],input[type=radio]') || undefined;
}
if (element && element.nodeName === 'INPUT') {
const type = element.getAttribute('type');
if (type && (type.toLowerCase() === 'checkbox' || type.toLowerCase() === 'radio'))
return (element as HTMLInputElement).checked;
}
throw new Error('Not a checkbox');
});
};

if (await isCheckboxChecked() === state)
if (await this._evaluateInUtility((injected, node) => injected.isCheckboxChecked(node)) === state)
return;
await this.click(options);
if (await isCheckboxChecked() !== state)
if (await this._evaluateInUtility((injected, node) => injected.isCheckboxChecked(node)) !== state)
throw new Error('Unable to click checkbox');
}

Expand Down Expand Up @@ -488,29 +378,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}

async _waitForStablePosition(options: types.TimeoutOptions = {}): Promise<void> {
const context = await this._context.frame._utilityContext();
const stablePromise = context.evaluate((injected: Injected, node: Node, timeout: number) => {
if (!node.isConnected)
throw new Error('Element is not attached to the DOM');
const element = node.nodeType === Node.ELEMENT_NODE ? (node as Element) : node.parentElement;
if (!element)
throw new Error('Element is not attached to the DOM');

let lastRect: types.Rect | undefined;
let counter = 0;
return injected.poll('raf', undefined, timeout, () => {
// First raf happens in the same animation frame as evaluation, so it does not produce
// any client rect difference compared to synchronous call. We skip the synchronous call
// and only force layout during actual rafs as a small optimisation.
if (++counter === 1)
return false;
const clientRect = element.getBoundingClientRect();
const rect = { x: clientRect.top, y: clientRect.left, width: clientRect.width, height: clientRect.height };
const isStable = lastRect && rect.x === lastRect.x && rect.y === lastRect.y && rect.width === lastRect.width && rect.height === lastRect.height;
lastRect = rect;
return isStable;
});
}, await context._injected(), this, options.timeout || 0);
const stablePromise = this._evaluateInUtility((injected, node, timeout) => {
return injected.waitForStablePosition(node, timeout);
}, options.timeout || 0);
await helper.waitWithTimeout(stablePromise, 'element to stop moving', options.timeout || 0);
}

Expand All @@ -524,18 +394,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
// Translate from viewport coordinates to frame coordinates.
point = { x: point.x - box.x, y: point.y - box.y };
}
const context = await this._context.frame._utilityContext();
const hitTargetPromise = context.evaluate((injected: Injected, node: Node, timeout: number, point: types.Point) => {
const element = node.nodeType === Node.ELEMENT_NODE ? (node as Element) : node.parentElement;
if (!element)
throw new Error('Element is not attached to the DOM');
return injected.poll('raf', undefined, timeout, () => {
let hitElement = injected.utils.deepElementFromPoint(document, point.x, point.y);
while (hitElement && hitElement !== element)
hitElement = injected.utils.parentElementOrShadowHost(hitElement);
return hitElement === element;
});
}, await context._injected(), this, options.timeout || 0, point);
const hitTargetPromise = this._evaluateInUtility((injected, node, timeout, point) => {
return injected.waitForHitTargetAt(node, timeout, point);
}, options.timeout || 0, point);
await helper.waitWithTimeout(hitTargetPromise, 'element to receive mouse events', options.timeout || 0);
}
}
Expand Down
Loading