Merge remote-tracking branch 'origin/main' into sync-73-815

This commit is contained in:
Liam DeBeasi
2023-08-15 11:41:43 -04:00
44 changed files with 213 additions and 141 deletions

View File

@@ -1277,9 +1277,6 @@ export namespace Components {
* The shape of the input. If "round" it will have an increased border radius.
*/
"shape"?: 'round';
/**
* The initial size of the control. This value is in pixels unless the value of the type attribute is `"text"` or `"password"`, in which case it is an integer number of characters. This attribute applies only when the `type` attribute is set to `"text"`, `"search"`, `"tel"`, `"url"`, `"email"`, or `"password"`, otherwise it is ignored.
*/
"size"?: number;
/**
* If `true`, the element will have its spelling and grammar checked.
@@ -5339,9 +5336,6 @@ declare namespace LocalJSX {
* The shape of the input. If "round" it will have an increased border radius.
*/
"shape"?: 'round';
/**
* The initial size of the control. This value is in pixels unless the value of the type attribute is `"text"` or `"password"`, in which case it is an integer number of characters. This attribute applies only when the `type` attribute is set to `"text"`, `"search"`, `"tel"`, `"url"`, `"email"`, or `"password"`, otherwise it is ignored.
*/
"size"?: number;
/**
* If `true`, the element will have its spelling and grammar checked.

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -51,7 +51,7 @@
}
.header-collapse-condense ion-toolbar:first-of-type {
padding-top: 7px;
padding-top: 1px;
z-index: 1;
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -268,9 +268,7 @@ export class Input implements ComponentInterface {
*/
@Prop() step?: string;
/**
* The initial size of the control. This value is in pixels unless the value of the type attribute is `"text"` or `"password"`, in which case it is an integer number of characters. This attribute applies only when the `type` attribute is set to `"text"`, `"search"`, `"tel"`, `"url"`, `"email"`, or `"password"`, otherwise it is ignored.
*/
// FW-4914 Remove this property in Ionic 8
@Prop() size?: number;
/**

View File

@@ -3,7 +3,6 @@ import { configs, dragElementBy, test } from '@utils/test/playwright';
import { testSlidingItem } from '../test.utils';
// TODO FW-3006
/**
* item-sliding doesn't have mode-specific styling
*/
@@ -18,19 +17,12 @@ configs({ modes: ['md'] }).forEach(({ title, screenshot, config }) => {
});
});
// TODO FW-3006
/**
* This behavior does not vary across modes/directions
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('item-sliding: basic'), () => {
// mouse gesture is flaky on CI, skip for now
test.fixme('should open when swiped', async ({ page, skip }) => {
skip.browser(
(browserName: string) => browserName !== 'chromium',
'dragElementBy is flaky outside of Chrome browsers.'
);
test('should open when swiped', async ({ page }) => {
await page.goto(`/src/components/item-sliding/test/basic`, config);
const item = page.locator('#item2');
@@ -41,7 +33,7 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
await expect(item).toHaveScreenshot(screenshot(`item-sliding-gesture`));
});
test.skip('should not scroll when the item-sliding is swiped', async ({ page, skip }) => {
test('should not scroll when the item-sliding is swiped', async ({ page, skip }) => {
skip.browser('webkit', 'mouse.wheel is not available in WebKit');
await page.goto(`/src/components/item-sliding/test/basic`, config);

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -2,6 +2,7 @@
// Item
// --------------------------------------------------
// TODO: FW-4955
:host {
/**
@@ -45,10 +46,10 @@
*
* @prop --ripple-color: Color of the item ripple effect
*
* @prop --highlight-height: The height of the highlight on the item
* @prop --highlight-color-focused: The color of the highlight on the item when focused
* @prop --highlight-color-valid: The color of the highlight on the item when valid
* @prop --highlight-color-invalid: The color of the highlight on the item when invalid
* @prop --highlight-height: The height of the highlight on the item. Only applies to inputs and textareas using the legacy form syntax. DEPRECATED: Highlights can be styled on `ion-input` or `ion-textarea` when using the modern form syntax.
* @prop --highlight-color-focused: The color of the highlight on the item when focused. Only applies to inputs and textareas using the legacy form syntax. DEPRECATED: Highlights can be styled on `ion-input` or `ion-textarea` when using the modern form syntax.
* @prop --highlight-color-valid: The color of the highlight on the item when valid. Only applies to inputs and textareas using the legacy form syntax. DEPRECATED: Highlights can be styled on `ion-input` or `ion-textarea` when using the modern form syntax.
* @prop --highlight-color-invalid: The color of the highlight on the item when invalid. Only applies to inputs and textareas using the legacy form syntax. DEPRECATED: Highlights can be styled on `ion-input` or `ion-textarea` when using the modern form syntax.
*/
--border-radius: 0px;
--border-width: 0px;

