mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
test(a11y): cover delayed and interrupted Map Source focus
Keep the Esri settlement assertion exact, verify selected-source focus and Tab departure in Chromium, cancel pending work during disposal, and document the focused behavior.
This commit is contained in:
1 parent
60812df34c
commit
231d339e92
6 files changed
+89
-14
No files matched your search
@@ -15,6 +15,10 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
|
||||
|
||||
### Fixed
|
||||
|
||||
- Map Source keyboard opening retries focus until the selected tile is visible.
|
||||
Leaving the disclosure, pointer interaction, or closing the tray cancels the
|
||||
pending handoff so delayed work cannot pull focus back.
|
||||
|
||||
- Scope, Bloom, Sharpen, location search and generated style sliders expose
|
||||
explicit accessible names. The first-run checkbox retains its native label.
|
||||
- FIRMS records a source as successful only after appending its rows, avoiding
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# God's Eye View Current State
|
||||
|
||||
## Map Source keyboard focus
|
||||
|
||||
Keyboard opening focuses the selected map-source tile, falling back to the first
|
||||
only when no source is selected. A bounded retry handles delayed tray visibility.
|
||||
Moving focus away, pointer interaction, closing the tray, or disposing the UI
|
||||
cancels pending work; a later reopening cannot inherit an old focus request.
|
||||
|
||||
## September 8, 2026
|
||||
|
||||
Earthquake refreshes validate the complete feed and construct replacement entities before clearing the previous snapshot. Malformed rows and duplicate rendered IDs retain the last good entities, overlays, count and timestamp and report a malformed response; unknown magnitude is excluded from M2.5+ rendering.
|
||||
|
||||
@@ -172,9 +172,8 @@ try {
|
||||
// flake as the tray timers above (#54). Poll the observable truth instead.
|
||||
const domDeadline = performance.now() + 3000;
|
||||
const settled = () => !document.body.innerText.includes('Powered by Esri')
|
||||
&& [...document.querySelectorAll('.map-stack-chip')]
|
||||
.filter((chip) => chip.getAttribute('aria-pressed') === 'true')
|
||||
.every((chip) => chip.dataset.stackId === 'osm');
|
||||
&& JSON.stringify([...document.querySelectorAll('.map-stack-chip[aria-pressed="true"]')]
|
||||
.map((chip) => chip.dataset.stackId)) === JSON.stringify(['osm']);
|
||||
while (!settled() && performance.now() < domDeadline) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
}
|
||||
@@ -230,16 +229,17 @@ try {
|
||||
wantExpanded, wantFocus,
|
||||
).catch(() => {});
|
||||
|
||||
const keyboardSource = await page.evaluate(() => window.__godsEyeView.styleManager.mapStackController.getActiveId());
|
||||
await page.focus('#control-panel-toggle');
|
||||
await page.keyboard.press('Enter');
|
||||
await waitTray('true', 'photoreal');
|
||||
await waitTray('true', keyboardSource);
|
||||
const keyboardOpen = await page.evaluate(() => ({
|
||||
expanded: document.getElementById('control-panel-toggle').getAttribute('aria-expanded'),
|
||||
activeStack: document.activeElement?.dataset?.stackId || null,
|
||||
}));
|
||||
check(
|
||||
'Enter opens the tray and hands focus to a Map Source tile',
|
||||
keyboardOpen.expanded === 'true' && keyboardOpen.activeStack === 'photoreal',
|
||||
'Enter opens the tray and hands focus to the selected Map Source tile',
|
||||
keyboardOpen.expanded === 'true' && keyboardOpen.activeStack === keyboardSource,
|
||||
JSON.stringify(keyboardOpen),
|
||||
);
|
||||
|
||||
@@ -256,14 +256,14 @@ try {
|
||||
);
|
||||
|
||||
await page.keyboard.press('Space');
|
||||
await waitTray('true', 'photoreal');
|
||||
await waitTray('true', keyboardSource);
|
||||
const spaceOpen = await page.evaluate(() => ({
|
||||
expanded: document.getElementById('control-panel-toggle').getAttribute('aria-expanded'),
|
||||
activeStack: document.activeElement?.dataset?.stackId || null,
|
||||
}));
|
||||
check(
|
||||
'Space opens the tray through the same keyboard path',
|
||||
spaceOpen.expanded === 'true' && spaceOpen.activeStack === 'photoreal',
|
||||
spaceOpen.expanded === 'true' && spaceOpen.activeStack === keyboardSource,
|
||||
JSON.stringify(spaceOpen),
|
||||
);
|
||||
|
||||
@@ -271,21 +271,68 @@ try {
|
||||
await page.keyboard.down('Enter');
|
||||
await new Promise((resolve) => setTimeout(resolve, 320));
|
||||
await page.keyboard.up('Enter');
|
||||
await waitTray('true', 'photoreal'); // let the hold's scheduled focus hand-off land
|
||||
await waitTray('true', keyboardSource); // let the hold's scheduled focus hand-off land
|
||||
await page.keyboard.press('Escape');
|
||||
await waitTray('false', 'toggle');
|
||||
await page.keyboard.press('Enter');
|
||||
await waitTray('true', 'photoreal');
|
||||
await waitTray('true', keyboardSource);
|
||||
const longHoldRecovery = await page.evaluate(() => ({
|
||||
expanded: document.getElementById('control-panel-toggle').getAttribute('aria-expanded'),
|
||||
activeStack: document.activeElement?.dataset?.stackId || null,
|
||||
}));
|
||||
check(
|
||||
'long Enter hold cannot strand the disclosure keyboard path',
|
||||
longHoldRecovery.expanded === 'true' && longHoldRecovery.activeStack === 'photoreal',
|
||||
longHoldRecovery.expanded === 'true' && longHoldRecovery.activeStack === keyboardSource,
|
||||
JSON.stringify(longHoldRecovery),
|
||||
);
|
||||
|
||||
// Force a delayed visible state while using the real controller and keyboard routes.
|
||||
const hideTray = () => page.evaluate(() => {
|
||||
const manager = window.__godsEyeView.styleManager;
|
||||
manager.setPanelCollapsed('control-panel', true);
|
||||
window.__qaTrayStyles = [...document.querySelectorAll('.map-stack-chip')]
|
||||
.map((chip) => [chip, chip.style.cssText]);
|
||||
for (const [chip] of window.__qaTrayStyles) chip.style.setProperty('visibility', 'hidden', 'important');
|
||||
document.getElementById('control-panel-toggle').focus();
|
||||
});
|
||||
const showTray = () => page.evaluate(() => {
|
||||
for (const [chip, cssText] of window.__qaTrayStyles) chip.style.cssText = cssText;
|
||||
delete window.__qaTrayStyles;
|
||||
});
|
||||
await hideTray();
|
||||
await page.keyboard.press('Enter');
|
||||
await new Promise((resolve) => setTimeout(resolve, 350));
|
||||
const delayedBefore = await page.evaluate(() => document.activeElement?.id);
|
||||
await showTray();
|
||||
await waitTray('true', keyboardSource);
|
||||
const delayedAfter = await page.evaluate(() => document.activeElement?.dataset?.stackId);
|
||||
check('a delayed visible tray receives selected-source focus after the first attempt',
|
||||
delayedBefore === 'control-panel-toggle' && delayedAfter === keyboardSource,
|
||||
JSON.stringify({ delayedBefore, delayedAfter, keyboardSource }));
|
||||
|
||||
await hideTray();
|
||||
await page.keyboard.press('Enter');
|
||||
await new Promise((resolve) => setTimeout(resolve, 350));
|
||||
await page.keyboard.press('Tab');
|
||||
const departure = await page.evaluate(() => {
|
||||
window.__qaDepartedFocus = document.activeElement;
|
||||
return document.activeElement !== document.getElementById('control-panel-toggle');
|
||||
});
|
||||
await showTray();
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
const focusRetained = await page.evaluate(() => {
|
||||
const same = document.activeElement === window.__qaDepartedFocus;
|
||||
delete window.__qaDepartedFocus;
|
||||
return same;
|
||||
});
|
||||
check('Tab away during the opening transition revokes delayed focus', departure && focusRetained,
|
||||
JSON.stringify({ departure, focusRetained }));
|
||||
|
||||
await page.evaluate(() => window.__godsEyeView.styleManager.setPanelCollapsed('control-panel', true));
|
||||
await page.focus('#control-panel-toggle');
|
||||
await page.keyboard.press('Enter');
|
||||
await waitTray('true', keyboardSource);
|
||||
|
||||
if (forceKeyless) {
|
||||
await page.evaluate(async () => {
|
||||
const styleManager = window.__godsEyeView.styleManager;
|
||||
@@ -617,8 +664,17 @@ try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
await page.focus('#control-panel-toggle');
|
||||
await page.keyboard.press('Enter'); // opens and hands focus to the active tile
|
||||
await new Promise((resolve) => setTimeout(resolve, 400));
|
||||
await page.keyboard.press('Tab'); // tab ONTO a tile, keyboard modality
|
||||
const selectedForHold = await page.evaluate(() => window.__godsEyeView.styleManager.mapStackController.getActiveId());
|
||||
await waitTray('true', selectedForHold);
|
||||
// Keyless starts on OSM, the last tile. Tab forward there correctly leaves
|
||||
// the tray, so navigate to a neighbouring tile in the available direction.
|
||||
const activeIsLastTile = await page.evaluate(() => {
|
||||
const chips = [...document.querySelectorAll('#map-stack-chips .map-stack-chip')];
|
||||
return document.activeElement === chips.at(-1);
|
||||
});
|
||||
if (activeIsLastTile) await page.keyboard.down('Shift');
|
||||
await page.keyboard.press('Tab');
|
||||
if (activeIsLastTile) await page.keyboard.up('Shift');
|
||||
await page.keyboard.press('Enter'); // activate it from the keyboard
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
const keyboardAfterActivate = await page.evaluate(() => ({
|
||||
|
||||
@@ -230,3 +230,10 @@ test('pointer leave preserves established keyboard focus and the open tray', ()
|
||||
});
|
||||
|
||||
|
||||
|
||||
test('disposed UI cannot complete a pending handoff', () => {
|
||||
const h = harness({ hidden: true }); h.key(); h.tick();
|
||||
h.manager._disposed = true; h.show(); h.drain();
|
||||
assert.equal(h.document.activeElement, h.disclosure);
|
||||
assert.equal(h.calls(), 1); assert.equal(h.timers.size, 0);
|
||||
});
|
||||
@@ -344,7 +344,7 @@ test('the Visual Presets tray owns Map Source and the retired left panel is abse
|
||||
'the compact wing must expose a semantic keyboard disclosure',
|
||||
);
|
||||
assert.match(ui, /event\.key !== 'Escape'[\s\S]*?disclosure\?\.focus/);
|
||||
assert.match(ui, /map-stack-chip\.active, \.map-stack-chip/);
|
||||
assert.match(ui, /querySelector\('\.map-stack-chip\.active'\)\s*\|\| panelEl\.querySelector\('\.map-stack-chip'\)/);
|
||||
|
||||
assert.match(
|
||||
ui,
|
||||
|
||||
@@ -10167,6 +10167,7 @@ export class StyleManager {
|
||||
this._globalStatusNotice = null;
|
||||
if (this._globalLoadingStatus) this._globalLoadingStatus.hidden = true;
|
||||
this._disposed = true;
|
||||
this._cancelMapSourceFocus?.();
|
||||
// Revoke persistence/hash authority before teardown can emit manager changes.
|
||||
this._layerStateCoordinator?.destroy();
|
||||
this._layerStateCoordinator = null;
|
||||
|
||||
Reference in new issue
Block a user