mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
764 lines
22 KiB
TypeScript
764 lines
22 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2023 Google Inc.
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import expect from 'expect';
|
|
import {TimeoutError} from 'puppeteer-core';
|
|
import {
|
|
Locator,
|
|
LocatorEvent,
|
|
} from 'puppeteer-core/internal/api/locators/locators.js';
|
|
import sinon from 'sinon';
|
|
|
|
import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
|
|
|
|
describe('Locator', function () {
|
|
setupTestBrowserHooks();
|
|
|
|
it('should work with a frame', async () => {
|
|
const {page} = await getTestState();
|
|
|
|
await page.setViewport({width: 500, height: 500});
|
|
await page.setContent(`
|
|
<button onclick="this.innerText = 'clicked';">test</button>
|
|
`);
|
|
let willClick = false;
|
|
await page
|
|
.mainFrame()
|
|
.locator('button')
|
|
.on(LocatorEvent.Action, () => {
|
|
willClick = true;
|
|
})
|
|
.click();
|
|
using button = await page.$('button');
|
|
const text = await button?.evaluate(el => {
|
|
return el.innerText;
|
|
});
|
|
expect(text).toBe('clicked');
|
|
expect(willClick).toBe(true);
|
|
});
|
|
|
|
it('should work without preconditions', async () => {
|
|
const {page} = await getTestState();
|
|
|
|
await page.setViewport({width: 500, height: 500});
|
|
await page.setContent(`
|
|
<button onclick="this.innerText = 'clicked';">test</button>
|
|
`);
|
|
let willClick = false;
|
|
await page
|
|
.locator('button')
|
|
.setEnsureElementIsInTheViewport(false)
|
|
.setTimeout(0)
|
|
.setVisibility(null)
|
|
.setWaitForEnabled(false)
|
|
.setWaitForStableBoundingBox(false)
|
|
.on(LocatorEvent.Action, () => {
|
|
willClick = true;
|
|
})
|
|
.click();
|
|
using button = await page.$('button');
|
|
const text = await button?.evaluate(el => {
|
|
return el.innerText;
|
|
});
|
|
expect(text).toBe('clicked');
|
|
expect(willClick).toBe(true);
|
|
});
|
|
|
|
describe('Locator.click', function () {
|
|
it('should work', async () => {
|
|
const {page} = await getTestState();
|
|
|
|
await page.setViewport({width: 500, height: 500});
|
|
await page.setContent(`
|
|
<button onclick="this.innerText = 'clicked';">test</button>
|
|
`);
|
|
let willClick = false;
|
|
await page
|
|
.locator('button')
|
|
.on(LocatorEvent.Action, () => {
|
|
willClick = true;
|
|
})
|
|
.click();
|
|
using button = await page.$('button');
|
|
const text = await button?.evaluate(el => {
|
|
return el.innerText;
|
|
});
|
|
expect(text).toBe('clicked');
|
|
expect(willClick).toBe(true);
|
|
});
|
|
|
|
it('should work for multiple selectors', async () => {
|
|
const {page} = await getTestState();
|
|
|
|
await page.setViewport({width: 500, height: 500});
|
|
await page.setContent(`
|
|
<button onclick="this.innerText = 'clicked';">test</button>
|
|
`);
|
|
let clicked = false;
|
|
await page
|
|
.locator('::-p-text(test), ::-p-xpath(/button)')
|
|
.on(LocatorEvent.Action, () => {
|
|
clicked = true;
|
|
})
|
|
.click();
|
|
using button = await page.$('button');
|
|
const text = await button?.evaluate(el => {
|
|
return el.innerText;
|
|
});
|
|
expect(text).toBe('clicked');
|
|
expect(clicked).toBe(true);
|
|
});
|
|
|
|
it('should work if the element is out of viewport', async () => {
|
|
const {page} = await getTestState();
|
|
|
|
await page.setViewport({width: 500, height: 500});
|
|
await page.setContent(`
|
|
<button style="margin-top: 600px;" onclick="this.innerText = 'clicked';">test</button>
|
|
`);
|
|
await page.locator('button').click();
|
|
using button = await page.$('button');
|
|
const text = await button?.evaluate(el => {
|
|
return el.innerText;
|
|
});
|
|
expect(text).toBe('clicked');
|
|
});
|
|
|
|
it('should work if the element becomes visible later', async () => {
|
|
const {page} = await getTestState();
|
|
|
|
await page.setViewport({width: 500, height: 500});
|
|
await page.setContent(`
|
|
<button style="display: none;" onclick="this.innerText = 'clicked';">test</button>
|
|
`);
|
|
using button = await page.$('button');
|
|
const result = page
|
|
.locator('button')
|
|
.click()
|
|
.catch(err => {
|
|
return err;
|
|
});
|
|
expect(
|
|
await button?.evaluate(el => {
|
|
return el.innerText;
|
|
})
|
|
).toBe('test');
|
|
await button?.evaluate(el => {
|
|
el.style.display = 'block';
|
|
});
|
|
const maybeError = await result;
|
|
if (maybeError instanceof Error) {
|
|
throw maybeError;
|
|
}
|
|
expect(
|
|
await button?.evaluate(el => {
|
|
return el.innerText;
|
|
})
|
|
).toBe('clicked');
|
|
});
|
|
|
|
it('should work if the element becomes enabled later', async () => {
|
|
const {page} = await getTestState();
|
|
|
|
await page.setViewport({width: 500, height: 500});
|
|
await page.setContent(`
|
|
<button disabled onclick="this.innerText = 'clicked';">test</button>
|
|
`);
|
|
using button = await page.$('button');
|
|
const result = page.locator('button').click();
|
|
expect(
|
|
await button?.evaluate(el => {
|
|
return el.innerText;
|
|
})
|
|
).toBe('test');
|
|
await button?.evaluate(el => {
|
|
el.disabled = false;
|
|
});
|
|
await result;
|
|
expect(
|
|
await button?.evaluate(el => {
|
|
return el.innerText;
|
|
})
|
|
).toBe('clicked');
|
|
});
|
|
|
|
it('should work if multiple conditions are satisfied later', async () => {
|
|
const {page} = await getTestState();
|
|
|
|
await page.setViewport({width: 500, height: 500});
|
|
await page.setContent(`
|
|
<button style="margin-top: 600px;" style="display: none;" disabled onclick="this.innerText = 'clicked';">test</button>
|
|
`);
|
|
using button = await page.$('button');
|
|
const result = page.locator('button').click();
|
|
expect(
|
|
await button?.evaluate(el => {
|
|
return el.innerText;
|
|
})
|
|
).toBe('test');
|
|
await button?.evaluate(el => {
|
|
el.disabled = false;
|
|
el.style.display = 'block';
|
|
});
|
|
await result;
|
|
expect(
|
|
await button?.evaluate(el => {
|
|
return el.innerText;
|
|
})
|
|
).toBe('clicked');
|
|
});
|
|
|
|
it('should time out', async () => {
|
|
const clock = sinon.useFakeTimers({
|
|
shouldClearNativeTimers: true,
|
|
shouldAdvanceTime: true,
|
|
});
|
|
try {
|
|
const {page} = await getTestState();
|
|
|
|
page.setDefaultTimeout(5000);
|
|
await page.setViewport({width: 500, height: 500});
|
|
await page.setContent(`
|
|
<button style="display: none;" onclick="this.innerText = 'clicked';">test</button>
|
|
`);
|
|
const result = page.locator('button').click();
|
|
clock.tick(5100);
|
|
await expect(result).rejects.toEqual(
|
|
new TimeoutError('Timed out after waiting 5000ms')
|
|
);
|
|
} finally {
|
|
clock.restore();
|
|
}
|
|
});
|
|
|
|
it('should retry clicks on errors', async () => {
|
|
const {page} = await getTestState();
|
|
const clock = sinon.useFakeTimers({
|
|
shouldClearNativeTimers: true,
|
|
shouldAdvanceTime: true,
|
|
});
|
|
try {
|
|
page.setDefaultTimeout(5000);
|
|
await page.setViewport({width: 500, height: 500});
|
|
await page.setContent(`
|
|
<button style="display: none;" onclick="this.innerText = 'clicked';">test</button>
|
|
`);
|
|
const result = page.locator('button').click();
|
|
clock.tick(5100);
|
|
await expect(result).rejects.toEqual(
|
|
new TimeoutError('Timed out after waiting 5000ms')
|
|
);
|
|
} finally {
|
|
clock.restore();
|
|
}
|
|
});
|
|
|
|
it('can be aborted', async () => {
|
|
const {page} = await getTestState();
|
|
const clock = sinon.useFakeTimers({
|
|
shouldClearNativeTimers: true,
|
|
shouldAdvanceTime: true,
|
|
});
|
|
try {
|
|
page.setDefaultTimeout(5000);
|
|
|
|
await page.setViewport({width: 500, height: 500});
|
|
await page.setContent(`
|
|
<button style="display: none;" onclick="this.innerText = 'clicked';">test</button>
|
|
`);
|
|
const abortController = new AbortController();
|
|
const result = page.locator('button').click({
|
|
signal: abortController.signal,
|
|
});
|
|
clock.tick(2000);
|
|
abortController.abort();
|
|
await expect(result).rejects.toThrow(/aborted/);
|
|
} finally {
|
|
clock.restore();
|
|
}
|
|
});
|
|
|
|
it('should work with a OOPIF', async () => {
|
|
const {page} = await getTestState();
|
|
|
|
await page.setViewport({width: 500, height: 500});
|
|
await page.setContent(`
|
|
<iframe src="data:text/html,<button onclick="this.innerText = 'clicked';">test</button>"></iframe>
|
|
`);
|
|
const frame = await page.waitForFrame(frame => {
|
|
return frame.url().startsWith('data');
|
|
});
|
|
let willClick = false;
|
|
await frame
|
|
.locator('button')
|
|
.on(LocatorEvent.Action, () => {
|
|
willClick = true;
|
|
})
|
|
.click();
|
|
using button = await frame.$('button');
|
|
const text = await button?.evaluate(el => {
|
|
return el.innerText;
|
|
});
|
|
expect(text).toBe('clicked');
|
|
expect(willClick).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Locator.hover', function () {
|
|
it('should work', async () => {
|
|
const {page} = await getTestState();
|
|
|
|
await page.setViewport({width: 500, height: 500});
|
|
await page.setContent(`
|
|
<button onmouseenter="this.innerText = 'hovered';">test</button>
|
|
`);
|
|
let hovered = false;
|
|
await page
|
|
.locator('button')
|
|
.on(LocatorEvent.Action, () => {
|
|
hovered = true;
|
|
})
|
|
.hover();
|
|
using button = await page.$('button');
|
|
const text = await button?.evaluate(el => {
|
|
return el.innerText;
|
|
});
|
|
expect(text).toBe('hovered');
|
|
expect(hovered).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Locator.scroll', function () {
|
|
it('should work', async () => {
|
|
const {page} = await getTestState();
|
|
|
|
await page.setViewport({width: 500, height: 500});
|
|
await page.setContent(`
|
|
<div style="height: 500px; width: 500px; overflow: scroll;">
|
|
<div style="height: 1000px; width: 1000px;">test</div>
|
|
</div>
|
|
`);
|
|
let scrolled = false;
|
|
await page
|
|
.locator('div')
|
|
.on(LocatorEvent.Action, () => {
|
|
scrolled = true;
|
|
})
|
|
.scroll({
|
|
scrollTop: 500,
|
|
scrollLeft: 500,
|
|
});
|
|
using scrollable = await page.$('div');
|
|
const scroll = await scrollable?.evaluate(el => {
|
|
return el.scrollTop + ' ' + el.scrollLeft;
|
|
});
|
|
expect(scroll).toBe('500 500');
|
|
expect(scrolled).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Locator.fill', function () {
|
|
it('should work for textarea', async () => {
|
|
const {page} = await getTestState();
|
|
|
|
await page.setContent(`
|
|
<textarea></textarea>
|
|
`);
|
|
let filled = false;
|
|
await page
|
|
.locator('textarea')
|
|
.on(LocatorEvent.Action, () => {
|
|
filled = true;
|
|
})
|
|
.fill('test');
|
|
expect(
|
|
await page.evaluate(() => {
|
|
return document.querySelector('textarea')?.value === 'test';
|
|
})
|
|
).toBe(true);
|
|
expect(filled).toBe(true);
|
|
});
|
|
|
|
it('should work for selects', async () => {
|
|
const {page} = await getTestState();
|
|
|
|
await page.setContent(`
|
|
<select>
|
|
<option value="value1">Option 1</option>
|
|
<option value="value2">Option 2</option>
|
|
<select>
|
|
`);
|
|
let filled = false;
|
|
await page
|
|
.locator('select')
|
|
.on(LocatorEvent.Action, () => {
|
|
filled = true;
|
|
})
|
|
.fill('value2');
|
|
expect(
|
|
await page.evaluate(() => {
|
|
return document.querySelector('select')?.value === 'value2';
|
|
})
|
|
).toBe(true);
|
|
expect(filled).toBe(true);
|
|
});
|
|
|
|
it('should work for inputs', async () => {
|
|
const {page} = await getTestState();
|
|
await page.setContent(`
|
|
<input>
|
|
`);
|
|
await page.locator('input').fill('test');
|
|
expect(
|
|
await page.evaluate(() => {
|
|
return document.querySelector('input')?.value === 'test';
|
|
})
|
|
).toBe(true);
|
|
});
|
|
|
|
it('should work if the input becomes enabled later', async () => {
|
|
const {page} = await getTestState();
|
|
|
|
await page.setContent(`
|
|
<input disabled>
|
|
`);
|
|
using input = await page.$('input');
|
|
const result = page.locator('input').fill('test');
|
|
expect(
|
|
await input?.evaluate(el => {
|
|
return el.value;
|
|
})
|
|
).toBe('');
|
|
await input?.evaluate(el => {
|
|
el.disabled = false;
|
|
});
|
|
await result;
|
|
expect(
|
|
await input?.evaluate(el => {
|
|
return el.value;
|
|
})
|
|
).toBe('test');
|
|
});
|
|
|
|
it('should work for contenteditable', async () => {
|
|
const {page} = await getTestState();
|
|
await page.setContent(`
|
|
<div contenteditable="true">
|
|
`);
|
|
await page.locator('div').fill('test');
|
|
expect(
|
|
await page.evaluate(() => {
|
|
return document.querySelector('div')?.innerText === 'test';
|
|
})
|
|
).toBe(true);
|
|
});
|
|
|
|
it('should work for pre-filled inputs', async () => {
|
|
const {page} = await getTestState();
|
|
await page.setContent(`
|
|
<input value="te">
|
|
`);
|
|
await page.locator('input').fill('test');
|
|
expect(
|
|
await page.evaluate(() => {
|
|
return document.querySelector('input')?.value === 'test';
|
|
})
|
|
).toBe(true);
|
|
});
|
|
|
|
it('should override pre-filled inputs', async () => {
|
|
const {page} = await getTestState();
|
|
await page.setContent(`
|
|
<input value="wrong prefix">
|
|
`);
|
|
await page.locator('input').fill('test');
|
|
expect(
|
|
await page.evaluate(() => {
|
|
return document.querySelector('input')?.value === 'test';
|
|
})
|
|
).toBe(true);
|
|
});
|
|
|
|
it('should work for non-text inputs', async () => {
|
|
const {page} = await getTestState();
|
|
await page.setContent(`
|
|
<input type="color">
|
|
`);
|
|
await page.locator('input').fill('#333333');
|
|
expect(
|
|
await page.evaluate(() => {
|
|
return document.querySelector('input')?.value === '#333333';
|
|
})
|
|
).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Locator.race', () => {
|
|
it('races multiple locators', async () => {
|
|
const {page} = await getTestState();
|
|
|
|
await page.setViewport({width: 500, height: 500});
|
|
await page.setContent(`
|
|
<button onclick="window.count++;">test</button>
|
|
`);
|
|
await page.evaluate(() => {
|
|
// @ts-expect-error different context.
|
|
window.count = 0;
|
|
});
|
|
await Locator.race([
|
|
page.locator('button'),
|
|
page.locator('button'),
|
|
]).click();
|
|
const count = await page.evaluate(() => {
|
|
// @ts-expect-error different context.
|
|
return globalThis.count;
|
|
});
|
|
expect(count).toBe(1);
|
|
});
|
|
|
|
it('can be aborted', async () => {
|
|
const {page} = await getTestState();
|
|
const clock = sinon.useFakeTimers({
|
|
shouldClearNativeTimers: true,
|
|
shouldAdvanceTime: true,
|
|
});
|
|
try {
|
|
await page.setViewport({width: 500, height: 500});
|
|
await page.setContent(`
|
|
<button style="display: none;" onclick="this.innerText = 'clicked';">test</button>
|
|
`);
|
|
const abortController = new AbortController();
|
|
const result = Locator.race([
|
|
page.locator('button'),
|
|
page.locator('button'),
|
|
])
|
|
.setTimeout(5000)
|
|
.click({
|
|
signal: abortController.signal,
|
|
});
|
|
clock.tick(2000);
|
|
abortController.abort();
|
|
await expect(result).rejects.toThrow(/aborted/);
|
|
} finally {
|
|
clock.restore();
|
|
}
|
|
});
|
|
|
|
it('should time out when all locators do not match', async () => {
|
|
const clock = sinon.useFakeTimers({
|
|
shouldClearNativeTimers: true,
|
|
shouldAdvanceTime: true,
|
|
});
|
|
try {
|
|
const {page} = await getTestState();
|
|
await page.setContent(`<button>test</button>`);
|
|
const result = Locator.race([
|
|
page.locator('not-found'),
|
|
page.locator('not-found'),
|
|
])
|
|
.setTimeout(5000)
|
|
.click();
|
|
clock.tick(5100);
|
|
await expect(result).rejects.toEqual(
|
|
new TimeoutError('Timed out after waiting 5000ms')
|
|
);
|
|
} finally {
|
|
clock.restore();
|
|
}
|
|
});
|
|
|
|
it('should not time out when one of the locators matches', async () => {
|
|
const {page} = await getTestState();
|
|
await page.setContent(`<button>test</button>`);
|
|
const result = Locator.race([
|
|
page.locator('not-found'),
|
|
page.locator('button'),
|
|
]).click();
|
|
await expect(result).resolves.toEqual(undefined);
|
|
});
|
|
});
|
|
|
|
describe('Locator.prototype.map', () => {
|
|
it('should work', async () => {
|
|
const {page} = await getTestState();
|
|
await page.setContent(`<div>test</div>`);
|
|
await expect(
|
|
page
|
|
.locator('::-p-text(test)')
|
|
.map(element => {
|
|
return element.getAttribute('clickable');
|
|
})
|
|
.wait()
|
|
).resolves.toEqual(null);
|
|
await page.evaluate(() => {
|
|
document.querySelector('div')?.setAttribute('clickable', 'true');
|
|
});
|
|
await expect(
|
|
page
|
|
.locator('::-p-text(test)')
|
|
.map(element => {
|
|
return element.getAttribute('clickable');
|
|
})
|
|
.wait()
|
|
).resolves.toEqual('true');
|
|
});
|
|
it('should work with throws', async () => {
|
|
const {page} = await getTestState();
|
|
await page.setContent(`<div>test</div>`);
|
|
const result = page
|
|
.locator('::-p-text(test)')
|
|
.map(element => {
|
|
const clickable = element.getAttribute('clickable');
|
|
if (!clickable) {
|
|
throw new Error('Missing `clickable` as an attribute');
|
|
}
|
|
return clickable;
|
|
})
|
|
.wait();
|
|
await page.evaluate(() => {
|
|
document.querySelector('div')?.setAttribute('clickable', 'true');
|
|
});
|
|
await expect(result).resolves.toEqual('true');
|
|
});
|
|
it('should work with expect', async () => {
|
|
const {page} = await getTestState();
|
|
await page.setContent(`<div>test</div>`);
|
|
const result = page
|
|
.locator('::-p-text(test)')
|
|
.filter(element => {
|
|
return element.getAttribute('clickable') !== null;
|
|
})
|
|
.map(element => {
|
|
return element.getAttribute('clickable');
|
|
})
|
|
.wait();
|
|
await page.evaluate(() => {
|
|
document.querySelector('div')?.setAttribute('clickable', 'true');
|
|
});
|
|
await expect(result).resolves.toEqual('true');
|
|
});
|
|
});
|
|
|
|
describe('Locator.prototype.filter', () => {
|
|
it('should resolve as soon as the predicate matches', async () => {
|
|
const clock = sinon.useFakeTimers({
|
|
shouldClearNativeTimers: true,
|
|
shouldAdvanceTime: true,
|
|
});
|
|
try {
|
|
const {page} = await getTestState();
|
|
await page.setContent(`<div>test</div>`);
|
|
const result = page
|
|
.locator('::-p-text(test)')
|
|
.setTimeout(5000)
|
|
.filter(async element => {
|
|
return element.getAttribute('clickable') === 'true';
|
|
})
|
|
.filter(element => {
|
|
return element.getAttribute('clickable') === 'true';
|
|
})
|
|
.hover();
|
|
clock.tick(2000);
|
|
await page.evaluate(() => {
|
|
document.querySelector('div')?.setAttribute('clickable', 'true');
|
|
});
|
|
clock.restore();
|
|
await expect(result).resolves.toEqual(undefined);
|
|
} finally {
|
|
clock.restore();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Locator.prototype.wait', () => {
|
|
it('should work', async () => {
|
|
const {page} = await getTestState();
|
|
void page.setContent(`
|
|
<script>
|
|
setTimeout(() => {
|
|
const element = document.createElement("div");
|
|
element.innerText = "test2"
|
|
document.body.append(element);
|
|
}, 50);
|
|
</script>
|
|
`);
|
|
// This shouldn't throw.
|
|
await page.locator('div').wait();
|
|
});
|
|
});
|
|
|
|
describe('Locator.prototype.waitHandle', () => {
|
|
it('should work', async () => {
|
|
const {page} = await getTestState();
|
|
void page.setContent(`
|
|
<script>
|
|
setTimeout(() => {
|
|
const element = document.createElement("div");
|
|
element.innerText = "test2"
|
|
document.body.append(element);
|
|
}, 50);
|
|
</script>
|
|
`);
|
|
await expect(page.locator('div').waitHandle()).resolves.toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('Locator.prototype.clone', () => {
|
|
it('should work', async () => {
|
|
const {page} = await getTestState();
|
|
const locator = page.locator('div');
|
|
const clone = locator.clone();
|
|
expect(locator).not.toStrictEqual(clone);
|
|
});
|
|
it('should work internally with delegated locators', async () => {
|
|
const {page} = await getTestState();
|
|
const locator = page.locator('div');
|
|
const delegatedLocators = [
|
|
locator.map(div => {
|
|
return div.textContent;
|
|
}),
|
|
locator.filter(div => {
|
|
return div.textContent?.length === 0;
|
|
}),
|
|
];
|
|
for (let delegatedLocator of delegatedLocators) {
|
|
delegatedLocator = delegatedLocator.setTimeout(500);
|
|
expect(delegatedLocator.timeout).not.toStrictEqual(locator.timeout);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('FunctionLocator', () => {
|
|
it('should work', async () => {
|
|
const {page} = await getTestState();
|
|
const result = page
|
|
.locator(() => {
|
|
return new Promise<boolean>(resolve => {
|
|
return setTimeout(() => {
|
|
return resolve(true);
|
|
}, 100);
|
|
});
|
|
})
|
|
.wait();
|
|
await expect(result).resolves.toEqual(true);
|
|
});
|
|
it('should work with actions', async () => {
|
|
const {page} = await getTestState();
|
|
await page.setContent(`<div onclick="window.clicked = true">test</div>`);
|
|
await page
|
|
.locator(() => {
|
|
return document.getElementsByTagName('div')[0] as HTMLDivElement;
|
|
})
|
|
.click();
|
|
await expect(
|
|
page.evaluate(() => {
|
|
return (window as unknown as {clicked: boolean}).clicked;
|
|
})
|
|
).resolves.toEqual(true);
|
|
});
|
|
});
|
|
});
|