fix!: fix bounding box visibility conditions (#8954)

This commit is contained in:
jrandolf 2022-09-15 09:25:20 +02:00 committed by GitHub
parent 64763e973b
commit ac9929d80f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 203 additions and 107 deletions

View File

@ -19,6 +19,8 @@ export const createFunction = (
return fn;
};
const HIDDEN_VISIBILITY_VALUES = ['hidden', 'collapse'];
/**
* @internal
*/
@ -38,11 +40,20 @@ export const checkVisibility = (
const style = window.getComputedStyle(element);
const isVisible =
style && style.visibility !== 'hidden' && isBoundingBoxVisible(element);
style &&
!HIDDEN_VISIBILITY_VALUES.includes(style.visibility) &&
isBoundingBoxVisible(element);
return visible === isVisible ? node : false;
};
function isBoundingBoxVisible(element: Element): boolean {
const rect = element.getBoundingClientRect();
return !!(rect.top || rect.bottom || rect.width || rect.height);
return (
rect.width > 0 &&
rect.height > 0 &&
rect.right > 0 &&
rect.bottom > 0 &&
rect.left < self.innerWidth &&
rect.top < self.innerHeight
);
}

View File

@ -446,7 +446,7 @@ describe('AriaQueryHandler', () => {
let divHidden = false;
await page.setContent(
`<div role='button' style='display: block;'></div>`
`<div role='button' style='display: block;'>text</div>`
);
const waitForSelector = page
.waitForSelector('aria/[role="button"]', {hidden: true})
@ -468,7 +468,9 @@ describe('AriaQueryHandler', () => {
const {page} = getTestState();
let divHidden = false;
await page.setContent(`<div role='main' style='display: block;'></div>`);
await page.setContent(
`<div role='main' style='display: block;'>text</div>`
);
const waitForSelector = page
.waitForSelector('aria/[role="main"]', {hidden: true})
.then(() => {
@ -488,7 +490,7 @@ describe('AriaQueryHandler', () => {
it('hidden should wait for removal', async () => {
const {page} = getTestState();
await page.setContent(`<div role='main'></div>`);
await page.setContent(`<div role='main'>text</div>`);
let divRemoved = false;
const waitForSelector = page
.waitForSelector('aria/[role="main"]', {hidden: true})
@ -516,13 +518,13 @@ describe('AriaQueryHandler', () => {
it('should respect timeout', async () => {
const {page, puppeteer} = getTestState();
let error!: Error;
await page
.waitForSelector('aria/[role="button"]', {timeout: 10})
.catch(error_ => {
return (error = error_);
const error = await page
.waitForSelector('aria/[role="button"]', {
timeout: 10,
})
.catch(error => {
return error;
});
expect(error).toBeTruthy();
expect(error.message).toContain(
'Waiting for selector `[role="button"]` failed: Waiting failed: 10ms exceeded'
);
@ -532,17 +534,15 @@ describe('AriaQueryHandler', () => {
it('should have an error message specifically for awaiting an element to be hidden', async () => {
const {page} = getTestState();
await page.setContent(`<div role='main'></div>`);
let error!: Error;
await page
.waitForSelector('aria/[role="main"]', {hidden: true, timeout: 10})
.catch(error_ => {
return (error = error_);
});
expect(error).toBeTruthy();
expect(error.message).toContain(
'Waiting for selector `[role="main"]` failed: Waiting failed: 10ms exceeded'
);
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',
});
});
it('should respond to node attribute mutation', async () => {

View File

@ -295,3 +295,14 @@ export const shortWaitForArrayToHaveAtLeastNElements = async (
});
}
};
export const createTimeout = <T>(
n: number,
value?: T
): Promise<T | undefined> => {
return new Promise(resolve => {
setTimeout(() => {
return resolve(value);
}, n);
});
};

View File

@ -17,6 +17,7 @@
import expect from 'expect';
import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js';
import {
createTimeout,
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
@ -478,113 +479,186 @@ describe('waittask specs', function () {
await waitForSelector;
expect(boxFound).toBe(true);
});
it('should wait for visible', async () => {
it('should wait for element to be visible (display)', async () => {
const {page} = getTestState();
let divFound = false;
const waitForSelector = page
.waitForSelector('div', {visible: true})
.then(() => {
return (divFound = true);
});
await page.setContent(
`<div style='display: none; visibility: hidden;'>1</div>`
);
expect(divFound).toBe(false);
await page.evaluate(() => {
return document.querySelector('div')?.style.removeProperty('display');
const promise = page.waitForSelector('div', {visible: true});
await page.setContent('<div style="display: none">text</div>');
const element = await page.evaluateHandle(() => {
return document.getElementsByTagName('div')[0]!;
});
expect(divFound).toBe(false);
await page.evaluate(() => {
return document
.querySelector('div')
?.style.removeProperty('visibility');
await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
e.style.removeProperty('display');
});
expect(await waitForSelector).toBe(true);
expect(divFound).toBe(true);
await expect(promise).resolves.toBeTruthy();
});
it('should wait for visible recursively', async () => {
it('should wait for element to be visible (visibility)', async () => {
const {page} = getTestState();
let divVisible = false;
const waitForSelector = page
.waitForSelector('div#inner', {visible: true})
.then(() => {
return (divVisible = true);
});
const promise = page.waitForSelector('div', {visible: true});
await page.setContent('<div style="visibility: hidden">text</div>');
const element = await page.evaluateHandle(() => {
return document.getElementsByTagName('div')[0]!;
});
await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
e.style.setProperty('visibility', 'collapse');
});
await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
e.style.removeProperty('visibility');
});
await expect(promise).resolves.toBeTruthy();
});
it('should wait for element to be visible (bounding box)', async () => {
const {page} = getTestState();
const promise = page.waitForSelector('div', {visible: true});
await page.setContent('<div style="width: 0">text</div>');
const element = await page.evaluateHandle(() => {
return document.getElementsByTagName('div')[0]!;
});
await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
e.style.setProperty('height', '0');
e.style.removeProperty('width');
});
await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
e.style.setProperty('position', 'absolute');
e.style.setProperty('right', '100vw');
e.style.removeProperty('height');
});
await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
e.style.setProperty('left', '100vw');
e.style.removeProperty('right');
});
await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
e.style.setProperty('top', '100vh');
e.style.removeProperty('left');
});
await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
e.style.setProperty('bottom', '100vh');
e.style.removeProperty('top');
});
await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
// Just peeking
e.style.setProperty('bottom', '99vh');
});
await expect(promise).resolves.toBeTruthy();
});
it('should wait for element to be visible recursively', async () => {
const {page} = getTestState();
const promise = page.waitForSelector('div#inner', {
visible: true,
});
await page.setContent(
`<div style='display: none; visibility: hidden;'><div id="inner">hi</div></div>`
);
expect(divVisible).toBe(false);
await page.evaluate(() => {
return document.querySelector('div')?.style.removeProperty('display');
const element = await page.evaluateHandle(() => {
return document.getElementsByTagName('div')[0]!;
});
expect(divVisible).toBe(false);
await page.evaluate(() => {
return document
.querySelector('div')
?.style.removeProperty('visibility');
await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
return e.style.removeProperty('display');
});
expect(await waitForSelector).toBe(true);
expect(divVisible).toBe(true);
await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
return e.style.removeProperty('visibility');
});
await expect(promise).resolves.toBeTruthy();
});
it('hidden should wait for visibility: hidden', async () => {
it('should wait for element to be hidden (visibility)', async () => {
const {page} = getTestState();
let divHidden = false;
await page.setContent(`<div style='display: block;'></div>`);
const waitForSelector = page
.waitForSelector('div', {hidden: true})
.then(() => {
return (divHidden = true);
});
await page.waitForSelector('div'); // do a round trip
expect(divHidden).toBe(false);
await page.evaluate(() => {
return document
.querySelector('div')
?.style.setProperty('visibility', 'hidden');
const promise = page.waitForSelector('div', {hidden: true});
await page.setContent(`<div style='display: block;'>text</div>`);
const element = await page.evaluateHandle(() => {
return document.getElementsByTagName('div')[0]!;
});
expect(await waitForSelector).toBe(true);
expect(divHidden).toBe(true);
await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
return e.style.setProperty('visibility', 'hidden');
});
await expect(promise).resolves.toBeTruthy();
});
it('hidden should wait for display: none', async () => {
it('should wait for element to be hidden (display)', async () => {
const {page} = getTestState();
let divHidden = false;
await page.setContent(`<div style='display: block;'></div>`);
const waitForSelector = page
.waitForSelector('div', {hidden: true})
.then(() => {
return (divHidden = true);
});
await page.waitForSelector('div'); // do a round trip
expect(divHidden).toBe(false);
await page.evaluate(() => {
return document
.querySelector('div')
?.style.setProperty('display', 'none');
const promise = page.waitForSelector('div', {hidden: true});
await page.setContent(`<div style='display: block;'>text</div>`);
const element = await page.evaluateHandle(() => {
return document.getElementsByTagName('div')[0]!;
});
expect(await waitForSelector).toBe(true);
expect(divHidden).toBe(true);
await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
return e.style.setProperty('display', 'none');
});
await expect(promise).resolves.toBeTruthy();
});
it('hidden should wait for removal', async () => {
it('should wait for element to be hidden (bounding box)', async () => {
const {page} = getTestState();
await page.setContent(`<div></div>`);
let divRemoved = false;
const waitForSelector = page
.waitForSelector('div', {hidden: true})
.then(() => {
return (divRemoved = true);
});
await page.waitForSelector('div'); // do a round trip
expect(divRemoved).toBe(false);
await page.evaluate(() => {
return document.querySelector('div')?.remove();
const promise = page.waitForSelector('div', {hidden: true});
await page.setContent('<div>text</div>');
const element = await page.evaluateHandle(() => {
return document.getElementsByTagName('div')[0]!;
});
expect(await waitForSelector).toBe(true);
expect(divRemoved).toBe(true);
await expect(
Promise.race([promise, createTimeout(40)])
).resolves.toBeFalsy();
await element.evaluate(e => {
e.style.setProperty('height', '0');
});
await expect(promise).resolves.toBeTruthy();
});
it('should wait for element to be hidden (removal)', async () => {
const {page} = getTestState();
const promise = page.waitForSelector('div', {hidden: true});
await page.setContent(`<div>text</div>`);
const element = await page.evaluateHandle(() => {
return document.getElementsByTagName('div')[0]!;
});
await expect(
Promise.race([promise, createTimeout(40, true)])
).resolves.toBeTruthy();
await element.evaluate(e => {
e.remove();
});
await expect(promise).resolves.toBeFalsy();
});
it('should return null if waiting to hide non-existing element', async () => {
const {page} = getTestState();
@ -609,7 +683,7 @@ describe('waittask specs', function () {
it('should have an error message specifically for awaiting an element to be hidden', async () => {
const {page} = getTestState();
await page.setContent(`<div></div>`);
await page.setContent(`<div>text</div>`);
let error!: Error;
await page
.waitForSelector('div', {hidden: true, timeout: 10})
@ -725,7 +799,7 @@ describe('waittask specs', function () {
const {page} = getTestState();
let divHidden = false;
await page.setContent(`<div style='display: block;'></div>`);
await page.setContent(`<div style='display: block;'>text</div>`);
const waitForXPath = page
.waitForXPath('//div', {hidden: true})
.then(() => {