2020-10-05 06:25:55 +00:00
|
|
|
/**
|
2024-01-03 10:11:33 +00:00
|
|
|
* @license
|
|
|
|
* Copyright 2020 Google Inc.
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
2020-10-05 06:25:55 +00:00
|
|
|
*/
|
|
|
|
|
2023-02-15 23:09:31 +00:00
|
|
|
import assert from 'assert';
|
|
|
|
|
2020-10-05 06:25:55 +00:00
|
|
|
import expect from 'expect';
|
2023-02-15 23:09:31 +00:00
|
|
|
import {TimeoutError} from 'puppeteer';
|
|
|
|
import type {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js';
|
|
|
|
|
2023-07-03 12:01:29 +00:00
|
|
|
import {getTestState, setupTestBrowserHooks} from './mocha-utils.js';
|
2023-04-25 13:02:25 +00:00
|
|
|
import {attachFrame, detachFrame} from './utils.js';
|
2020-10-05 06:25:55 +00:00
|
|
|
|
2022-09-08 10:32:39 +00:00
|
|
|
describe('AriaQueryHandler', () => {
|
2023-07-03 12:01:29 +00:00
|
|
|
setupTestBrowserHooks();
|
|
|
|
|
2020-10-05 06:25:55 +00:00
|
|
|
describe('parseAriaSelector', () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
it('should find button', async () => {
|
|
|
|
const {page} = await getTestState();
|
2020-10-05 06:25:55 +00:00
|
|
|
await page.setContent(
|
|
|
|
'<button id="btn" role="button"> Submit button and some spaces </button>'
|
|
|
|
);
|
2022-06-15 10:09:22 +00:00
|
|
|
const expectFound = async (button: ElementHandle | null) => {
|
|
|
|
assert(button);
|
|
|
|
const id = await button.evaluate((button: Element) => {
|
|
|
|
return button.id;
|
|
|
|
});
|
2020-10-05 06:25:55 +00:00
|
|
|
expect(id).toBe('btn');
|
|
|
|
};
|
2023-08-30 10:02:59 +00:00
|
|
|
{
|
|
|
|
using button = await page.$(
|
|
|
|
'aria/Submit button and some spaces[role="button"]'
|
|
|
|
);
|
|
|
|
await expectFound(button);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
using button = await page.$(
|
|
|
|
"aria/Submit button and some spaces[role='button']"
|
|
|
|
);
|
|
|
|
await expectFound(button);
|
|
|
|
}
|
|
|
|
using button = await page.$(
|
2020-10-05 06:25:55 +00:00
|
|
|
'aria/ Submit button and some spaces[role="button"]'
|
|
|
|
);
|
|
|
|
await expectFound(button);
|
2023-08-30 10:02:59 +00:00
|
|
|
{
|
|
|
|
using button = await page.$(
|
|
|
|
'aria/Submit button and some spaces [role="button"]'
|
|
|
|
);
|
|
|
|
await expectFound(button);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
using button = await page.$(
|
|
|
|
'aria/Submit button and some spaces [ role = "button" ] '
|
|
|
|
);
|
|
|
|
await expectFound(button);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
using button = await page.$(
|
|
|
|
'aria/[role="button"]Submit button and some spaces'
|
|
|
|
);
|
|
|
|
await expectFound(button);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
using button = await page.$(
|
|
|
|
'aria/Submit button [role="button"]and some spaces'
|
|
|
|
);
|
|
|
|
await expectFound(button);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
using button = await page.$(
|
|
|
|
'aria/[name=" Submit button and some spaces"][role="button"]'
|
|
|
|
);
|
|
|
|
await expectFound(button);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
using button = await page.$(
|
|
|
|
"aria/[name=' Submit button and some spaces'][role='button']"
|
|
|
|
);
|
|
|
|
await expectFound(button);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
using button = await page.$(
|
|
|
|
'aria/ignored[name="Submit button and some spaces"][role="button"]'
|
|
|
|
);
|
|
|
|
await expectFound(button);
|
|
|
|
await expect(page.$('aria/smth[smth="true"]')).rejects.toThrow(
|
|
|
|
'Unknown aria attribute "smth" in selector'
|
|
|
|
);
|
|
|
|
}
|
2020-10-05 06:25:55 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('queryOne', () => {
|
|
|
|
it('should find button by role', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await getTestState();
|
2020-10-05 06:25:55 +00:00
|
|
|
await page.setContent(
|
|
|
|
'<div id="div"><button id="btn" role="button">Submit</button></div>'
|
|
|
|
);
|
2023-08-30 10:02:59 +00:00
|
|
|
using button = (await page.$(
|
2022-07-06 07:05:37 +00:00
|
|
|
'aria/[role="button"]'
|
|
|
|
)) as ElementHandle<HTMLButtonElement>;
|
|
|
|
const id = await button!.evaluate(button => {
|
2022-06-15 10:09:22 +00:00
|
|
|
return button.id;
|
|
|
|
});
|
2020-10-05 06:25:55 +00:00
|
|
|
expect(id).toBe('btn');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should find button by name and role', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await getTestState();
|
2020-10-05 06:25:55 +00:00
|
|
|
await page.setContent(
|
|
|
|
'<div id="div"><button id="btn" role="button">Submit</button></div>'
|
|
|
|
);
|
2023-08-30 10:02:59 +00:00
|
|
|
using button = (await page.$(
|
2022-07-06 07:05:37 +00:00
|
|
|
'aria/Submit[role="button"]'
|
|
|
|
)) as ElementHandle<HTMLButtonElement>;
|
|
|
|
const id = await button!.evaluate(button => {
|
2022-06-15 10:09:22 +00:00
|
|
|
return button.id;
|
|
|
|
});
|
2020-10-05 06:25:55 +00:00
|
|
|
expect(id).toBe('btn');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should find first matching element', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await getTestState();
|
2020-10-05 06:25:55 +00:00
|
|
|
await page.setContent(
|
|
|
|
`
|
|
|
|
<div role="menu" id="mnu1" aria-label="menu div"></div>
|
|
|
|
<div role="menu" id="mnu2" aria-label="menu div"></div>
|
|
|
|
`
|
|
|
|
);
|
2023-08-30 10:02:59 +00:00
|
|
|
using div = (await page.$(
|
2022-07-06 07:05:37 +00:00
|
|
|
'aria/menu div'
|
|
|
|
)) as ElementHandle<HTMLDivElement>;
|
|
|
|
const id = await div!.evaluate(div => {
|
2022-06-15 10:09:22 +00:00
|
|
|
return div.id;
|
|
|
|
});
|
2020-10-05 06:25:55 +00:00
|
|
|
expect(id).toBe('mnu1');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should find by name', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await getTestState();
|
2020-10-05 06:25:55 +00:00
|
|
|
await page.setContent(
|
|
|
|
`
|
|
|
|
<div role="menu" id="mnu1" aria-label="menu-label1">menu div</div>
|
|
|
|
<div role="menu" id="mnu2" aria-label="menu-label2">menu div</div>
|
|
|
|
`
|
|
|
|
);
|
2023-08-30 10:02:59 +00:00
|
|
|
using menu = (await page.$(
|
2022-07-06 07:05:37 +00:00
|
|
|
'aria/menu-label1'
|
|
|
|
)) as ElementHandle<HTMLDivElement>;
|
|
|
|
const id = await menu!.evaluate(div => {
|
2022-06-15 10:09:22 +00:00
|
|
|
return div.id;
|
|
|
|
});
|
2020-10-05 06:25:55 +00:00
|
|
|
expect(id).toBe('mnu1');
|
|
|
|
});
|
|
|
|
|
2024-01-24 17:13:55 +00:00
|
|
|
it('should find 2nd element by name', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await getTestState();
|
2020-10-05 06:25:55 +00:00
|
|
|
await page.setContent(
|
|
|
|
`
|
|
|
|
<div role="menu" id="mnu1" aria-label="menu-label1">menu div</div>
|
|
|
|
<div role="menu" id="mnu2" aria-label="menu-label2">menu div</div>
|
|
|
|
`
|
|
|
|
);
|
2023-08-30 10:02:59 +00:00
|
|
|
using menu = (await page.$(
|
2022-07-06 07:05:37 +00:00
|
|
|
'aria/menu-label2'
|
|
|
|
)) as ElementHandle<HTMLDivElement>;
|
|
|
|
const id = await menu!.evaluate(div => {
|
2022-06-15 10:09:22 +00:00
|
|
|
return div.id;
|
|
|
|
});
|
2020-10-05 06:25:55 +00:00
|
|
|
expect(id).toBe('mnu2');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('queryAll', () => {
|
|
|
|
it('should find menu by name', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await getTestState();
|
2020-10-05 06:25:55 +00:00
|
|
|
await page.setContent(
|
|
|
|
`
|
|
|
|
<div role="menu" id="mnu1" aria-label="menu div"></div>
|
|
|
|
<div role="menu" id="mnu2" aria-label="menu div"></div>
|
|
|
|
`
|
|
|
|
);
|
2022-07-06 07:05:37 +00:00
|
|
|
const divs = (await page.$$('aria/menu div')) as Array<
|
|
|
|
ElementHandle<HTMLDivElement>
|
|
|
|
>;
|
2020-10-05 06:25:55 +00:00
|
|
|
const ids = await Promise.all(
|
2022-06-22 13:25:44 +00:00
|
|
|
divs.map(n => {
|
2022-07-06 07:05:37 +00:00
|
|
|
return n.evaluate(div => {
|
2022-06-15 10:09:22 +00:00
|
|
|
return div.id;
|
|
|
|
});
|
|
|
|
})
|
2020-10-05 06:25:55 +00:00
|
|
|
);
|
|
|
|
expect(ids.join(', ')).toBe('mnu1, mnu2');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
describe('queryAllArray', () => {
|
2023-03-24 08:35:45 +00:00
|
|
|
it('$$eval should handle many elements', async function () {
|
2023-09-01 15:13:29 +00:00
|
|
|
this.timeout(40_000);
|
2023-03-24 08:35:45 +00:00
|
|
|
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await getTestState();
|
2020-10-05 06:25:55 +00:00
|
|
|
await page.setContent('');
|
|
|
|
await page.evaluate(
|
|
|
|
`
|
|
|
|
for (var i = 0; i <= 10000; i++) {
|
|
|
|
const button = document.createElement('button');
|
|
|
|
button.textContent = i;
|
|
|
|
document.body.appendChild(button);
|
|
|
|
}
|
|
|
|
`
|
|
|
|
);
|
2022-06-22 13:25:44 +00:00
|
|
|
const sum = await page.$$eval('aria/[role="button"]', buttons => {
|
2022-06-15 10:09:22 +00:00
|
|
|
return buttons.reduce((acc, button) => {
|
|
|
|
return acc + Number(button.textContent);
|
|
|
|
}, 0);
|
|
|
|
});
|
2020-10-05 06:25:55 +00:00
|
|
|
expect(sum).toBe(50005000);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2020-10-07 08:49:11 +00:00
|
|
|
describe('waitForSelector (aria)', function () {
|
2022-06-15 10:09:22 +00:00
|
|
|
const addElement = (tag: string) => {
|
|
|
|
return document.body.appendChild(document.createElement(tag));
|
|
|
|
};
|
2020-10-07 08:49:11 +00:00
|
|
|
|
|
|
|
it('should immediately resolve promise if node exists', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page, server} = await getTestState();
|
2020-10-07 08:49:11 +00:00
|
|
|
await page.goto(server.EMPTY_PAGE);
|
|
|
|
await page.evaluate(addElement, 'button');
|
|
|
|
await page.waitForSelector('aria/[role="button"]');
|
2021-01-25 12:01:59 +00:00
|
|
|
});
|
|
|
|
|
2022-12-17 17:47:21 +00:00
|
|
|
it('should work for ElementHandle.waitForSelector', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page, server} = await getTestState();
|
2021-12-21 08:53:20 +00:00
|
|
|
await page.goto(server.EMPTY_PAGE);
|
2022-06-15 10:09:22 +00:00
|
|
|
await page.evaluate(() => {
|
|
|
|
return (document.body.innerHTML = `<div><button>test</button></div>`);
|
|
|
|
});
|
2023-08-30 10:02:59 +00:00
|
|
|
using element = (await page.$('div'))!;
|
2022-06-15 10:09:22 +00:00
|
|
|
await element!.waitForSelector('aria/test');
|
2021-12-21 08:53:20 +00:00
|
|
|
});
|
|
|
|
|
2021-01-25 12:01:59 +00:00
|
|
|
it('should persist query handler bindings across reloads', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page, server} = await getTestState();
|
2021-01-25 12:01:59 +00:00
|
|
|
await page.goto(server.EMPTY_PAGE);
|
|
|
|
await page.evaluate(addElement, 'button');
|
|
|
|
await page.waitForSelector('aria/[role="button"]');
|
|
|
|
await page.reload();
|
|
|
|
await page.evaluate(addElement, 'button');
|
|
|
|
await page.waitForSelector('aria/[role="button"]');
|
2021-02-08 18:56:04 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should persist query handler bindings across navigations', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page, server} = await getTestState();
|
2021-02-08 18:56:04 +00:00
|
|
|
|
|
|
|
// Reset page but make sure that execution context ids start with 1.
|
|
|
|
await page.goto('data:text/html,');
|
|
|
|
await page.goto(server.EMPTY_PAGE);
|
|
|
|
await page.evaluate(addElement, 'button');
|
|
|
|
await page.waitForSelector('aria/[role="button"]');
|
|
|
|
|
|
|
|
// Reset page but again make sure that execution context ids start with 1.
|
|
|
|
await page.goto('data:text/html,');
|
|
|
|
await page.goto(server.EMPTY_PAGE);
|
|
|
|
await page.evaluate(addElement, 'button');
|
|
|
|
await page.waitForSelector('aria/[role="button"]');
|
2020-10-07 08:49:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should work independently of `exposeFunction`', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page, server} = await getTestState();
|
2020-10-07 08:49:11 +00:00
|
|
|
await page.goto(server.EMPTY_PAGE);
|
2022-06-15 10:09:22 +00:00
|
|
|
await page.exposeFunction('ariaQuerySelector', (a: number, b: number) => {
|
|
|
|
return a + b;
|
|
|
|
});
|
2020-10-07 08:49:11 +00:00
|
|
|
await page.evaluate(addElement, 'button');
|
|
|
|
await page.waitForSelector('aria/[role="button"]');
|
|
|
|
const result = await page.evaluate('globalThis.ariaQuerySelector(2,8)');
|
|
|
|
expect(result).toBe(10);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should work with removed MutationObserver', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await getTestState();
|
2020-10-07 08:49:11 +00:00
|
|
|
|
2022-06-15 10:09:22 +00:00
|
|
|
await page.evaluate(() => {
|
|
|
|
// @ts-expect-error This is the point of the test.
|
|
|
|
return delete window.MutationObserver;
|
|
|
|
});
|
2020-10-07 08:49:11 +00:00
|
|
|
const [handle] = await Promise.all([
|
|
|
|
page.waitForSelector('aria/anything'),
|
|
|
|
page.setContent(`<h1>anything</h1>`),
|
|
|
|
]);
|
feat!: type inference for evaluation types (#8547)
This PR greatly improves the types within Puppeteer:
- **Almost everything** is auto-deduced.
- Parameters don't need to be specified in the function. They are deduced from the spread.
- Return types don't need to be specified. They are deduced from the function. (More on this below)
- Selections based on tag names correctly deduce element type, similar to TypeScript's mechanism for `getElementByTagName`.
- [**BREAKING CHANGE**] We've removed the ability to declare return types in type arguments for the following reasons:
1. Setting them will indubitably break auto-deduction.
2. You can just use `as ...` in TypeScript to coerce the correct type (given it makes sense).
- [**BREAKING CHANGE**] `waitFor` is officially gone.
To migrate to these changes, there are only four things you may need to change:
- If you set a return type using the `ReturnType` type parameter, remove it and use `as ...` and `HandleFor` (if necessary).
⛔ `evaluate<ReturnType>(a: number, b: number) => {...}, a, b)`
✅ `(await evaluate(a, b) => {...}, a, b)) as ReturnType`
⛔ `evaluateHandle<ReturnType>(a: number, b: number) => {...}, a, b)`
✅ `(await evaluateHandle(a, b) => {...}, a, b)) as HandleFor<ReturnType>`
- If you set any type parameters in the *parameters* of an evaluation function, remove them.
⛔ `evaluate(a: number, b: number) => {...}, a, b)`
✅ `evaluate(a, b) => {...}, a, b)`
- If you set any type parameters in the method's declaration, remove them.
⛔ `evaluate<(a: number, b: number) => void>((a, b) => {...}, a, b)`
✅ `evaluate(a, b) => {...}, a, b)`
2022-06-23 09:29:46 +00:00
|
|
|
assert(handle);
|
2020-10-07 08:49:11 +00:00
|
|
|
expect(
|
feat!: type inference for evaluation types (#8547)
This PR greatly improves the types within Puppeteer:
- **Almost everything** is auto-deduced.
- Parameters don't need to be specified in the function. They are deduced from the spread.
- Return types don't need to be specified. They are deduced from the function. (More on this below)
- Selections based on tag names correctly deduce element type, similar to TypeScript's mechanism for `getElementByTagName`.
- [**BREAKING CHANGE**] We've removed the ability to declare return types in type arguments for the following reasons:
1. Setting them will indubitably break auto-deduction.
2. You can just use `as ...` in TypeScript to coerce the correct type (given it makes sense).
- [**BREAKING CHANGE**] `waitFor` is officially gone.
To migrate to these changes, there are only four things you may need to change:
- If you set a return type using the `ReturnType` type parameter, remove it and use `as ...` and `HandleFor` (if necessary).
⛔ `evaluate<ReturnType>(a: number, b: number) => {...}, a, b)`
✅ `(await evaluate(a, b) => {...}, a, b)) as ReturnType`
⛔ `evaluateHandle<ReturnType>(a: number, b: number) => {...}, a, b)`
✅ `(await evaluateHandle(a, b) => {...}, a, b)) as HandleFor<ReturnType>`
- If you set any type parameters in the *parameters* of an evaluation function, remove them.
⛔ `evaluate(a: number, b: number) => {...}, a, b)`
✅ `evaluate(a, b) => {...}, a, b)`
- If you set any type parameters in the method's declaration, remove them.
⛔ `evaluate<(a: number, b: number) => void>((a, b) => {...}, a, b)`
✅ `evaluate(a, b) => {...}, a, b)`
2022-06-23 09:29:46 +00:00
|
|
|
await page.evaluate(x => {
|
2022-06-15 10:09:22 +00:00
|
|
|
return x.textContent;
|
|
|
|
}, handle)
|
2020-10-07 08:49:11 +00:00
|
|
|
).toBe('anything');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should resolve promise when node is added', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page, server} = await getTestState();
|
2020-10-07 08:49:11 +00:00
|
|
|
|
|
|
|
await page.goto(server.EMPTY_PAGE);
|
|
|
|
const frame = page.mainFrame();
|
|
|
|
const watchdog = frame.waitForSelector('aria/[role="heading"]');
|
|
|
|
await frame.evaluate(addElement, 'br');
|
|
|
|
await frame.evaluate(addElement, 'h1');
|
2023-08-30 10:02:59 +00:00
|
|
|
using elementHandle = (await watchdog)!;
|
2022-06-15 10:09:22 +00:00
|
|
|
const tagName = await (
|
|
|
|
await elementHandle.getProperty('tagName')
|
|
|
|
).jsonValue();
|
2020-10-07 08:49:11 +00:00
|
|
|
expect(tagName).toBe('H1');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should work when node is added through innerHTML', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page, server} = await getTestState();
|
2020-10-07 08:49:11 +00:00
|
|
|
|
|
|
|
await page.goto(server.EMPTY_PAGE);
|
|
|
|
const watchdog = page.waitForSelector('aria/name');
|
|
|
|
await page.evaluate(addElement, 'span');
|
2022-06-15 10:09:22 +00:00
|
|
|
await page.evaluate(() => {
|
|
|
|
return (document.querySelector('span')!.innerHTML =
|
|
|
|
'<h3><div aria-label="name"></div></h3>');
|
|
|
|
});
|
2020-10-07 08:49:11 +00:00
|
|
|
await watchdog;
|
|
|
|
});
|
|
|
|
|
|
|
|
it('Page.waitForSelector is shortcut for main frame', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page, server} = await getTestState();
|
2020-10-07 08:49:11 +00:00
|
|
|
|
|
|
|
await page.goto(server.EMPTY_PAGE);
|
2023-04-25 13:02:25 +00:00
|
|
|
await attachFrame(page, 'frame1', server.EMPTY_PAGE);
|
2020-10-07 08:49:11 +00:00
|
|
|
const otherFrame = page.frames()[1];
|
|
|
|
const watchdog = page.waitForSelector('aria/[role="button"]');
|
2022-06-15 10:09:22 +00:00
|
|
|
await otherFrame!.evaluate(addElement, 'button');
|
2020-10-07 08:49:11 +00:00
|
|
|
await page.evaluate(addElement, 'button');
|
2023-08-30 10:02:59 +00:00
|
|
|
using elementHandle = await watchdog;
|
2022-08-25 15:38:02 +00:00
|
|
|
expect(elementHandle!.frame).toBe(page.mainFrame());
|
2020-10-07 08:49:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should run in specified frame', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page, server} = await getTestState();
|
2020-10-07 08:49:11 +00:00
|
|
|
|
2023-04-25 13:02:25 +00:00
|
|
|
await attachFrame(page, 'frame1', server.EMPTY_PAGE);
|
|
|
|
await attachFrame(page, 'frame2', server.EMPTY_PAGE);
|
2020-10-07 08:49:11 +00:00
|
|
|
const frame1 = page.frames()[1];
|
|
|
|
const frame2 = page.frames()[2];
|
2022-06-15 10:09:22 +00:00
|
|
|
const waitForSelectorPromise = frame2!.waitForSelector(
|
2020-10-07 08:49:11 +00:00
|
|
|
'aria/[role="button"]'
|
|
|
|
);
|
2022-06-15 10:09:22 +00:00
|
|
|
await frame1!.evaluate(addElement, 'button');
|
|
|
|
await frame2!.evaluate(addElement, 'button');
|
2023-08-30 10:02:59 +00:00
|
|
|
using elementHandle = await waitForSelectorPromise;
|
2022-08-25 15:38:02 +00:00
|
|
|
expect(elementHandle!.frame).toBe(frame2);
|
2020-10-07 08:49:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should throw when frame is detached', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page, server} = await getTestState();
|
2020-10-07 08:49:11 +00:00
|
|
|
|
2023-04-25 13:02:25 +00:00
|
|
|
await attachFrame(page, 'frame1', server.EMPTY_PAGE);
|
2020-10-07 08:49:11 +00:00
|
|
|
const frame = page.frames()[1];
|
2022-06-15 10:09:22 +00:00
|
|
|
let waitError!: Error;
|
|
|
|
const waitPromise = frame!
|
2020-10-07 08:49:11 +00:00
|
|
|
.waitForSelector('aria/does-not-exist')
|
2022-06-22 13:25:44 +00:00
|
|
|
.catch(error => {
|
2022-06-15 10:09:22 +00:00
|
|
|
return (waitError = error);
|
|
|
|
});
|
2023-04-25 13:02:25 +00:00
|
|
|
await detachFrame(page, 'frame1');
|
2020-10-07 08:49:11 +00:00
|
|
|
await waitPromise;
|
|
|
|
expect(waitError).toBeTruthy();
|
2024-02-12 16:28:21 +00:00
|
|
|
expect(waitError.message).atLeastOneToContain([
|
|
|
|
'waitForFunction failed: frame got detached.',
|
|
|
|
'Browsing context already closed.',
|
|
|
|
]);
|
2020-10-07 08:49:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should survive cross-process navigation', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page, server} = await getTestState();
|
2020-10-07 08:49:11 +00:00
|
|
|
|
|
|
|
let imgFound = false;
|
|
|
|
const waitForSelector = page
|
2023-09-13 14:35:47 +00:00
|
|
|
.waitForSelector('aria/[role="image"]')
|
2022-06-15 10:09:22 +00:00
|
|
|
.then(() => {
|
|
|
|
return (imgFound = true);
|
|
|
|
});
|
2020-10-07 08:49:11 +00:00
|
|
|
await page.goto(server.EMPTY_PAGE);
|
|
|
|
expect(imgFound).toBe(false);
|
|
|
|
await page.reload();
|
|
|
|
expect(imgFound).toBe(false);
|
|
|
|
await page.goto(server.CROSS_PROCESS_PREFIX + '/grid.html');
|
|
|
|
await waitForSelector;
|
|
|
|
expect(imgFound).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should wait for visible', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await getTestState();
|
2020-10-07 08:49:11 +00:00
|
|
|
|
|
|
|
let divFound = false;
|
|
|
|
const waitForSelector = page
|
2022-06-22 13:25:44 +00:00
|
|
|
.waitForSelector('aria/name', {visible: true})
|
2022-06-15 10:09:22 +00:00
|
|
|
.then(() => {
|
|
|
|
return (divFound = true);
|
|
|
|
});
|
2020-10-07 08:49:11 +00:00
|
|
|
await page.setContent(
|
|
|
|
`<div aria-label='name' style='display: none; visibility: hidden;'>1</div>`
|
|
|
|
);
|
|
|
|
expect(divFound).toBe(false);
|
2022-06-15 10:09:22 +00:00
|
|
|
await page.evaluate(() => {
|
|
|
|
return document.querySelector('div')!.style.removeProperty('display');
|
|
|
|
});
|
2020-10-07 08:49:11 +00:00
|
|
|
expect(divFound).toBe(false);
|
2022-06-15 10:09:22 +00:00
|
|
|
await page.evaluate(() => {
|
|
|
|
return document
|
|
|
|
.querySelector('div')!
|
|
|
|
.style.removeProperty('visibility');
|
|
|
|
});
|
2020-10-07 08:49:11 +00:00
|
|
|
expect(await waitForSelector).toBe(true);
|
|
|
|
expect(divFound).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should wait for visible recursively', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await getTestState();
|
2020-10-07 08:49:11 +00:00
|
|
|
|
|
|
|
let divVisible = false;
|
|
|
|
const waitForSelector = page
|
2022-06-22 13:25:44 +00:00
|
|
|
.waitForSelector('aria/inner', {visible: true})
|
2022-06-15 10:09:22 +00:00
|
|
|
.then(() => {
|
|
|
|
return (divVisible = true);
|
2023-06-13 13:05:01 +00:00
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
return (divVisible = false);
|
2022-06-15 10:09:22 +00:00
|
|
|
});
|
2020-10-07 08:49:11 +00:00
|
|
|
await page.setContent(
|
|
|
|
`<div style='display: none; visibility: hidden;'><div aria-label="inner">hi</div></div>`
|
|
|
|
);
|
|
|
|
expect(divVisible).toBe(false);
|
2022-06-15 10:09:22 +00:00
|
|
|
await page.evaluate(() => {
|
|
|
|
return document.querySelector('div')!.style.removeProperty('display');
|
|
|
|
});
|
2020-10-07 08:49:11 +00:00
|
|
|
expect(divVisible).toBe(false);
|
2022-06-15 10:09:22 +00:00
|
|
|
await page.evaluate(() => {
|
|
|
|
return document
|
|
|
|
.querySelector('div')!
|
|
|
|
.style.removeProperty('visibility');
|
|
|
|
});
|
2020-10-07 08:49:11 +00:00
|
|
|
expect(await waitForSelector).toBe(true);
|
|
|
|
expect(divVisible).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('hidden should wait for visibility: hidden', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await getTestState();
|
2020-10-07 08:49:11 +00:00
|
|
|
|
|
|
|
let divHidden = false;
|
|
|
|
await page.setContent(
|
2022-09-15 07:25:20 +00:00
|
|
|
`<div role='button' style='display: block;'>text</div>`
|
2020-10-07 08:49:11 +00:00
|
|
|
);
|
|
|
|
const waitForSelector = page
|
2022-06-22 13:25:44 +00:00
|
|
|
.waitForSelector('aria/[role="button"]', {hidden: true})
|
2022-06-15 10:09:22 +00:00
|
|
|
.then(() => {
|
|
|
|
return (divHidden = true);
|
2023-06-13 13:05:01 +00:00
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
return (divHidden = false);
|
2022-06-15 10:09:22 +00:00
|
|
|
});
|
2020-10-07 08:49:11 +00:00
|
|
|
await page.waitForSelector('aria/[role="button"]'); // do a round trip
|
|
|
|
expect(divHidden).toBe(false);
|
2022-06-15 10:09:22 +00:00
|
|
|
await page.evaluate(() => {
|
|
|
|
return document
|
|
|
|
.querySelector('div')!
|
|
|
|
.style.setProperty('visibility', 'hidden');
|
|
|
|
});
|
2020-10-07 08:49:11 +00:00
|
|
|
expect(await waitForSelector).toBe(true);
|
|
|
|
expect(divHidden).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('hidden should wait for display: none', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await getTestState();
|
2020-10-07 08:49:11 +00:00
|
|
|
|
|
|
|
let divHidden = false;
|
2022-09-15 07:25:20 +00:00
|
|
|
await page.setContent(
|
|
|
|
`<div role='main' style='display: block;'>text</div>`
|
|
|
|
);
|
2020-10-07 08:49:11 +00:00
|
|
|
const waitForSelector = page
|
2022-06-22 13:25:44 +00:00
|
|
|
.waitForSelector('aria/[role="main"]', {hidden: true})
|
2022-06-15 10:09:22 +00:00
|
|
|
.then(() => {
|
|
|
|
return (divHidden = true);
|
2023-06-13 13:05:01 +00:00
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
return (divHidden = false);
|
2022-06-15 10:09:22 +00:00
|
|
|
});
|
2020-10-07 08:49:11 +00:00
|
|
|
await page.waitForSelector('aria/[role="main"]'); // do a round trip
|
|
|
|
expect(divHidden).toBe(false);
|
2022-06-15 10:09:22 +00:00
|
|
|
await page.evaluate(() => {
|
|
|
|
return document
|
|
|
|
.querySelector('div')!
|
|
|
|
.style.setProperty('display', 'none');
|
|
|
|
});
|
2020-10-07 08:49:11 +00:00
|
|
|
expect(await waitForSelector).toBe(true);
|
|
|
|
expect(divHidden).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('hidden should wait for removal', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await getTestState();
|
2020-10-07 08:49:11 +00:00
|
|
|
|
2022-09-15 07:25:20 +00:00
|
|
|
await page.setContent(`<div role='main'>text</div>`);
|
2020-10-07 08:49:11 +00:00
|
|
|
let divRemoved = false;
|
|
|
|
const waitForSelector = page
|
2022-06-22 13:25:44 +00:00
|
|
|
.waitForSelector('aria/[role="main"]', {hidden: true})
|
2022-06-15 10:09:22 +00:00
|
|
|
.then(() => {
|
|
|
|
return (divRemoved = true);
|
2023-06-13 13:05:01 +00:00
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
return (divRemoved = false);
|
2022-06-15 10:09:22 +00:00
|
|
|
});
|
2020-10-07 08:49:11 +00:00
|
|
|
await page.waitForSelector('aria/[role="main"]'); // do a round trip
|
|
|
|
expect(divRemoved).toBe(false);
|
2022-06-15 10:09:22 +00:00
|
|
|
await page.evaluate(() => {
|
|
|
|
return document.querySelector('div')!.remove();
|
|
|
|
});
|
2020-10-07 08:49:11 +00:00
|
|
|
expect(await waitForSelector).toBe(true);
|
|
|
|
expect(divRemoved).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should return null if waiting to hide non-existing element', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await getTestState();
|
2020-10-07 08:49:11 +00:00
|
|
|
|
2023-08-30 10:02:59 +00:00
|
|
|
using handle = await page.waitForSelector('aria/non-existing', {
|
2020-10-07 08:49:11 +00:00
|
|
|
hidden: true,
|
|
|
|
});
|
|
|
|
expect(handle).toBe(null);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should respect timeout', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await getTestState();
|
2020-10-07 08:49:11 +00:00
|
|
|
|
2022-09-15 07:25:20 +00:00
|
|
|
const error = await page
|
|
|
|
.waitForSelector('aria/[role="button"]', {
|
|
|
|
timeout: 10,
|
|
|
|
})
|
|
|
|
.catch(error => {
|
|
|
|
return error;
|
2022-06-15 10:09:22 +00:00
|
|
|
});
|
2020-10-07 08:49:11 +00:00
|
|
|
expect(error.message).toContain(
|
2022-09-15 06:22:20 +00:00
|
|
|
'Waiting for selector `[role="button"]` failed: Waiting failed: 10ms exceeded'
|
2020-10-07 08:49:11 +00:00
|
|
|
);
|
2022-10-06 14:21:24 +00:00
|
|
|
expect(error).toBeInstanceOf(TimeoutError);
|
2020-10-07 08:49:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should have an error message specifically for awaiting an element to be hidden', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await getTestState();
|
2020-10-07 08:49:11 +00:00
|
|
|
|
2022-09-15 07:25:20 +00:00
|
|
|
await page.setContent(`<div role='main'>text</div>`);
|
|
|
|
const promise = page.waitForSelector('aria/[role="main"]', {
|
|
|
|
hidden: true,
|
|
|
|
timeout: 10,
|
|
|
|
});
|
|
|
|
await expect(promise).rejects.toMatchObject({
|
|
|
|
message:
|
|
|
|
'Waiting for selector `[role="main"]` failed: Waiting failed: 10ms exceeded',
|
|
|
|
});
|
2020-10-07 08:49:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should respond to node attribute mutation', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await getTestState();
|
2020-10-07 08:49:11 +00:00
|
|
|
|
|
|
|
let divFound = false;
|
2023-06-13 13:05:01 +00:00
|
|
|
const waitForSelector = page
|
|
|
|
.waitForSelector('aria/zombo')
|
|
|
|
.then(() => {
|
|
|
|
return (divFound = true);
|
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
return (divFound = false);
|
|
|
|
});
|
2020-10-07 08:49:11 +00:00
|
|
|
await page.setContent(`<div aria-label='notZombo'></div>`);
|
|
|
|
expect(divFound).toBe(false);
|
2022-06-15 10:09:22 +00:00
|
|
|
await page.evaluate(() => {
|
|
|
|
return document
|
|
|
|
.querySelector('div')!
|
|
|
|
.setAttribute('aria-label', 'zombo');
|
|
|
|
});
|
2020-10-07 08:49:11 +00:00
|
|
|
expect(await waitForSelector).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should return the element handle', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await getTestState();
|
2020-10-07 08:49:11 +00:00
|
|
|
|
2023-06-13 13:05:01 +00:00
|
|
|
const waitForSelector = page.waitForSelector('aria/zombo').catch(err => {
|
|
|
|
return err;
|
|
|
|
});
|
2020-10-07 08:49:11 +00:00
|
|
|
await page.setContent(`<div aria-label='zombo'>anything</div>`);
|
|
|
|
expect(
|
2023-07-17 08:52:54 +00:00
|
|
|
await page.evaluate(
|
|
|
|
x => {
|
|
|
|
return x?.textContent;
|
|
|
|
},
|
|
|
|
await waitForSelector
|
|
|
|
)
|
2020-10-07 08:49:11 +00:00
|
|
|
).toBe('anything');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should have correct stack trace for timeout', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await getTestState();
|
2020-10-07 08:49:11 +00:00
|
|
|
|
2022-06-15 10:09:22 +00:00
|
|
|
let error!: Error;
|
2022-06-22 13:25:44 +00:00
|
|
|
await page.waitForSelector('aria/zombo', {timeout: 10}).catch(error_ => {
|
|
|
|
return (error = error_);
|
|
|
|
});
|
2022-09-15 06:22:20 +00:00
|
|
|
expect(error!.stack).toContain(
|
|
|
|
'Waiting for selector `zombo` failed: Waiting failed: 10ms exceeded'
|
|
|
|
);
|
2020-10-07 08:49:11 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-05-24 12:08:36 +00:00
|
|
|
describe('queryOne (Chromium web test)', () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
async function setupPage(): ReturnType<typeof getTestState> {
|
|
|
|
const state = await getTestState();
|
|
|
|
await state.page.setContent(
|
2020-10-05 06:25:55 +00:00
|
|
|
`
|
|
|
|
<h2 id="shown">title</h2>
|
|
|
|
<h2 id="hidden" aria-hidden="true">title</h2>
|
|
|
|
<div id="node1" aria-labeledby="node2"></div>
|
|
|
|
<div id="node2" aria-label="bar"></div>
|
|
|
|
<div id="node3" aria-label="foo"></div>
|
|
|
|
<div id="node4" class="container">
|
|
|
|
<div id="node5" role="button" aria-label="foo"></div>
|
|
|
|
<div id="node6" role="button" aria-label="foo"></div>
|
|
|
|
<!-- Accessible name not available when element is hidden -->
|
|
|
|
<div id="node7" hidden role="button" aria-label="foo"></div>
|
|
|
|
<div id="node8" role="button" aria-label="bar"></div>
|
|
|
|
</div>
|
|
|
|
<button id="node10">text content</button>
|
|
|
|
<h1 id="node11">text content</h1>
|
|
|
|
<!-- Accessible name not available when role is "presentation" -->
|
|
|
|
<h1 id="node12" role="presentation">text content</h1>
|
|
|
|
<!-- Elements inside shadow dom should be found -->
|
|
|
|
<script>
|
|
|
|
const div = document.createElement('div');
|
|
|
|
const shadowRoot = div.attachShadow({mode: 'open'});
|
|
|
|
const h1 = document.createElement('h1');
|
|
|
|
h1.textContent = 'text content';
|
|
|
|
h1.id = 'node13';
|
|
|
|
shadowRoot.appendChild(h1);
|
|
|
|
document.documentElement.appendChild(div);
|
|
|
|
</script>
|
|
|
|
<img id="node20" src="" alt="Accessible Name">
|
|
|
|
<input id="node21" type="submit" value="Accessible Name">
|
|
|
|
<label id="node22" for="node23">Accessible Name</label>
|
|
|
|
<!-- Accessible name for the <input> is "Accessible Name" -->
|
|
|
|
<input id="node23">
|
|
|
|
<div id="node24" title="Accessible Name"></div>
|
|
|
|
<div role="treeitem" id="node30">
|
|
|
|
<div role="treeitem" id="node31">
|
|
|
|
<div role="treeitem" id="node32">item1</div>
|
|
|
|
<div role="treeitem" id="node33">item2</div>
|
|
|
|
</div>
|
|
|
|
<div role="treeitem" id="node34">item3</div>
|
|
|
|
</div>
|
|
|
|
<!-- Accessible name for the <div> is "item1 item2 item3" -->
|
|
|
|
<div aria-describedby="node30"></div>
|
|
|
|
`
|
|
|
|
);
|
2023-06-21 19:41:09 +00:00
|
|
|
return state;
|
|
|
|
}
|
2022-06-15 10:09:22 +00:00
|
|
|
const getIds = async (elements: ElementHandle[]) => {
|
2023-09-01 07:49:33 +00:00
|
|
|
return await Promise.all(
|
2022-06-22 13:25:44 +00:00
|
|
|
elements.map(element => {
|
2022-06-15 10:09:22 +00:00
|
|
|
return element.evaluate((element: Element) => {
|
|
|
|
return element.id;
|
|
|
|
});
|
|
|
|
})
|
2020-10-05 06:25:55 +00:00
|
|
|
);
|
2022-06-15 10:09:22 +00:00
|
|
|
};
|
2020-10-05 06:25:55 +00:00
|
|
|
it('should find by name "foo"', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await setupPage();
|
2020-10-05 06:25:55 +00:00
|
|
|
const found = await page.$$('aria/foo');
|
|
|
|
const ids = await getIds(found);
|
|
|
|
expect(ids).toEqual(['node3', 'node5', 'node6']);
|
|
|
|
});
|
|
|
|
it('should find by name "bar"', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await setupPage();
|
2020-10-05 06:25:55 +00:00
|
|
|
const found = await page.$$('aria/bar');
|
|
|
|
const ids = await getIds(found);
|
|
|
|
expect(ids).toEqual(['node1', 'node2', 'node8']);
|
|
|
|
});
|
|
|
|
it('should find treeitem by name', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await setupPage();
|
2020-10-05 06:25:55 +00:00
|
|
|
const found = await page.$$('aria/item1 item2 item3');
|
|
|
|
const ids = await getIds(found);
|
|
|
|
expect(ids).toEqual(['node30']);
|
|
|
|
});
|
|
|
|
it('should find by role "button"', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await setupPage();
|
2022-07-06 07:05:37 +00:00
|
|
|
const found = (await page.$$('aria/[role="button"]')) as Array<
|
|
|
|
ElementHandle<HTMLButtonElement>
|
|
|
|
>;
|
2020-10-05 06:25:55 +00:00
|
|
|
const ids = await getIds(found);
|
2022-02-04 11:18:53 +00:00
|
|
|
expect(ids).toEqual([
|
|
|
|
'node5',
|
|
|
|
'node6',
|
|
|
|
'node7',
|
|
|
|
'node8',
|
|
|
|
'node10',
|
|
|
|
'node21',
|
|
|
|
]);
|
2020-10-05 06:25:55 +00:00
|
|
|
});
|
|
|
|
it('should find by role "heading"', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await setupPage();
|
2020-10-05 06:25:55 +00:00
|
|
|
const found = await page.$$('aria/[role="heading"]');
|
|
|
|
const ids = await getIds(found);
|
2021-08-04 12:22:15 +00:00
|
|
|
expect(ids).toEqual(['shown', 'hidden', 'node11', 'node13']);
|
2020-10-05 06:25:55 +00:00
|
|
|
});
|
2021-08-04 12:22:15 +00:00
|
|
|
it('should find both ignored and unignored', async () => {
|
2023-06-21 19:41:09 +00:00
|
|
|
const {page} = await setupPage();
|
2020-10-05 06:25:55 +00:00
|
|
|
const found = await page.$$('aria/title');
|
|
|
|
const ids = await getIds(found);
|
2022-08-31 13:27:59 +00:00
|
|
|
expect(ids).toEqual(['shown']);
|
2020-10-05 06:25:55 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|