fix(web-test): заголовок формы в состоянии + formHasField по массиву fields

Два задокументированных ассерта бросали ВСЕГДА, то есть были мертвы:

- formTitle читал state.title, которого не заполнял никто: getFormStateScript
  собирал форму без заголовка. Единственным носителем оставалась панель
  открытых окон (activeTab), а она отключается в настройках 1С — на такой базе
  заголовок был недоступен ничем.
- formHasField читал state.fields[name], хотя fields — массив объектов
  {name, value, …}. На массиве это всегда undefined.

getFormState теперь отдаёт title. Берётся он из шапки самой формы: заголовок
лежит в атрибуте (title у .toplineBoxTitle, data-title у родителя), сам элемент
пустой — поэтому поиском по тексту он и не находился.

Выбор шапки — не «первая видимая»: при открытом всплывающем окне видимы ДВЕ,
родителя и окна, и наивное правило отдавало заголовок родителя — правдоподобный
неверный ответ, при котором тест «окно выбора открылось» зеленел бы по
документу. Приоритет взят тот же, что уже отлажен для крестика закрытия в
closeCrossScript: плавающее окно ps<N> с наибольшим индексом → собственная шапка
формы → и только потом панель открытых окон. Привязка к id, а не к тексту — не
ломается на другой локали. Панель осталась последним звеном: она отключаема, а
при всплывающем окне ещё и показывает родителя.

Диагностика раннера (resetState) тоже переведена на title с прежним activeTab
как запасным.

formHasField ищет по массиву и перечисляет доступные имена в ошибке (раньше
Object.keys по массиву давал индексы). formTitle отличает «заголовок недоступен»
(title === null) от несовпадения.

Почему не поймали раньше: из 12 ассертов сюита вызывала 8, и оба сломанных были
среди четырёх невызываемых. Теперь все четыре задействованы на настоящем выводе
getFormState — formTitle/formHasField/noErrors в 12-formstate (включая случай
всплывающего окна), tableRowCount в 09-filter. Отдельного юнит-теста намеренно
нет: состояние для него пришлось бы писать руками, а именно неверное
представление о форме состояния и породило оба дефекта.

Доки приведены к массиву: примеры вида s.fields['X']?.value в regress.md и в
спеке заменены на fields.find(f => f.name === 'X').

Проверено: заголовок на списке, форме элемента и всплывающем окне; каскад
разведён по значениям (шапка выигрывает у панели, при пустой шапке — откат);
позитив и негатив всех четырёх ассертов на реальном состоянии формы; полный
регресс 29/29 до и после, file/name/status идентичны.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-20 18:51:12 +03:00
co-authored by Claude Opus 4.8
parent 9b65dccd8a
commit 8826a88427
8 changed files with 66 additions and 19 deletions
+7 -5
View File
@@ -306,11 +306,13 @@ await assert.throws(asyncFn, msg?) // ожидает исключение
```js
assert.formHasField(state, fieldName, msg?)
// проверяет наличие state.fields[fieldName]; в сообщении об ошибке
// перечисляются доступные поля для быстрой диагностики
// проверяет, что в массиве state.fields есть поле с таким name;
// в сообщении об ошибке перечисляются доступные поля для быстрой диагностики
assert.formTitle(state, expected, msg?)
// проверяет, что state.title содержит expected
// проверяет, что state.title СОДЕРЖИТ expected (подстрока, не строгое равенство).
// state.title — заголовок активной формы: сначала из шапки формы, при её отсутствии —
// из панели открытых окон; null, если недоступны оба (тогда ассерт падает с этим фактом)
assert.tableHasRow(table, predicate, msg?)
// predicate: объект (частичное совпадение по ===) или функция row => bool
@@ -609,7 +611,7 @@ await step('Менеджер утверждает', async () => {
await step('Кладовщик проверяет статус', async () => {
// страница кладовщика ТА ЖЕ — форма открыта, навигация не нужна
const state = await clerk.getFormState();
assert.equal(state.fields['Статус']?.value, 'Утверждён');
assert.equal(state.fields.find(f => f.name === 'Статус')?.value, 'Утверждён');
});
```
@@ -918,7 +920,7 @@ export const params = [
export default async function({ fillFields, getFormState, assert }, { type, field, value }) {
await fillFields({ [field]: value });
const state = await getFormState();
assert.equal(state.fields[field]?.value, String(value));
assert.equal(state.fields.find(f => f.name === field)?.value, String(value));
}
```