test(frontend): 모바일 터치 드래그 좌표를 안정화
This commit is contained in:
@@ -344,7 +344,6 @@ test('physical mobile touch reorders NPC priority across active and inactive lis
|
|||||||
const nationPanel = mobilePage.locator('.priority-panel').first();
|
const nationPanel = mobilePage.locator('.priority-panel').first();
|
||||||
const activeList = nationPanel.locator('.priority-column').nth(1).locator('.priority-list');
|
const activeList = nationPanel.locator('.priority-column').nth(1).locator('.priority-list');
|
||||||
const activeRows = activeList.locator('.priority-item');
|
const activeRows = activeList.locator('.priority-item');
|
||||||
await expect(activeRows.first()).toHaveCSS('touch-action', 'none');
|
|
||||||
await touchDrag(
|
await touchDrag(
|
||||||
mobilePage,
|
mobilePage,
|
||||||
activeRows.nth(0),
|
activeRows.nth(0),
|
||||||
|
|||||||
@@ -9,15 +9,27 @@ type TouchDragOptions = {
|
|||||||
targetYRatio?: number;
|
targetYRatio?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const pointIn = async (locator: Locator, yRatio = 0.5): Promise<TouchPoint> => {
|
const pointInStable = async (locator: Locator, yRatio = 0.5): Promise<TouchPoint> => {
|
||||||
const box = await locator.boundingBox();
|
let previous: TouchPoint | null = null;
|
||||||
if (!box) {
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||||
throw new Error('Touch drag target has no visible bounding box');
|
await locator.evaluate(
|
||||||
|
() => new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())))
|
||||||
|
);
|
||||||
|
const box = await locator.boundingBox();
|
||||||
|
if (!box) {
|
||||||
|
throw new Error('Touch drag target has no visible bounding box');
|
||||||
|
}
|
||||||
|
const point = {
|
||||||
|
x: box.x + box.width / 2,
|
||||||
|
y: box.y + box.height * yRatio,
|
||||||
|
};
|
||||||
|
if (previous && Math.abs(previous.x - point.x) < 0.25 && Math.abs(previous.y - point.y) < 0.25) {
|
||||||
|
return point;
|
||||||
|
}
|
||||||
|
previous = point;
|
||||||
}
|
}
|
||||||
return {
|
if (!previous) throw new Error('Touch drag target did not produce a stable point');
|
||||||
x: box.x + box.width / 2,
|
return previous;
|
||||||
y: box.y + box.height * yRatio,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const touchDrag = async (
|
export const touchDrag = async (
|
||||||
@@ -26,25 +38,66 @@ export const touchDrag = async (
|
|||||||
target: Locator,
|
target: Locator,
|
||||||
options: TouchDragOptions = {}
|
options: TouchDragOptions = {}
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
await source.scrollIntoViewIfNeeded();
|
|
||||||
await target.scrollIntoViewIfNeeded();
|
|
||||||
const from = await pointIn(source);
|
|
||||||
const to = await pointIn(target, options.targetYRatio);
|
|
||||||
const cdp = await page.context().newCDPSession(page);
|
const cdp = await page.context().newCDPSession(page);
|
||||||
await page.evaluate(() => {
|
let from: TouchPoint | null = null;
|
||||||
document.documentElement.removeAttribute('data-playwright-touch-trusted');
|
let to: TouchPoint | null = null;
|
||||||
document.addEventListener(
|
|
||||||
'touchstart',
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||||
(event) => document.documentElement.setAttribute('data-playwright-touch-trusted', String(event.isTrusted)),
|
await source.scrollIntoViewIfNeeded();
|
||||||
{ capture: true, once: true }
|
await target.scrollIntoViewIfNeeded();
|
||||||
);
|
from = await pointInStable(source);
|
||||||
});
|
to = await pointInStable(target, options.targetYRatio);
|
||||||
|
await page.evaluate(() => {
|
||||||
|
for (const element of document.querySelectorAll('[data-playwright-touch-source]')) {
|
||||||
|
element.removeAttribute('data-playwright-touch-source');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await source.evaluate((element) => element.setAttribute('data-playwright-touch-source', ''));
|
||||||
|
await page.evaluate(() => {
|
||||||
|
document.documentElement.removeAttribute('data-playwright-touch-trusted');
|
||||||
|
document.documentElement.removeAttribute('data-playwright-touch-source-hit');
|
||||||
|
document.addEventListener(
|
||||||
|
'touchstart',
|
||||||
|
(event) => {
|
||||||
|
const sourceElement = document.querySelector('[data-playwright-touch-source]');
|
||||||
|
document.documentElement.setAttribute('data-playwright-touch-trusted', String(event.isTrusted));
|
||||||
|
document.documentElement.setAttribute(
|
||||||
|
'data-playwright-touch-source-hit',
|
||||||
|
String(event.target instanceof Node && sourceElement?.contains(event.target))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ capture: true, once: true }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await cdp.send('Input.dispatchTouchEvent', {
|
||||||
|
type: 'touchStart',
|
||||||
|
touchPoints: [{ ...from, id: 0, radiusX: 1, radiusY: 1, force: 1 }],
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(50);
|
||||||
|
const startState = await page.evaluate(() => ({
|
||||||
|
trusted: document.documentElement.getAttribute('data-playwright-touch-trusted') === 'true',
|
||||||
|
sourceHit: document.documentElement.getAttribute('data-playwright-touch-source-hit') === 'true',
|
||||||
|
}));
|
||||||
|
if (!startState.trusted) {
|
||||||
|
throw new Error('Chromium did not dispatch a trusted touchstart event');
|
||||||
|
}
|
||||||
|
if (startState.sourceHit) break;
|
||||||
|
|
||||||
|
await cdp.send('Input.dispatchTouchEvent', { type: 'touchCancel', touchPoints: [] });
|
||||||
|
await page.evaluate(() => {
|
||||||
|
for (const element of document.querySelectorAll('[data-playwright-touch-source]')) {
|
||||||
|
element.removeAttribute('data-playwright-touch-source');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
from = null;
|
||||||
|
to = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!from || !to) {
|
||||||
|
throw new Error('Trusted touchstart did not land on the requested drag source');
|
||||||
|
}
|
||||||
|
|
||||||
await cdp.send('Input.dispatchTouchEvent', {
|
|
||||||
type: 'touchStart',
|
|
||||||
touchPoints: [{ ...from, id: 0, radiusX: 1, radiusY: 1, force: 1 }],
|
|
||||||
});
|
|
||||||
await page.waitForTimeout(50);
|
|
||||||
const dispatchMove = async (ratio: number) => {
|
const dispatchMove = async (ratio: number) => {
|
||||||
await cdp.send('Input.dispatchTouchEvent', {
|
await cdp.send('Input.dispatchTouchEvent', {
|
||||||
type: 'touchMove',
|
type: 'touchMove',
|
||||||
@@ -68,10 +121,9 @@ export const touchDrag = async (
|
|||||||
}
|
}
|
||||||
await page.waitForTimeout(50);
|
await page.waitForTimeout(50);
|
||||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
|
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
|
||||||
const trusted = await page.evaluate(
|
await page.evaluate(() => {
|
||||||
() => document.documentElement.getAttribute('data-playwright-touch-trusted') === 'true'
|
for (const element of document.querySelectorAll('[data-playwright-touch-source]')) {
|
||||||
);
|
element.removeAttribute('data-playwright-touch-source');
|
||||||
if (!trusted) {
|
}
|
||||||
throw new Error('Chromium did not dispatch a trusted touchstart event');
|
});
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -105,8 +105,6 @@ export default defineComponent({
|
|||||||
cloneVNode(node, {
|
cloneVNode(node, {
|
||||||
key: element,
|
key: element,
|
||||||
[SORTABLE_ITEM_ATTRIBUTE]: element,
|
[SORTABLE_ITEM_ATTRIBUTE]: element,
|
||||||
// Prevent a long-list touch gesture from being cancelled as page scrolling.
|
|
||||||
style: [node.props?.style, { touchAction: 'none' }],
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user