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; return fn;
}; };
const HIDDEN_VISIBILITY_VALUES = ['hidden', 'collapse'];
/** /**
* @internal * @internal
*/ */
@ -38,11 +40,20 @@ export const checkVisibility = (
const style = window.getComputedStyle(element); const style = window.getComputedStyle(element);
const isVisible = const isVisible =
style && style.visibility !== 'hidden' && isBoundingBoxVisible(element); style &&
!HIDDEN_VISIBILITY_VALUES.includes(style.visibility) &&
isBoundingBoxVisible(element);
return visible === isVisible ? node : false; return visible === isVisible ? node : false;
}; };
function isBoundingBoxVisible(element: Element): boolean { function isBoundingBoxVisible(element: Element): boolean {
const rect = element.getBoundingClientRect(); 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; let divHidden = false;
await page.setContent( await page.setContent(
`<div role='button' style='display: block;'></div>` `<div role='button' style='display: block;'>text</div>`
); );
const waitForSelector = page const waitForSelector = page
.waitForSelector('aria/[role="button"]', {hidden: true}) .waitForSelector('aria/[role="button"]', {hidden: true})
@ -468,7 +468,9 @@ describe('AriaQueryHandler', () => {
const {page} = getTestState(); const {page} = getTestState();
let divHidden = false; 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 const waitForSelector = page
.waitForSelector('aria/[role="main"]', {hidden: true}) .waitForSelector('aria/[role="main"]', {hidden: true})
.then(() => { .then(() => {
@ -488,7 +490,7 @@ describe('AriaQueryHandler', () => {
it('hidden should wait for removal', async () => { it('hidden should wait for removal', async () => {
const {page} = getTestState(); const {page} = getTestState();
await page.setContent(`<div role='main'></div>`); await page.setContent(`<div role='main'>text</div>`);
let divRemoved = false; let divRemoved = false;
const waitForSelector = page const waitForSelector = page
.waitForSelector('aria/[role="main"]', {hidden: true}) .waitForSelector('aria/[role="main"]', {hidden: true})
@ -516,13 +518,13 @@ describe('AriaQueryHandler', () => {
it('should respect timeout', async () => { it('should respect timeout', async () => {
const {page, puppeteer} = getTestState(); const {page, puppeteer} = getTestState();
let error!: Error; const error = await page
await page .waitForSelector('aria/[role="button"]', {
.waitForSelector('aria/[role="button"]', {timeout: 10}) timeout: 10,
.catch(error_ => { })
return (error = error_); .catch(error => {
return error;
}); });
expect(error).toBeTruthy();
expect(error.message).toContain( expect(error.message).toContain(
'Waiting for selector `[role="button"]` failed: Waiting failed: 10ms exceeded' '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 () => { it('should have an error message specifically for awaiting an element to be hidden', async () => {
const {page} = getTestState(); const {page} = getTestState();
await page.setContent(`<div role='main'></div>`); await page.setContent(`<div role='main'>text</div>`);
let error!: Error; const promise = page.waitForSelector('aria/[role="main"]', {
await page hidden: true,
.waitForSelector('aria/[role="main"]', {hidden: true, timeout: 10}) timeout: 10,
.catch(error_ => { });
return (error = error_); await expect(promise).rejects.toMatchObject({
}); message:
expect(error).toBeTruthy(); 'Waiting for selector `[role="main"]` failed: Waiting failed: 10ms exceeded',
expect(error.message).toContain( });
'Waiting for selector `[role="main"]` failed: Waiting failed: 10ms exceeded'
);
}); });
it('should respond to node attribute mutation', async () => { 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 expect from 'expect';
import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js'; import {isErrorLike} from '../../lib/cjs/puppeteer/util/ErrorLike.js';
import { import {
createTimeout,
getTestState, getTestState,
setupTestBrowserHooks, setupTestBrowserHooks,
setupTestPageAndContextHooks, setupTestPageAndContextHooks,
@ -478,113 +479,186 @@ describe('waittask specs', function () {
await waitForSelector; await waitForSelector;
expect(boxFound).toBe(true); expect(boxFound).toBe(true);
}); });
it('should wait for visible', async () => { it('should wait for element to be visible (display)', async () => {
const {page} = getTestState(); const {page} = getTestState();
let divFound = false; const promise = page.waitForSelector('div', {visible: true});
const waitForSelector = page await page.setContent('<div style="display: none">text</div>');
.waitForSelector('div', {visible: true}) const element = await page.evaluateHandle(() => {
.then(() => { return document.getElementsByTagName('div')[0]!;
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');
}); });
expect(divFound).toBe(false); await expect(
await page.evaluate(() => { Promise.race([promise, createTimeout(40)])
return document ).resolves.toBeFalsy();
.querySelector('div') await element.evaluate(e => {
?.style.removeProperty('visibility'); e.style.removeProperty('display');
}); });
expect(await waitForSelector).toBe(true); await expect(promise).resolves.toBeTruthy();
expect(divFound).toBe(true);
}); });
it('should wait for visible recursively', async () => { it('should wait for element to be visible (visibility)', async () => {
const {page} = getTestState(); const {page} = getTestState();
let divVisible = false; const promise = page.waitForSelector('div', {visible: true});
const waitForSelector = page await page.setContent('<div style="visibility: hidden">text</div>');
.waitForSelector('div#inner', {visible: true}) const element = await page.evaluateHandle(() => {
.then(() => { return document.getElementsByTagName('div')[0]!;
return (divVisible = true); });
}); 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( await page.setContent(
`<div style='display: none; visibility: hidden;'><div id="inner">hi</div></div>` `<div style='display: none; visibility: hidden;'><div id="inner">hi</div></div>`
); );
expect(divVisible).toBe(false); const element = await page.evaluateHandle(() => {
await page.evaluate(() => { return document.getElementsByTagName('div')[0]!;
return document.querySelector('div')?.style.removeProperty('display');
}); });
expect(divVisible).toBe(false); await expect(
await page.evaluate(() => { Promise.race([promise, createTimeout(40)])
return document ).resolves.toBeFalsy();
.querySelector('div') await element.evaluate(e => {
?.style.removeProperty('visibility'); return e.style.removeProperty('display');
}); });
expect(await waitForSelector).toBe(true); await expect(
expect(divVisible).toBe(true); 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(); const {page} = getTestState();
let divHidden = false; const promise = page.waitForSelector('div', {hidden: true});
await page.setContent(`<div style='display: block;'></div>`); await page.setContent(`<div style='display: block;'>text</div>`);
const waitForSelector = page const element = await page.evaluateHandle(() => {
.waitForSelector('div', {hidden: true}) return document.getElementsByTagName('div')[0]!;
.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');
}); });
expect(await waitForSelector).toBe(true); await expect(
expect(divHidden).toBe(true); 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(); const {page} = getTestState();
let divHidden = false; const promise = page.waitForSelector('div', {hidden: true});
await page.setContent(`<div style='display: block;'></div>`); await page.setContent(`<div style='display: block;'>text</div>`);
const waitForSelector = page const element = await page.evaluateHandle(() => {
.waitForSelector('div', {hidden: true}) return document.getElementsByTagName('div')[0]!;
.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');
}); });
expect(await waitForSelector).toBe(true); await expect(
expect(divHidden).toBe(true); 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(); const {page} = getTestState();
await page.setContent(`<div></div>`); const promise = page.waitForSelector('div', {hidden: true});
let divRemoved = false; await page.setContent('<div>text</div>');
const waitForSelector = page const element = await page.evaluateHandle(() => {
.waitForSelector('div', {hidden: true}) return document.getElementsByTagName('div')[0]!;
.then(() => {
return (divRemoved = true);
});
await page.waitForSelector('div'); // do a round trip
expect(divRemoved).toBe(false);
await page.evaluate(() => {
return document.querySelector('div')?.remove();
}); });
expect(await waitForSelector).toBe(true); await expect(
expect(divRemoved).toBe(true); 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 () => { it('should return null if waiting to hide non-existing element', async () => {
const {page} = getTestState(); 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 () => { it('should have an error message specifically for awaiting an element to be hidden', async () => {
const {page} = getTestState(); const {page} = getTestState();
await page.setContent(`<div></div>`); await page.setContent(`<div>text</div>`);
let error!: Error; let error!: Error;
await page await page
.waitForSelector('div', {hidden: true, timeout: 10}) .waitForSelector('div', {hidden: true, timeout: 10})
@ -725,7 +799,7 @@ describe('waittask specs', function () {
const {page} = getTestState(); const {page} = getTestState();
let divHidden = false; let divHidden = false;
await page.setContent(`<div style='display: block;'></div>`); await page.setContent(`<div style='display: block;'>text</div>`);
const waitForXPath = page const waitForXPath = page
.waitForXPath('//div', {hidden: true}) .waitForXPath('//div', {hidden: true})
.then(() => { .then(() => {