fix(KAN-71): correct Apple navigation gestures and scroll bounds
CI/CD / preconditions (pull_request) Successful in 2s
Native JavaScript checks / Native launcher and release tests (pull_request) Successful in 1m4s
macOS checks / Native Rust tests and unsigned build (pull_request) Successful in 1m31s
CI/CD / test_and_check_app (pull_request) Successful in 6m44s
CI/CD / Publish immutable image (pull_request) Skipped

This commit is contained in:
2026-09-29 07:22:45 +02:00
parent 0662a6970e
commit 4afaa79834
12 changed files with 784 additions and 44 deletions
+151
View File
@@ -0,0 +1,151 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { createServer } from 'node:http';
import process from 'node:process';
const { chromium } = await import(
process.env.PLAYWRIGHT_MODULE ?? 'playwright'
);
const script = await readFile(
new URL('../ui/workspace-navigation.js', import.meta.url),
'utf8',
);
const server = createServer((request, response) => {
response.setHeader('Content-Type', 'text/html');
response.end(`<!doctype html><html><head><meta name="viewport" content="width=device-width,initial-scale=1"><style>
body { margin: 0; } #board { overflow-x: auto; width: 300px; height: 100px; }
#board > div { width: 1500px; height: 100px; } dialog { max-height: 200px; overflow-y: auto; }
</style></head><body><p id="page">${request.url}</p><input id="draft" value="retained"><div id="board"><div>Board columns</div></div>
<dialog><button id="close">Close</button><div style="height:1000px">Long dialog</div></dialog><div id="long"></div></body></html>`);
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const browser = await chromium.launch({ channel: 'chrome', headless: true });
const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
await page.addInitScript(script);
const root = `http://127.0.0.1:${server.address().port}`;
try {
await page.goto(root);
const length = await page.evaluate(() => history.length);
for (const path of ['/board', '/ticket', '/settings']) {
await page.evaluate(
(path) =>
history.pushState(
{
page: { url: path },
rememberedState: { draft: 'retained' },
},
'',
path,
),
path,
);
}
await page.evaluate(() => {
window.steps = 0;
addEventListener('popstate', () => window.steps++);
});
await page.evaluate(() => window.__kanbanNavigation.step(-1));
await page.waitForFunction(() => location.pathname === '/ticket');
assert.equal(await page.evaluate(() => window.steps), 1);
await page.evaluate(() => window.__kanbanNavigation.step(-1));
await page.waitForFunction(() => location.pathname === '/board');
await page.evaluate(() => window.__kanbanNavigation.step(1));
await page.waitForFunction(() => location.pathname === '/ticket');
assert.equal(await page.evaluate(() => window.steps), 3);
assert.equal(await page.evaluate(() => history.length), length + 3);
assert.equal(await page.locator('#draft').inputValue(), 'retained');
assert.equal(
await page.evaluate(() => history.state.rememberedState.draft),
'retained',
);
await page.goto(`${root}/full-page`);
await page.evaluate(() => window.__kanbanNavigation.step(-1));
await page.waitForURL(`${root}/ticket`);
await page.evaluate(() => window.__kanbanNavigation.step(1));
await page.waitForURL(`${root}/full-page`);
await page.locator('dialog').evaluate((dialog) => dialog.showModal());
assert.equal(
await page.evaluate(() => window.__kanbanNavigation.canSwipe(1, 1)),
false,
);
await page.locator('dialog').evaluate((dialog) => dialog.close());
for (const selector of ['#board', '#draft']) {
const point = await page.locator(selector).boundingBox();
assert.equal(
await page.evaluate(
({ x, y }) => window.__kanbanNavigation.canSwipe(x + 5, y + 5),
point,
),
false,
);
}
assert.equal(
await page.evaluate(() => window.__kanbanNavigation.canSwipe(1, 1)),
true,
);
await page.dispatchEvent('#board', 'dragstart');
assert.equal(
await page.evaluate(() => window.__kanbanNavigation.canSwipe(1, 1)),
false,
);
await page.dispatchEvent('#board', 'dragend');
assert.equal(
await page.evaluate(() => window.__kanbanNavigation.canSwipe(1, 1)),
true,
);
assert.equal(
await page.evaluate(
() =>
getComputedStyle(document.documentElement).overscrollBehaviorY,
),
'none',
);
assert.equal(
await page.evaluate(
() =>
getComputedStyle(document.documentElement).overscrollBehaviorX,
),
'auto',
);
assert.equal(
await page.evaluate(() => document.documentElement.scrollHeight),
844,
);
await page.mouse.move(380, 400);
await page.mouse.wheel(0, -1000);
await page.waitForTimeout(200);
assert.equal(await page.evaluate(() => scrollY), 0);
await page
.locator('#long')
.evaluate((element) => (element.style.height = '3000px'));
await page.mouse.wheel(0, 1000);
await page.waitForFunction(() => scrollY > 0);
await page.evaluate(() =>
scrollTo(0, document.documentElement.scrollHeight),
);
await page.mouse.wheel(0, 5000);
await page.waitForTimeout(200);
assert.equal(
await page.evaluate(() => scrollY + innerHeight),
await page.evaluate(() => document.documentElement.scrollHeight),
);
await page.evaluate(() => scrollTo(0, 0));
await page.locator('#board').hover();
await page.mouse.wheel(500, 0);
await page.waitForFunction(
() => document.querySelector('#board').scrollLeft > 0,
);
assert.equal(new URL(page.url()).pathname, '/full-page');
await page.locator('dialog').evaluate((dialog) => dialog.showModal());
await page.locator('dialog').hover();
await page.mouse.wheel(0, 500);
await page.waitForFunction(
() => document.querySelector('dialog').scrollTop > 0,
);
console.log(
'PASS: SPA/full-page history, forward history, overlays, short/long scroll bounds, horizontal board, drag and input guards',
);
} finally {
await browser.close();
await new Promise((resolve) => server.close(resolve));
}