View File

@@ -73,7 +73,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
await ionModalDidPresent.next();
const modalHeader = page.locator('#modal-header');
await dragElementBy(modalHeader, page, 0, -500);
await dragElementBy(modalHeader, page, 0, 30);
const modal = page.locator('ion-modal');
expect(modal).not.toBe(null);

View File

@@ -9,12 +9,7 @@ import { CardModalPage } from '../fixtures';
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('card modal - nav'), () => {
let cardModalPage: CardModalPage;
test.beforeEach(async ({ page, skip }) => {
skip.browser(
(browserName: string) => browserName !== 'chromium',
'dragElementBy is flaky outside of Chrome browsers.'
);
test.beforeEach(async ({ page }) => {
cardModalPage = new CardModalPage(page);
await cardModalPage.navigate('/src/components/modal/test/card-nav?ionic:_testing=false', config);
});
@@ -33,7 +28,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
const content = page.locator('.page-two-content');
await dragElementBy(content, page, 1000, 0, 10);
await dragElementBy(content, page, 370, 0, 10);
await ionNavDidChange.next();
});
@@ -47,7 +42,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
await ionNavDidChange.next();
await cardModalPage.swipeToCloseModal('ion-modal ion-content.page-two-content');
await cardModalPage.swipeToCloseModal('ion-modal ion-content.page-two-content', true, 270);
});
});
});

View File

@@ -15,7 +15,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
const modal = page.locator('ion-modal');
const content = (await page.$('ion-modal ion-content'))!;
await dragElementBy(content, page, 0, 500);
await dragElementBy(content, page, 0, 300);
await content.waitForElementState('stable');

View File

@@ -30,7 +30,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
await content.evaluate((el: HTMLElement) => (el.scrollTop = 500));
await dragElementBy(content, page, 0, 500);
await dragElementBy(content, page, 0, 300);
await content.waitForElementState('stable');

View File

