Files
magistr/frontend/tests/browser/shared-controls.test.mjs
2026-10-02 04:07:56 +03:00

153 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict';
import { after, before, test } from 'node:test';
import { build } from 'esbuild';
import { chromium } from 'playwright';
let browser;
let bundle;
before(async () => {
const result = await build({
stdin: {
contents: `
import React from 'react';
import { createRoot } from 'react-dom/client';
import { AsyncCombobox } from './react/shared/ui/AsyncCombobox.jsx';
import { CustomSelect } from './react/shared/ui/CustomSelect.jsx';
window.requests = [];
window.selected = null;
window.submitted = false;
function RequiredSelectForm() {
const [value, setValue] = React.useState('');
return <form onSubmit={event => { event.preventDefault(); window.submitted = true; }}>
<label htmlFor="required-select">Обязательный выбор</label>
<CustomSelect id="required-select" name="choice" required value={value} onChange={event => setValue(event.target.value)}>
<option value="">Выберите вариант</option>
<option value="valid">Правильный вариант</option>
</CustomSelect>
<button type="submit">Отправить</button>
</form>;
}
createRoot(document.getElementById('root')).render(
<><AsyncCombobox
selectedLabel="Выбранная группа"
selectedValue="1"
load={query => new Promise((resolve, reject) => {
window.requests.push({ query, resolve, reject });
})}
onSelect={item => { window.selected = item; }}
/><RequiredSelectForm /></>
);
`,
resolveDir: process.cwd(),
loader: 'jsx'
},
bundle: true,
write: false,
format: 'iife',
jsx: 'automatic',
define: { 'process.env.NODE_ENV': '"test"' }
});
bundle = result.outputFiles[0].text;
browser = await chromium.launch({ executablePath: process.env.CHROMIUM_PATH || '/usr/bin/chromium' });
});
after(async () => { await browser?.close(); });
async function fixture(t) {
const page = await browser.newPage();
const errors = [];
page.on('pageerror', error => errors.push(error.message));
t.after(async () => {
await page.close();
assert.deepEqual(errors, [], 'Компонент вызвал ошибку JavaScript');
});
await page.setContent('<style>.async-combobox-menu { position: absolute; }</style><div id="root"></div><button id="outside">Другой элемент</button>');
await page.addScriptTag({ content: bundle });
const input = page.getByRole('combobox', { name: 'Поисковый список', exact: true });
await input.focus();
await page.waitForFunction(() => window.requests.length === 1);
return { page, input };
}
test('устаревший ответ или ошибка не заменяют новый поиск во время задержки', async t => {
for (const outcome of ['resolve', 'reject']) {
const { page, input } = await fixture(t);
await input.fill('Новая группа');
await page.evaluate(outcome => {
const request = window.requests[0];
if (outcome === 'resolve') request.resolve([{ id: 2, name: 'Устаревшая группа' }]);
else request.reject(new Error('Ошибка старого запроса'));
}, outcome);
assert.equal(await page.getByRole('option').count(), 0, 'Устаревший вариант доступен для выбора');
await page.getByText('Поиск...', { exact: true }).waitFor();
await page.waitForFunction(() => window.requests.length === 2);
await page.evaluate(() => window.requests[1].resolve([{ id: 3, name: 'Новая группа' }]));
await page.getByRole('option', { name: 'Новая группа' }).waitFor();
assert.equal(await page.getByRole('option').count(), 1);
}
});
test('Escape из списка возвращает фокус и оставляет меню закрытым', async t => {
const { page, input } = await fixture(t);
await page.evaluate(() => window.requests[0].resolve([{ id: 2, name: 'Группа' }]));
await page.getByRole('option').waitFor();
await input.press('ArrowDown');
await page.keyboard.press('Escape');
assert.equal(await input.getAttribute('aria-expanded'), 'false');
assert.equal(await input.evaluate(element => element === document.activeElement), true);
assert.equal(await page.evaluate(() => window.requests.length), 1, 'Возврат фокуса запустил лишний поиск');
});
test('возврат стрелкой вверх сохраняет поисковый текст и результаты', async t => {
const { page, input } = await fixture(t);
await input.fill('Запрос');
await page.waitForFunction(() => window.requests.length === 2);
await page.evaluate(() => window.requests[1].resolve([{ id: 2, name: 'Результат' }]));
await page.getByRole('option').waitFor();
await input.press('ArrowDown');
await page.keyboard.press('ArrowUp');
assert.equal(await input.inputValue(), 'Запрос');
assert.equal(await page.evaluate(() => window.requests.length), 2);
});
test('ввод после Escape открывает поиск, а уход фокуса отменяет отложенный запрос', async t => {
const { page, input } = await fixture(t);
await input.press('Escape');
await input.fill('Новый запрос');
assert.equal(await input.getAttribute('aria-expanded'), 'true');
await page.locator('#outside').focus();
await page.waitForTimeout(400);
assert.equal(await input.getAttribute('aria-expanded'), 'false');
assert.equal(await page.evaluate(() => window.requests.length), 1);
});
test('выбор варианта сохраняет значение и возвращает фокус без нового поиска', async t => {
const { page, input } = await fixture(t);
await page.evaluate(() => window.requests[0].resolve([{ id: 2, name: 'Группа' }]));
await page.getByRole('option').click();
assert.deepEqual(await page.evaluate(() => window.selected), { id: 2, name: 'Группа' });
assert.equal(await input.getAttribute('aria-expanded'), 'false');
assert.equal(await input.evaluate(element => element === document.activeElement), true);
assert.equal(await page.evaluate(() => window.requests.length), 1);
});
test('обязательный селект показывает ошибку, получает фокус и разрешает отправку после выбора', async t => {
const { page } = await fixture(t);
const errors = [];
page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); });
const select = page.getByRole('combobox', { name: 'Обязательный выбор', exact: true });
await page.getByRole('button', { name: 'Отправить', exact: true }).click();
assert.equal(await page.evaluate(() => window.submitted), false);
await page.waitForFunction(() => document.activeElement?.getAttribute('aria-label') === 'Обязательный выбор');
assert.equal(await select.evaluate(element => element === document.activeElement), true);
assert.equal(await select.getAttribute('aria-invalid'), 'true');
await page.getByRole('alert').filter({ hasText: 'Выберите значение' }).waitFor();
await select.click();
await page.getByRole('option', { name: 'Правильный вариант', exact: true }).click();
assert.equal(await select.getAttribute('aria-invalid'), null);
await page.getByRole('button', { name: 'Отправить', exact: true }).click();
assert.equal(await page.evaluate(() => window.submitted), true);
assert.deepEqual(errors, [], 'Браузер не смог сфокусировать обязательный селект');
});