mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
fix(street-level): modal viewer, pinned redirects, real-route CI checks
From the IC8 review of #768, each with tests that fail before: - P2: the expanded viewer is a real modal: while it is on screen every other <body> child is inert (only the ones it set are restored, on shrink, close and destroy, and when Clean View/recording/cockpit hide it). The voice pill and toasts are not clickable while it is expanded. - P2: the tile proxy follows redirects by hand, at most 3 hops, each one HTTPS on exactly https://tiles.mapillary.com without credentials; anything else is a 502 and the token never leaves that origin. - P2: the real dev and preview server smoke tests request /api/mapillary/status and a tile, and the browser gate first checks the server's real status route before any interception, so CI fails if the Mapillary plugin is not registered. - P2: both gates press panel headers through one shared helper (scripts/qa-panelDrag.mjs): re-resolve the header until it is under the pointer, record what the press hit, wait on the real floating/docked state, and retry only a press that provably missed the header. Verified: unit 5910 pass; fixture gate 17/17; panel gate 5/5; live gate with a real Mapillary token 22/22. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
bd97d38a99
commit
038d8facc2
12 files changed
+881
-97
No files matched your search
@@ -37,6 +37,7 @@
|
||||
"docs/APPLICATION.md",
|
||||
"scripts/qa-application.mjs",
|
||||
"scripts/qa-panel-resize.mjs",
|
||||
"scripts/qa-panelDrag.mjs",
|
||||
"src/tooling/panelResizeQa.test.mjs",
|
||||
"scripts/qa-terrain-429.mjs",
|
||||
"src/app/application.js",
|
||||
@@ -915,6 +916,7 @@
|
||||
"src/layers/streetLevel/cameraFollow.test.mjs",
|
||||
"src/ui/streetLevelControls.test.mjs",
|
||||
"src/ui/panelDock.test.mjs",
|
||||
"src/layers/streetLevel/marker.test.mjs",
|
||||
"src/layers/streetLevel/viewerHost.test.mjs",
|
||||
"src/layers/streetLevel/providers/mapillary/sequences.test.mjs",
|
||||
"src/layers/streetLevel/selection.test.mjs",
|
||||
|
||||
+21
-74
@@ -8,6 +8,12 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { RESIZE_DIRECTIONS, resizeBox } from '../src/ui/panelResize.js';
|
||||
import {
|
||||
describePress,
|
||||
dockPanelByDoubleClick,
|
||||
dragPointer,
|
||||
liftPanelByHeader,
|
||||
} from './qa-panelDrag.mjs';
|
||||
|
||||
export const PANEL_ID = 'cctv-panel';
|
||||
export const STORAGE_KEY = `godsEyeView.v8.panelPos.${PANEL_ID}`;
|
||||
@@ -170,14 +176,7 @@ async function main() {
|
||||
}, PANEL_ID);
|
||||
return rect;
|
||||
};
|
||||
const drag = async (from, dx, dy) => {
|
||||
await page.mouse.move(from.x, from.y);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(from.x + dx / 2, from.y + dy / 2, { steps: 6 });
|
||||
await page.mouse.move(from.x + dx, from.y + dy, { steps: 6 });
|
||||
await page.mouse.up();
|
||||
await new Promise((resolve) => setTimeout(resolve, 120));
|
||||
};
|
||||
const drag = (from, dx, dy) => dragPointer(page, from, dx, dy);
|
||||
|
||||
await page.goto(`${url}/`, { waitUntil: 'domcontentloaded' });
|
||||
await page.evaluate((key) => {
|
||||
@@ -223,66 +222,23 @@ async function main() {
|
||||
return out.join(' ');
|
||||
}, PANEL_ID);
|
||||
const aboveAtDock = await railAbove();
|
||||
/** One header drag; reports what the press landed on. */
|
||||
const tryLift = async () => {
|
||||
await page.evaluate((id) => {
|
||||
window.addEventListener(
|
||||
'pointerdown',
|
||||
(event) => {
|
||||
const r = document.getElementById(id).getBoundingClientRect();
|
||||
const target = event.target;
|
||||
window.__qaPressBox = {
|
||||
left: r.left,
|
||||
top: r.top,
|
||||
width: r.width,
|
||||
// What the press landed on, so a failed lift says why.
|
||||
target: `${target.tagName?.toLowerCase()}${target.id ? `#${target.id}` : ''}.${[...(target.classList || [])].join('.')}`,
|
||||
inHeader: Boolean(target.closest?.(`#${id} .panel-header`)),
|
||||
};
|
||||
},
|
||||
{ capture: true, once: true },
|
||||
);
|
||||
}, PANEL_ID);
|
||||
// Press only where the header really is: if the rail is still shifting,
|
||||
// wait (bounded) for the title point to land on the CCTV header.
|
||||
let grab = await headerPoint();
|
||||
for (let tries = 0; tries < 20; tries++) {
|
||||
const onHeader = await page.evaluate(
|
||||
({ x, y, id }) =>
|
||||
Boolean(
|
||||
document.elementFromPoint(x, y)?.closest(`#${id} .panel-header`),
|
||||
),
|
||||
{ ...grab, id: PANEL_ID },
|
||||
);
|
||||
if (onHeader) break;
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
grab = await headerPoint();
|
||||
}
|
||||
await drag(grab, -200, -100);
|
||||
return {
|
||||
grab,
|
||||
lifted: await readPanel(),
|
||||
pressed: await page.evaluate(() => window.__qaPressBox),
|
||||
};
|
||||
};
|
||||
// A press the rail shifted out from under the header (seen on the CI
|
||||
// Press only where the header really is, recording what the press landed
|
||||
// on. A press the rail shifted out from under the header (seen on the CI
|
||||
// runner: something above CCTV grows ~32 px after load) proves nothing
|
||||
// about the drag, so it is retried; a press that landed in the header and
|
||||
// did not lift fails at once.
|
||||
let attempt = await tryLift();
|
||||
for (let retry = 0; retry < 2; retry++) {
|
||||
if (attempt.lifted.floating || attempt.pressed?.inHeader) break;
|
||||
console.log(
|
||||
`note: the press missed the header (on ${attempt.pressed?.target}; rail above CCTV ${aboveAtDock} -> ${await railAbove()}); retrying`,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
attempt = await tryLift();
|
||||
}
|
||||
const { grab, lifted, pressed } = attempt;
|
||||
// did not lift fails at once (scripts/qa-panelDrag.mjs).
|
||||
const attempt = await liftPanelByHeader(page, PANEL_ID, {
|
||||
dx: -200,
|
||||
dy: -100,
|
||||
note: async () =>
|
||||
`rail above CCTV ${aboveAtDock} -> ${await railAbove()}`,
|
||||
});
|
||||
const { pressed } = attempt;
|
||||
const lifted = await readPanel();
|
||||
assert.equal(
|
||||
lifted.floating,
|
||||
true,
|
||||
`a header drag lifts the panel out (pressed ${grab.x},${grab.y} on ${pressed?.target}, in header: ${pressed?.inHeader}; panel docked at ${docked.top}, at press ${pressed?.top})`,
|
||||
`a header drag lifts the panel out (${describePress(attempt)}; panel docked at ${docked.top}, at press ${pressed?.top})`,
|
||||
);
|
||||
assert.equal(
|
||||
lifted.collapsed,
|
||||
@@ -397,21 +353,12 @@ async function main() {
|
||||
console.log('PASS: reload restores position and size');
|
||||
|
||||
// Double-click the header to snap back.
|
||||
const point = await headerPoint();
|
||||
const headerHit = await page.evaluate(({ x, y }) => {
|
||||
const node = document.elementFromPoint(x, y);
|
||||
return node
|
||||
? `${node.tagName.toLowerCase()}${node.id ? `#${node.id}` : ''}.${[...node.classList].join('.')}`
|
||||
: 'nothing';
|
||||
}, point);
|
||||
await page.mouse.click(point.x, point.y, { clickCount: 1 });
|
||||
await page.mouse.click(point.x, point.y, { clickCount: 2 });
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
const dock = await dockPanelByDoubleClick(page, PANEL_ID);
|
||||
const snapped = await readPanel();
|
||||
assert.equal(
|
||||
snapped.floating,
|
||||
false,
|
||||
`double-click snaps back (pointer on ${headerHit})`,
|
||||
`double-click snaps back (${describePress(dock)})`,
|
||||
);
|
||||
assert.equal(snapped.inRail, true);
|
||||
assert.equal(snapped.inlineWidth, '');
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* Header press-and-verify for the browser QA gates that lift a rail panel
|
||||
* into a floating window (header drag) and dock it again (header
|
||||
* double-click). The rail animates panel heights, so a point read once can
|
||||
* be under another panel by the time the pointer goes down. Each attempt
|
||||
* re-resolves the header, presses only where the header really is, records
|
||||
* what the press landed on, and waits for the panel's actual state. Only a
|
||||
* provable miss (a press that landed outside the header) is retried; a press
|
||||
* in the header that did not lift or dock is a real failure.
|
||||
*/
|
||||
|
||||
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
/** Retries after the first attempt, and the pause before each. */
|
||||
export const PRESS_RETRIES = 2;
|
||||
const RETRY_PAUSE_MS = 1000;
|
||||
|
||||
/**
|
||||
* Whether an attempt proves nothing about the panel: it did not reach the
|
||||
* wanted state and the press demonstrably landed outside the header.
|
||||
* @param {{done: boolean, pressed: {inHeader: boolean}|null}} attempt
|
||||
*/
|
||||
export function pressMissed(attempt) {
|
||||
return !attempt.done && attempt.pressed?.inHeader !== true;
|
||||
}
|
||||
|
||||
/** The centre of the panel's header title, read now. */
|
||||
export function headerPoint(page, panelId) {
|
||||
return page.evaluate((id) => {
|
||||
const title = document.querySelector(`#${id} .panel-header .panel-title`);
|
||||
const r = title.getBoundingClientRect();
|
||||
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
||||
}, panelId);
|
||||
}
|
||||
|
||||
/** Name the element at a viewport point, and whether it is the panel's header. */
|
||||
export function hitAt(page, point, panelId) {
|
||||
return page.evaluate(
|
||||
({ x, y, id }) => {
|
||||
const node = document.elementFromPoint(x, y);
|
||||
if (!node) return { target: 'nothing', inHeader: false };
|
||||
return {
|
||||
target: `${node.tagName.toLowerCase()}${node.id ? `#${node.id}` : ''}.${[...node.classList].join('.')}`,
|
||||
inHeader: Boolean(node.closest(`#${id} .panel-header`)),
|
||||
};
|
||||
},
|
||||
{ ...point, id: panelId },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The header point to press, re-resolved until it lands on the header (or the
|
||||
* bounded wait runs out; the press then records the miss).
|
||||
*/
|
||||
export async function headerTarget(
|
||||
page,
|
||||
panelId,
|
||||
{ tries = 20, intervalMs = 500 } = {},
|
||||
) {
|
||||
let point = await headerPoint(page, panelId);
|
||||
let hit = await hitAt(page, point, panelId);
|
||||
for (let i = 0; i < tries && !hit.inHeader; i++) {
|
||||
await delay(intervalMs);
|
||||
point = await headerPoint(page, panelId);
|
||||
hit = await hitAt(page, point, panelId);
|
||||
}
|
||||
return { point, hit };
|
||||
}
|
||||
|
||||
/**
|
||||
* Best effort: wait until the panel's box (to the whole pixel) has held still
|
||||
* for `stillMs`, so a rail still animating heights does not move the header
|
||||
* between the hit check and the press. Never fails the run.
|
||||
*/
|
||||
export async function waitForStill(
|
||||
page,
|
||||
panelId,
|
||||
{ stillMs = 300, timeout = 5000 } = {},
|
||||
) {
|
||||
try {
|
||||
await page.waitForFunction(
|
||||
(id, ms) => {
|
||||
const r = document.getElementById(id).getBoundingClientRect();
|
||||
const box = [r.left, r.top, r.width, r.height].map(Math.round).join();
|
||||
const now = performance.now();
|
||||
if (window.__qaStillBox !== box) {
|
||||
window.__qaStillBox = box;
|
||||
window.__qaStillSince = now;
|
||||
}
|
||||
return now - window.__qaStillSince >= ms;
|
||||
},
|
||||
{ polling: 100, timeout },
|
||||
panelId,
|
||||
stillMs,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error?.name !== 'TimeoutError') throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record what the next pointerdown lands on, with the panel's box at that
|
||||
* moment, in `window.__qaPressBox` (null until a press arrives).
|
||||
*/
|
||||
export function recordNextPress(page, panelId) {
|
||||
return page.evaluate((id) => {
|
||||
window.__qaPressBox = null;
|
||||
window.addEventListener(
|
||||
'pointerdown',
|
||||
(event) => {
|
||||
const r = document.getElementById(id).getBoundingClientRect();
|
||||
const target = event.target;
|
||||
window.__qaPressBox = {
|
||||
left: r.left,
|
||||
top: r.top,
|
||||
width: r.width,
|
||||
// What the press landed on, so a failed press says why.
|
||||
target: `${target.tagName?.toLowerCase()}${target.id ? `#${target.id}` : ''}.${[...(target.classList || [])].join('.')}`,
|
||||
inHeader: Boolean(target.closest?.(`#${id} .panel-header`)),
|
||||
};
|
||||
},
|
||||
{ capture: true, once: true },
|
||||
);
|
||||
}, panelId);
|
||||
}
|
||||
|
||||
/** The press recorded by `recordNextPress`, or null. */
|
||||
function readPress(page) {
|
||||
return page.evaluate(() => window.__qaPressBox ?? null);
|
||||
}
|
||||
|
||||
/** Wait (bounded) until the panel is, or is not, a floating window. */
|
||||
export async function waitForFloating(
|
||||
page,
|
||||
panelId,
|
||||
floating,
|
||||
{ timeout = 3000 } = {},
|
||||
) {
|
||||
try {
|
||||
await page.waitForFunction(
|
||||
(id, want) =>
|
||||
document.getElementById(id).classList.contains('panel-floating') ===
|
||||
want,
|
||||
{ polling: 50, timeout },
|
||||
panelId,
|
||||
floating,
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error?.name !== 'TimeoutError') throw error;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Press at `from`, move by (dx, dy) in two legs, release. */
|
||||
export async function dragPointer(page, from, dx, dy, { steps = 6 } = {}) {
|
||||
await page.mouse.move(from.x, from.y);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(from.x + dx / 2, from.y + dy / 2, { steps });
|
||||
await page.mouse.move(from.x + dx, from.y + dy, { steps });
|
||||
await page.mouse.up();
|
||||
await delay(120);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run `press` (one attempt at the header) until it reaches the wanted state,
|
||||
* a press in the header fails to, or the retries run out.
|
||||
*/
|
||||
async function pressUntilDecided(
|
||||
page,
|
||||
panelId,
|
||||
press,
|
||||
{ retries, log, note, pauseMs = RETRY_PAUSE_MS },
|
||||
) {
|
||||
for (let round = 0; ; round++) {
|
||||
await waitForStill(page, panelId);
|
||||
await recordNextPress(page, panelId);
|
||||
const { point, hit } = await headerTarget(page, panelId);
|
||||
const done = await press(point);
|
||||
const attempt = { point, hit, done, pressed: await readPress(page) };
|
||||
if (round >= retries || !pressMissed(attempt)) return attempt;
|
||||
const extra = note ? `; ${await note()}` : '';
|
||||
log(
|
||||
`note: the press missed the ${panelId} header (on ${attempt.pressed?.target ?? hit.target}${extra}); retrying`,
|
||||
);
|
||||
await delay(pauseMs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lift a docked panel into a floating window with a header drag.
|
||||
* @returns {Promise<{point: {x: number, y: number}, hit: {target: string, inHeader: boolean}, done: boolean, pressed: object|null}>}
|
||||
* `done` is whether the panel is floating; `pressed` names what the press
|
||||
* landed on and the panel's box at that moment.
|
||||
*/
|
||||
export function liftPanelByHeader(
|
||||
page,
|
||||
panelId,
|
||||
{ dx, dy, retries = PRESS_RETRIES, log = console.log, note, pauseMs } = {},
|
||||
) {
|
||||
return pressUntilDecided(
|
||||
page,
|
||||
panelId,
|
||||
async (point) => {
|
||||
await dragPointer(page, point, dx, dy);
|
||||
return waitForFloating(page, panelId, true);
|
||||
},
|
||||
{ retries, log, note, pauseMs },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dock a floating panel with a header double-click, waiting on the docked
|
||||
* state rather than a fixed sleep. Same result shape as `liftPanelByHeader`;
|
||||
* `done` is whether the panel is docked.
|
||||
*/
|
||||
export function dockPanelByDoubleClick(
|
||||
page,
|
||||
panelId,
|
||||
{ retries = PRESS_RETRIES, log = console.log, note, pauseMs } = {},
|
||||
) {
|
||||
return pressUntilDecided(
|
||||
page,
|
||||
panelId,
|
||||
async (point) => {
|
||||
await page.mouse.click(point.x, point.y, { clickCount: 1 });
|
||||
await page.mouse.click(point.x, point.y, { clickCount: 2 });
|
||||
return waitForFloating(page, panelId, false);
|
||||
},
|
||||
{ retries, log, note, pauseMs },
|
||||
);
|
||||
}
|
||||
|
||||
/** One line naming where an attempt pressed, for an assertion message. */
|
||||
export function describePress(attempt) {
|
||||
const { point, hit, pressed } = attempt;
|
||||
return `pressed ${Math.round(point.x)},${Math.round(point.y)} on ${pressed?.target ?? `${hit.target} (no press recorded)`}, in header: ${pressed?.inHeader ?? hit.inHeader}`;
|
||||
}
|
||||
+79
-15
@@ -13,13 +13,22 @@
|
||||
* is needed. The steps that open a photo need real imagery and are skipped.
|
||||
* Fixture runs add what a live run cannot stage: a key Mapillary rejects, and
|
||||
* a second page whose server has no key (the keyless gate most installs see).
|
||||
* Filter assertions are stricter because the fixture data is known.
|
||||
* Filter assertions are stricter because the fixture data is known. Every run
|
||||
* first asks the server's real status route from Node, before the browser
|
||||
* intercepts anything, so a server without the Mapillary routes fails.
|
||||
*/
|
||||
import assert from 'node:assert/strict';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { LAYER_STATE_REGISTRY } from '../src/data/layerState.js';
|
||||
import { tileBounds } from '../src/layers/streetLevel/tileMath.js';
|
||||
import { encodeCoverageTile } from '../src/layers/streetLevel/providers/mapillary/coverageFixture.mjs';
|
||||
import {
|
||||
describePress,
|
||||
dockPanelByDoubleClick,
|
||||
liftPanelByHeader,
|
||||
} from './qa-panelDrag.mjs';
|
||||
|
||||
const PANEL_ID = 'street-level-panel';
|
||||
|
||||
/** Viewports every layout assertion runs at. */
|
||||
export const VIEWPORTS = Object.freeze([
|
||||
@@ -105,6 +114,49 @@ export function fixtureTile(z, x, y, now = Date.now()) {
|
||||
return encodeCoverageTile(tile, { sequences });
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the server's real Mapillary status route, outside the browser's
|
||||
* interception, and require its JSON shape. Fixture runs answer the route in
|
||||
* the page, so without this a server that never registered it would pass.
|
||||
* Node sends no Origin or Sec-Fetch-Site, so the same-site gate admits it.
|
||||
* @param {string} url the application's base URL
|
||||
* @param {typeof fetch} [fetchImpl]
|
||||
* @returns {Promise<{configured: boolean}>}
|
||||
*/
|
||||
export async function assertRealStatusRoute(url, fetchImpl = fetch) {
|
||||
const route = new URL('/api/mapillary/status', url).href;
|
||||
let response;
|
||||
try {
|
||||
response = await fetchImpl(route, {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`${route} is unreachable: ${error?.message || error}`, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
const type = response.headers.get('content-type') || '';
|
||||
const text = await response.text();
|
||||
assert.equal(
|
||||
response.status,
|
||||
200,
|
||||
`${route} must be the server's Mapillary route (HTTP ${response.status}: ${text.slice(0, 120)})`,
|
||||
);
|
||||
assert.match(type, /application\/json/, `${route} answers JSON, not ${type}`);
|
||||
let body;
|
||||
try {
|
||||
body = JSON.parse(text);
|
||||
} catch {
|
||||
assert.fail(`${route} answered unparseable JSON: ${text.slice(0, 120)}`);
|
||||
}
|
||||
assert.equal(
|
||||
typeof body?.configured,
|
||||
'boolean',
|
||||
`${route} reports a boolean \`configured\`: ${text.slice(0, 120)}`,
|
||||
);
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Answer the page's Mapillary requests from fixtures; nothing reaches
|
||||
* mapillary.com. `fixture` is live: `configured` is what the status route
|
||||
@@ -277,6 +329,14 @@ async function main() {
|
||||
});
|
||||
};
|
||||
try {
|
||||
// Before any interception: fixture runs answer this route in the page.
|
||||
await step(
|
||||
'the server registers the real Mapillary status route',
|
||||
async () => {
|
||||
const real = await assertRealStatusRoute(url);
|
||||
console.log(` (server status: configured=${real.configured})`);
|
||||
},
|
||||
);
|
||||
const page = await browser.newPage();
|
||||
const fixture = { configured: true, tiles: 'ok', tileRequests: 0 };
|
||||
if (fixtures) await serveFixtures(page, fixture);
|
||||
@@ -865,19 +925,21 @@ async function main() {
|
||||
const r = node.getBoundingClientRect();
|
||||
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
||||
});
|
||||
// Lifting and docking re-resolve the header and retry only a press that
|
||||
// provably missed it: right after a dock the rail is still animating, and
|
||||
// a title point read once can be under another panel (scripts/
|
||||
// qa-panelDrag.mjs, shared with the panel-resize gate).
|
||||
const floatPanel = async () => {
|
||||
const title = await center('#street-level-panel .panel-title');
|
||||
await page.mouse.move(title.x, title.y);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(title.x - 200, title.y + 40, { steps: 8 });
|
||||
await page.mouse.move(title.x - 400, title.y + 80, { steps: 8 });
|
||||
await page.mouse.up();
|
||||
await sleep(400);
|
||||
const attempt = await liftPanelByHeader(page, PANEL_ID, {
|
||||
dx: -400,
|
||||
dy: 80,
|
||||
});
|
||||
assert.equal(
|
||||
await floating(),
|
||||
attempt.done,
|
||||
true,
|
||||
'a header drag lifts the panel out of the rail',
|
||||
`a header drag lifts the panel out of the rail (${describePress(attempt)})`,
|
||||
);
|
||||
assert.equal(await floating(), true);
|
||||
};
|
||||
await photoStep(
|
||||
'the panel floats on a header drag, resizes, and the viewer takes the room',
|
||||
@@ -946,11 +1008,13 @@ async function main() {
|
||||
);
|
||||
await step('a header double-click docks a floating window', async () => {
|
||||
await floatPanel();
|
||||
const title = await center('#street-level-panel .panel-title');
|
||||
await page.mouse.click(title.x, title.y, { clickCount: 1 });
|
||||
await page.mouse.click(title.x, title.y, { clickCount: 2 });
|
||||
await sleep(400);
|
||||
assert.equal(await floating(), false, 'docked again');
|
||||
// Waits on the docked state itself, not a fixed sleep.
|
||||
const attempt = await dockPanelByDoubleClick(page, PANEL_ID);
|
||||
assert.equal(
|
||||
attempt.done,
|
||||
true,
|
||||
`docked again (${describePress(attempt)})`,
|
||||
);
|
||||
});
|
||||
await step(
|
||||
'collapsing a floating window docks it as the rail strip',
|
||||
|
||||
Reference in new issue
Block a user