@@ -61,12 +61,12 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
test('it should not swipe to close when swiped on the content but the content is scrolled', async ({ page }) => {
const modal = await cardModalPage.openModalByTrigger('#card');
const content = (await page.$('ion-modal ion-content'))!;
const content = page.locator('ion-modal ion-content');
await content.evaluate((el: HTMLIonContentElement) => el.scrollToBottom(0));
await cardModalPage.swipeToCloseModal('ion-modal ion-content', false);
await content.waitForElementState('stable');
await content.waitFor();
await expect(modal).toBeVisible();
});
test('it should not swipe to close when swiped on the content but the content is scrolled even when content is replaced', async ({
@@ -76,12 +76,12 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
await page.click('ion-button.replace');
const content = (await page.$('ion-modal ion-content'))!;
const content = page.locator('ion-modal ion-content');
await content.evaluate((el: HTMLIonContentElement) => el.scrollToBottom(0));
await cardModalPage.swipeToCloseModal('ion-modal ion-content', false);
await content.waitForElementState('stable');
await content.waitFor();
await expect(modal).toBeVisible();
});
test('content should be scrollable after gesture ends', async ({ page }) => {

View File

@@ -16,7 +16,7 @@ export class CardModalPage {
this.ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
}
async openModalByTrigger(selector: string) {
await this.page.click(selector);
await this.page.locator(selector).click();
await this.ionModalDidPresent.next();
return this.page.locator('ion-modal');

View File

@@ -129,7 +129,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
const ionBreakpointDidChange = await page.spyOnEvent('ionBreakpointDidChange');
const header = page.locator('.modal-sheet ion-header');
await dragElementBy(header, page, 0, 150);
await dragElementBy(header, page, 0, 125);
await ionBreakpointDidChange.next();

View File

@@ -77,13 +77,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
expect(await scrollEl.evaluate((el: HTMLElement) => el.scrollTop)).toEqual(0);
const box = (await knobEl.boundingBox())!;
const centerX = box.x + box.width / 2;
const centerY = box.y + box.height / 2;
await page.mouse.move(centerX, centerY);
await page.mouse.down();
await page.mouse.move(centerX + 30, centerY);
await dragElementBy(knobEl, page, 30, 0, undefined, undefined, false);
/**
* Do not use scrollToBottom() or other scrolling methods

View File

@@ -10,15 +10,28 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
/**
* The mouse events are flaky on CI
*/
test.fixme('should emit start/end events', async ({ page }, testInfo) => {
await page.setContent(`<ion-range value="20"></ion-range>`, config);
test.fixme('should emit start/end events', async ({ page }) => {
/**
* Requires padding to prevent the knob from being clipped.
* If it's clipped, then the value might be one off.
* For example, if the knob is clipped on the right, then the value
* will be 99 instead of 100.
*/
await page.setContent(
`
<div style="padding: 0 20px">
<ion-range value="20"></ion-range>
</div>
`,
config
);
const rangeStart = await page.spyOnEvent('ionKnobMoveStart');
const rangeEnd = await page.spyOnEvent('ionKnobMoveEnd');
const rangeEl = page.locator('ion-range');
await dragElementBy(rangeEl, page, testInfo.project.metadata.rtl ? -300 : 300, 0);
await dragElementBy(rangeEl, page, 300, 0);
await page.waitForChanges();
/**
@@ -65,13 +78,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
expect(await scrollEl.evaluate((el: HTMLElement) => el.scrollTop)).toEqual(0);
const box = (await knobEl.boundingBox())!;
const centerX = box.x + box.width / 2;
const centerY = box.y + box.height / 2;
await page.mouse.move(centerX, centerY);
await page.mouse.down();
await page.mouse.move(centerX + 30, centerY);
await dragElementBy(knobEl, page, 30, 0, undefined, undefined, false);
/**
* Do not use scrollToBottom() or other scrolling methods
@@ -118,13 +125,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
const rangeHandle = page.locator('ion-range .range-knob-handle');
const ionChangeSpy = await page.spyOnEvent('ionChange');
const boundingBox = await rangeHandle.boundingBox();
await rangeHandle.hover();
await page.mouse.down();
await page.mouse.move(boundingBox!.x + 100, boundingBox!.y);
await page.mouse.up();
await dragElementBy(rangeHandle, page, 100, 0);
await ionChangeSpy.next();
@@ -169,11 +170,9 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
const rangeHandle = page.locator('ion-range .range-knob-handle');
const ionInputSpy = await page.spyOnEvent('ionInput');
const boundingBox = await rangeHandle.boundingBox();
await rangeHandle.hover();
await page.mouse.down();
await page.mouse.move(boundingBox!.x + 100, boundingBox!.y);
await dragElementBy(rangeHandle, page, 100, 0, undefined, undefined, false);
await ionInputSpy.next();

View File

@@ -3,13 +3,11 @@ import { configs, test } from '@utils/test/playwright';
import { pullToRefresh } from '../test.utils';
// TODO FW-2795: Enable this test when touch events/gestures are better supported in Playwright
/**
* This behavior does not vary across directions.
*/
configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe.skip(title('refresher: basic'), () => {
test.describe(title('refresher: basic'), () => {
test.beforeEach(async ({ page }) => {
await page.goto('/src/components/refresher/test/basic', config);
});

View File

@@ -3,12 +3,11 @@ import { configs, test } from '@utils/test/playwright';
import { pullToRefresh } from '../test.utils';
// TODO FW-2795: Enable this test when touch events/gestures are better supported in Playwright
/**
* This behavior does not vary across directions.
*/
configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe.skip(title('refresher: custom scroll target'), () => {
test.describe(title('refresher: custom scroll target'), () => {
test.beforeEach(async ({ page }) => {
await page.goto('/src/components/refresher/test/scroll-target', config);
});
@@ -19,7 +18,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
expect(await items.count()).toBe(30);
await pullToRefresh(page, '#inner-scroll');
await pullToRefresh(page);
expect(await items.count()).toBe(60);
});
@@ -39,7 +38,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
expect(await items.count()).toBe(30);
await pullToRefresh(page, '#inner-scroll');
await pullToRefresh(page);
expect(await items.count()).toBe(60);
});

View File

@@ -18,7 +18,7 @@ const pullToRefresh = async (page: E2EPage, selector = 'body') => {
const ev = await page.spyOnEvent('ionRefreshComplete');
await dragElementByYAxis(target, page, 400);
await dragElementByYAxis(target, page, 320);
await ev.next();
};

View File

@@ -1,18 +1,12 @@
import { expect } from '@playwright/test';
import { configs, test, dragElementBy } from '@utils/test/playwright';
// TODO FW-3079
/**
* Reorder group does not have per-mode styles
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe.skip(title('reorder group: interactive'), () => {
test.beforeEach(async ({ page, skip }) => {
skip.browser(
(browserName: string) => browserName !== 'chromium',
'dragElementBy is flaky outside of Chrome browsers.'
);
test.describe(title('reorder group: interactive'), () => {
test.beforeEach(async ({ page }) => {
await page.goto(`/src/components/reorder-group/test/interactive`, config);
});
test('should drag and drop when ion-reorder wraps ion-item', async ({ page }) => {

View File

@@ -1,17 +1,12 @@
import { expect } from '@playwright/test';
import { configs, test, dragElementBy } from '@utils/test/playwright';
// TODO FW-3079
/**
* Reorder group does not have per-mode styles
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe.skip(title('reorder group: nested'), () => {
test.beforeEach(async ({ page, skip }) => {
skip.browser(
(browserName: string) => browserName !== 'chromium',
'dragElementBy is flaky outside of Chrome browsers.'
);
test.describe(title('reorder group: nested'), () => {
test.beforeEach(async ({ page }) => {
await page.goto(`/src/components/reorder-group/test/nested`, config);
});
test('should drag and drop when ion-reorder wraps ion-item', async ({ page }) => {

View File

@@ -1,17 +1,12 @@
import { expect } from '@playwright/test';
import { configs, test, dragElementBy } from '@utils/test/playwright';
// TODO FW-3079
/**
* Reorder group does not have per-mode styles
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe.skip(title('reorder group: scroll-target'), () => {
test.beforeEach(async ({ page, skip }) => {
skip.browser(
(browserName: string) => browserName !== 'chromium',
'dragElementBy is flaky outside of Chrome browsers.'
);
test.describe(title('reorder group: scroll-target'), () => {
test.beforeEach(async ({ page }) => {
await page.goto(`/src/components/reorder-group/test/scroll-target`, config);
});
test('should drag and drop when ion-reorder wraps ion-item', async ({ page }) => {

View File

@@ -12,3 +12,23 @@ configs().forEach(({ title, screenshot, config }) => {
});
});
});
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('title: special characters'), () => {
test('should not cut off characters', async ({ page }) => {
await page.setContent(
`
<ion-header>
<ion-toolbar>
<ion-title size="large">ÔÔÔ</ion-title>
</ion-toolbar>
</ion-header>
`,
config
);
const title = page.locator('ion-title');
await expect(title).toHaveScreenshot(screenshot('title-characters'));
});
});
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -49,8 +49,6 @@
min-width: 100%;
padding-bottom: 6px;
font-size: 34px;
font-weight: 700;

View File

@@ -84,6 +84,16 @@
// Toolbar: Large Title
// --------------------------------------------------
/**
* Ensure there is enough spacing between the large
* title and any potential content in subsequent
* toolbars. We do not add padding to the title
* itself because it is absolutely positioned.
*/
:host(.toolbar-title-large) {
padding-bottom: 7px;
}
:host(.toolbar-title-large) .toolbar-container {
flex-wrap: wrap;
align-items: flex-start;
@@ -94,4 +104,4 @@
order: map-get($toolbar-order-ios, title-large);
min-width: 100%;
}
}

View File

@@ -1,7 +1,13 @@
import { doc } from '@utils/browser';
import type { Config } from '../../interface';
import { now, pointerCoord } from '../helpers';
export const startTapClick = (config: Config) => {
if (doc === undefined) {
return;
}
let lastTouch = -MOUSE_WAIT * 10;
let lastActivated = 0;
@@ -143,7 +149,6 @@ export const startTapClick = (config: Config) => {
}
};
const doc = document;
doc.addEventListener('ionGestureCaptured', cancelActive);
doc.addEventListener('touchstart', onTouchStart, true);

View File

@@ -1,3 +1,11 @@
/**
* The drag gesture will not operate as expected when the element is dragged outside of the viewport because the Mouse class does not fire events outside of the viewport.
*
* For example, if the mouse is moved outside of the viewport, then the `mouseup` event will not fire.
*
* See https://playwright.dev/docs/api/class-mouse#mouse-move for more information.
*/
import type { ElementHandle, Locator } from '@playwright/test';
import type { E2EPage } from './';
@@ -8,7 +16,8 @@ export const dragElementBy = async (
dragByX = 0,
dragByY = 0,
startXCoord?: number,
startYCoord?: number
startYCoord?: number,
releaseDrag = true
) => {
const boundingBox = await el.boundingBox();
@@ -21,14 +30,17 @@ export const dragElementBy = async (
const startX = startXCoord === undefined ? boundingBox.x + boundingBox.width / 2 : startXCoord;
const startY = startYCoord === undefined ? boundingBox.y + boundingBox.height / 2 : startYCoord;
const endX = startX + dragByX;
const endY = startY + dragByY;
// Navigate to the start position.
await page.mouse.move(startX, startY);
await page.mouse.down();
await page.mouse.move(endX, endY, { steps: 10 });
await page.mouse.up();
// Drag the element.
await moveElement(page, startX, startY, dragByX, dragByY);
if (releaseDrag) {
await page.mouse.up();
}
};
/**
@@ -55,12 +67,83 @@ export const dragElementByYAxis = async (
const startX = boundingBox.x + boundingBox.width / 2;
const startY = startYCoord === undefined ? boundingBox.y + boundingBox.height / 2 : startYCoord;
// Navigate to the start position.
await page.mouse.move(startX, startY);
await page.mouse.down();
for (let i = 0; i < dragByY; i += 20) {
await page.mouse.move(startX, startY + i);
}
// Drag the element.
await moveElement(page, startX, startY, 0, dragByY);
await page.mouse.up();
};
const validateDragByX = (startX: number, dragByX: number, viewportWidth: number) => {
const endX = startX + dragByX;
// The element is being dragged past the right of the viewport.
if (endX > viewportWidth) {
const recommendedDragByX = viewportWidth - startX - 5;
throw new Error(
`The element is being dragged past the right of the viewport. Update the dragByX value to prevent going out of bounds. A recommended value is ${recommendedDragByX}.`
);
}
// The element is being dragged past the left of the viewport.
if (endX < 0) {
const recommendedDragByX = startX - 5;
throw new Error(
`The element is being dragged past the left of the viewport. Update the dragByX value to prevent going out of bounds. A recommended value is ${recommendedDragByX}.`
);
}
};
const validateDragByY = (startY: number, dragByY: number, viewportHeight: number) => {
const endY = startY + dragByY;
// The element is being dragged past the bottom of the viewport.
if (endY > viewportHeight) {
const recommendedDragByY = viewportHeight - startY - 5;
throw new Error(
`The element is being dragged past the bottom of the viewport. Update the dragByY value to prevent going out of bounds. A recommended value is ${recommendedDragByY}.`
);
}
// The element is being dragged past the top of the viewport.
if (endY < 0) {
const recommendedDragByY = startY - 5;
throw new Error(
`The element is being dragged past the top of the viewport. Update the dragByY value to prevent going out of bounds. A recommended value is ${recommendedDragByY}.`
);
}
};
const moveElement = async (page: E2EPage, startX: number, startY: number, dragByX = 0, dragByY = 0) => {
const steps = 10;
const browser = page.context().browser()!.browserType().name();
const viewport = page.viewportSize();
if (viewport === null) {
throw new Error(
'Cannot get viewport size. See https://playwright.dev/docs/api/class-page#page-viewport-size for more information'
);
}
validateDragByX(startX, dragByX, viewport.width);
validateDragByY(startY, dragByY, viewport.height);
const endX = startX + dragByX;
const endY = startY + dragByY;
// Drag the element.
for (let i = 1; i <= steps; i++) {
const middleX = startX + (endX - startX) * (i / steps);
const middleY = startY + (endY - startY) * (i / steps);
await page.mouse.move(middleX, middleY);
// Safari needs to wait for a repaint to occur before moving the mouse again.
if (browser === 'webkit' && i % 2 === 0) {
// Repainting every 2 steps is enough to keep the drag gesture smooth.
// Anything past 4 steps will cause the drag gesture to be flaky.
await page.evaluate(() => new Promise(requestAnimationFrame));
}
}
};