164 lines
5.1 KiB
JavaScript
164 lines
5.1 KiB
JavaScript
import { compileFunction } from 'node:vm';
|
|
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import fs from 'node:fs';
|
|
import { createRequire } from 'node:module';
|
|
import React from 'react';
|
|
import ts from 'typescript';
|
|
|
|
// Exercise the real component's event/state contract; this is not browser visual QA.
|
|
const require = createRequire(import.meta.url);
|
|
const source = fs.readFileSync(
|
|
new URL(
|
|
'../../resources/js/components/territory-search-select.tsx',
|
|
import.meta.url,
|
|
),
|
|
'utf8',
|
|
);
|
|
const compiled = ts.transpileModule(source, {
|
|
compilerOptions: {
|
|
module: ts.ModuleKind.CommonJS,
|
|
target: ts.ScriptTarget.ES2022,
|
|
jsx: ts.JsxEmit.ReactJSX,
|
|
},
|
|
}).outputText;
|
|
function harness(overrides = {}) {
|
|
const memory = [];
|
|
let cursor = 0;
|
|
const chosen = [];
|
|
const hooks = {
|
|
...React,
|
|
useState(initial) {
|
|
const i = cursor++;
|
|
if (!(i in memory)) memory[i] = initial;
|
|
return [
|
|
memory[i],
|
|
(value) => {
|
|
memory[i] =
|
|
typeof value === 'function' ? value(memory[i]) : value;
|
|
},
|
|
];
|
|
},
|
|
useRef: () => ({ current: null }),
|
|
useEffect: () => {},
|
|
};
|
|
const module = { exports: {} };
|
|
compileFunction(compiled, ['require', 'module', 'exports'])(
|
|
(name) => (name === 'react' ? hooks : require(name)),
|
|
module,
|
|
module.exports,
|
|
);
|
|
const options = [
|
|
{ id: 'province-a', name: 'Nampula' },
|
|
{ id: 'province-b', name: 'Maputo Província' },
|
|
];
|
|
const props = {
|
|
id: 'territory',
|
|
label: 'Província',
|
|
search: '',
|
|
options,
|
|
onSearch(query) {
|
|
props.search = query;
|
|
},
|
|
onChange(option) {
|
|
chosen.push(option);
|
|
props.selected = option;
|
|
},
|
|
...overrides,
|
|
};
|
|
let tree;
|
|
const flat = (node) =>
|
|
!node || typeof node !== 'object'
|
|
? []
|
|
: [
|
|
node,
|
|
...React.Children.toArray(node.props?.children).flatMap(flat),
|
|
];
|
|
const render = () => {
|
|
cursor = 0;
|
|
tree = module.exports.default(props);
|
|
return tree;
|
|
};
|
|
const find = (predicate) => flat(tree).find(predicate);
|
|
const input = () => find((node) => node.type === 'input');
|
|
const key = (value) =>
|
|
input().props.onKeyDown({ key: value, preventDefault() {} });
|
|
render();
|
|
return { props, chosen, render, find, input, key, options };
|
|
}
|
|
void test('one search input owns the dropdown; the separate select is absent', () => {
|
|
const h = harness();
|
|
assert.equal(
|
|
h.find((node) => node.type === 'select'),
|
|
undefined,
|
|
);
|
|
assert.equal(h.input().props.role, 'combobox');
|
|
assert.equal(h.input().props['aria-expanded'], false);
|
|
h.input().props.onFocus();
|
|
h.render();
|
|
assert.equal(h.input().props['aria-expanded'], true);
|
|
assert.ok(h.find((node) => node.props?.role === 'listbox'));
|
|
h.find(
|
|
(node) =>
|
|
node.props?.role === 'option' &&
|
|
node.props.id === 'territory-options-1',
|
|
).props.onClick();
|
|
h.render();
|
|
assert.deepEqual(h.chosen, [h.options[1]]);
|
|
assert.equal(h.input().props.value, 'Maputo Província');
|
|
assert.equal(h.input().props['aria-expanded'], false);
|
|
});
|
|
void test('typing and Escape preserve the existing selection until an option is explicitly chosen', () => {
|
|
const selected = {
|
|
id: 'saved',
|
|
name: 'Selecção guardada fora desta página',
|
|
};
|
|
const h = harness({ selected });
|
|
h.input().props.onFocus();
|
|
h.render();
|
|
h.input().props.onChange({ target: { value: 'Map' } });
|
|
h.render();
|
|
assert.equal(h.props.search, 'Map');
|
|
assert.deepEqual(h.chosen, []);
|
|
h.key('Escape');
|
|
h.render();
|
|
assert.equal(h.input().props.value, selected.name);
|
|
assert.deepEqual(h.chosen, []);
|
|
});
|
|
void test('arrow keys and Enter choose from the list; loading cannot commit an old result', () => {
|
|
const h = harness();
|
|
h.input().props.onFocus();
|
|
h.render();
|
|
h.key('ArrowDown');
|
|
h.render();
|
|
h.key('ArrowDown');
|
|
h.render();
|
|
assert.equal(
|
|
h.input().props['aria-activedescendant'],
|
|
'territory-options-1',
|
|
);
|
|
h.key('Enter');
|
|
h.render();
|
|
assert.deepEqual(h.chosen, [h.options[1]]);
|
|
const busy = harness({ loading: true });
|
|
busy.input().props.onFocus();
|
|
busy.render();
|
|
busy.key('Enter');
|
|
assert.deepEqual(busy.chosen, []);
|
|
});
|
|
void test('clear is explicit; disabled dependent levels cannot expose the dropdown', () => {
|
|
const h = harness({ selected: { id: 'saved', name: 'Nampula' } });
|
|
h.find(
|
|
(node) => node.props?.['aria-label'] === 'Limpar Província',
|
|
).props.onClick();
|
|
h.render();
|
|
assert.deepEqual(h.chosen, [undefined]);
|
|
assert.equal(h.input().props.value, '');
|
|
const disabled = harness({ disabled: true });
|
|
assert.equal(disabled.input().props.disabled, true);
|
|
assert.equal(
|
|
disabled.find((node) => node.props?.role === 'listbox'),
|
|
undefined,
|
|
);
|
|
});
|