playwright/tests/hit-target.spec.ts
Dmitry Gozman 0b04c7d504
fix(drag&drop): relax layout shift logic when dropping (#11760)
When element that is being dragged stays under the mouse,
it prevents the hit target check on drop from working,
because drop target is overlayed by the dragged element.

To workaround this, we perform a one-time hit target check
before moving for the drop, as we used to.
2022-01-31 16:21:35 -08:00

187 lines
6.7 KiB
TypeScript

/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { contextTest as it, expect } from './config/browserTest';
declare const renderComponent;
declare const e;
declare const MaterialUI;
it('should block all events when hit target is wrong', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/button.html');
await page.evaluate(() => {
const blocker = document.createElement('div');
blocker.style.position = 'absolute';
blocker.style.width = '400px';
blocker.style.height = '400px';
blocker.style.left = '0';
blocker.style.top = '0';
document.body.appendChild(blocker);
const allEvents = [];
(window as any).allEvents = allEvents;
for (const name of ['mousedown', 'mouseup', 'click', 'dblclick', 'auxclick', 'contextmenu', 'pointerdown', 'pointerup']) {
window.addEventListener(name, e => allEvents.push(e.type));
blocker.addEventListener(name, e => allEvents.push(e.type));
}
});
const error = await page.click('button', { timeout: 1000 }).catch(e => e);
expect(error.message).toContain('page.click: Timeout 1000ms exceeded.');
// Give it some time, just in case.
await page.waitForTimeout(1000);
const allEvents = await page.evaluate(() => (window as any).allEvents);
expect(allEvents).toEqual([]);
});
it('should block click when mousedown fails', async ({ page, server }) => {
it.skip(!!process.env.PLAYWRIGHT_NO_LAYOUT_SHIFT_CHECK);
await page.goto(server.PREFIX + '/input/button.html');
await page.$eval('button', button => {
button.addEventListener('mousemove', () => {
button.style.marginLeft = '100px';
});
const allEvents = [];
(window as any).allEvents = allEvents;
for (const name of ['mousemove', 'mousedown', 'mouseup', 'click', 'dblclick', 'auxclick', 'contextmenu', 'pointerdown', 'pointerup'])
button.addEventListener(name, e => allEvents.push(e.type));
});
await page.click('button');
expect(await page.evaluate('result')).toBe('Clicked');
const allEvents = await page.evaluate(() => (window as any).allEvents);
expect(allEvents).toEqual([
// First attempt failed.
'mousemove',
// Second attempt succeeded.
'mousemove', 'pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click',
]);
});
it('should click when element detaches in mousedown', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/button.html');
await page.$eval('button', button => {
button.addEventListener('mousedown', () => {
(window as any).result = 'Mousedown';
button.remove();
});
});
await page.click('button', { timeout: 1000 });
expect(await page.evaluate('result')).toBe('Mousedown');
});
it('should block all events when hit target is wrong and element detaches', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/button.html');
await page.$eval('button', button => {
const blocker = document.createElement('div');
blocker.style.position = 'absolute';
blocker.style.width = '400px';
blocker.style.height = '400px';
blocker.style.left = '0';
blocker.style.top = '0';
document.body.appendChild(blocker);
window.addEventListener('mousemove', () => button.remove());
const allEvents = [];
(window as any).allEvents = allEvents;
for (const name of ['mousedown', 'mouseup', 'click', 'dblclick', 'auxclick', 'contextmenu', 'pointerdown', 'pointerup']) {
window.addEventListener(name, e => allEvents.push(e.type));
blocker.addEventListener(name, e => allEvents.push(e.type));
}
});
const error = await page.click('button', { timeout: 1000 }).catch(e => e);
expect(error.message).toContain('page.click: Timeout 1000ms exceeded.');
// Give it some time, just in case.
await page.waitForTimeout(1000);
const allEvents = await page.evaluate(() => (window as any).allEvents);
expect(allEvents).toEqual([]);
});
it('should not block programmatic events', async ({ page, server }) => {
it.skip(!!process.env.PLAYWRIGHT_NO_LAYOUT_SHIFT_CHECK);
await page.goto(server.PREFIX + '/input/button.html');
await page.$eval('button', button => {
button.addEventListener('mousemove', () => {
button.style.marginLeft = '100px';
button.dispatchEvent(new MouseEvent('click'));
});
const allEvents = [];
(window as any).allEvents = allEvents;
button.addEventListener('click', e => {
if (!e.isTrusted)
allEvents.push(e.type);
});
});
await page.click('button');
expect(await page.evaluate('result')).toBe('Clicked');
const allEvents = await page.evaluate(() => (window as any).allEvents);
// We should get one programmatic click on each attempt.
expect(allEvents).toEqual([
'click', 'click',
]);
});
it('should click the button again after document.write', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/button.html');
await page.click('button');
expect(await page.evaluate('result')).toBe('Clicked');
await page.evaluate(() => {
document.open();
document.write('<button onclick="window.result2 = true"></button>');
document.close();
});
await page.click('button');
expect(await page.evaluate('result2')).toBe(true);
});
it('should work with mui select', async ({ page, server }) => {
await page.goto(server.PREFIX + '/mui.html');
await page.evaluate(() => {
renderComponent(e(MaterialUI.FormControl, { fullWidth: true }, [
e(MaterialUI.InputLabel, { id: 'demo-simple-select-label' }, ['Age']),
e(MaterialUI.Select, {
labelId: 'demo-simple-select-label',
id: 'demo-simple-select',
value: 10,
label: 'Age',
}, [
e(MaterialUI.MenuItem, { value: 10 }, ['Ten']),
e(MaterialUI.MenuItem, { value: 20 }, ['Twenty']),
e(MaterialUI.MenuItem, { value: 30 }, ['Thirty']),
]),
]));
});
await page.click('div.MuiFormControl-root:has-text("Age")');
await expect(page.locator('text=Thirty')).toBeVisible();
});
it('should work with drag and drop that moves the element under cursor', async ({ page, server }) => {
await page.goto(server.PREFIX + '/input/drag-n-drop-manual.html');
await page.dragAndDrop('#from', '#to');
await expect(page.locator('#to')).toHaveText('Dropped');
});