Files
gods-eye-view/src/ui/locationSearchRecovery.test.mjs
T
Sameh Khamis 5f8f9bb921 feat(ui): render the location tray's pills, mini status and search error from a store
LocationControls (./ui/location) no longer builds the city and landmark
pills or writes the mini status and the search error: it presents the tray
and hands it to its publish option, so the entry stays Vue-free. The shell
publishes it to a new gev-location store and connects the controls, and
the dock renders it and calls chooseCity() and choosePoi(). The landmark
row still expands a frame after its pills render. The search field, its
toggle and the reset buttons keep their listeners. The location QA harness
reads the rendered elements.
2026-10-31 12:19:44 -07:00

106 lines
3.3 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { NavigationController } from './navigationController.js';
import { LocationNavigation } from './locationNavigation.js';
function input() {
const classes = new Set(['expanded', 'searching']);
const attributes = new Map();
return {
id: 'location-search',
value: 'Pariss',
focused: false,
selected: false,
classList: {
remove: (...names) => names.forEach((name) => classes.delete(name)),
contains: (name) => classes.has(name),
},
focus() {
this.focused = true;
},
blur() {
this.focused = false;
},
select() {
this.selected = true;
},
setAttribute: (name, value) => attributes.set(name, value),
removeAttribute: (name) => attributes.delete(name),
getAttribute: (name) => attributes.get(name) ?? null,
};
}
function settle(searchInput, options) {
const owner = { _activeLocationSearchGeneration: 4, searchInput };
NavigationController.prototype._settleLocationSearchUi.call(
owner,
4,
options,
);
return owner;
}
test('a search that found nothing keeps its query, the open field and focus', () => {
const field = input();
const owner = settle(field, { keepQuery: true });
assert.equal(owner._activeLocationSearchGeneration, null);
assert.equal(field.value, 'Pariss');
assert.equal(field.classList.contains('expanded'), true);
assert.equal(field.classList.contains('searching'), false);
assert.equal(field.focused, true);
assert.equal(field.selected, true);
});
test('a search that arrived still clears and closes the field', () => {
const field = input();
field.focused = true;
settle(field);
assert.equal(field.value, '');
assert.equal(field.classList.contains('expanded'), false);
assert.equal(field.focused, false);
});
function navigation() {
const field = input();
// The tray renders the error and the field's aria-invalid from the
// controls' published view (src/ui/commandDock.test.mjs).
const errors = [];
const settled = [];
const toasts = [];
const owner = {
_disposed: false,
_locationSearch: field,
_locationControls: { setSearchError: (message) => errors.push(message) },
_activeLocationSearchGeneration: null,
_settleLocationSearchUi: (generation, options) =>
settled.push([generation, options]),
_showToast: (message) => toasts.push(message),
_setLocationSearchError:
LocationNavigation.prototype._setLocationSearchError,
};
const handle = (status, type) =>
LocationNavigation.prototype._handleLocationSearchState.call(
owner,
{ status, query: field.value },
{ type, generation: 7 },
);
return { owner, field, errors, settled, toasts, handle };
}
for (const [status, text] of [
['missing', 'Location not found'],
['failed', 'Search failed'],
]) {
test(`a ${status} search announces a lasting error on the field instead of a toast`, () => {
const n = navigation();
n.handle('searching', 'started');
n.handle(status, status);
n.handle(status, 'settled');
assert.match(n.errors.at(-1), new RegExp(`^${text}\\. `));
assert.deepEqual(n.settled, [[7, { keepQuery: true }]]);
assert.deepEqual(n.toasts, []);
n.handle('searching', 'started');
assert.equal(n.errors.at(-1), null);
});
}