test(web-test): M3 P1 batch 1 — confirm-save-no/pending, more-menu, clear/ref-form, table checkbox/clear

02-crud: confirm-save-no (rollback при save:false), confirm-pending
(closeForm() без решения возвращает confirmation), more-menu (clickElement
'Ещё' возвращает submenu).

03-fillfields: clear (Shift+F4 через пустое значение), reference-non-quickchoice
(fillFields на quickChoice=false поле — method=dropdown через DLB; чистый
form-path требует hasPick && !hasSelect, такого поля в синтетике нет).

04-selectvalue: clear (selectValue '' → Shift+F4). show-all-form отложен —
требует quickChoice=true каталога с количеством > порога dropdown
(в синтетике нет).

05-table: checkbox (fillTableRow с Boolean), clear (Shift+F4 на ref-ячейке +
восстановление для последующего delete).

Live на webtest: все шаги проходят.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-05-10 15:40:27 +03:00
parent 1af318325d
commit 71e3691cf1
4 changed files with 138 additions and 1 deletions
+49 -1
View File
@@ -2,7 +2,7 @@ export const name = 'CRUD: открытие, чтение, закрытие с
export const tags = ['crud', 'smoke'];
export const timeout = 60000;
export default async function({ navigateSection, openCommand, clickElement, closeForm, readTable, fillField, getFormState, assert, step, log }) {
export default async function({ navigateSection, openCommand, clickElement, closeForm, readTable, fillField, getFormState, getPage, assert, step, log }) {
await step('read: список Контрагентов отдаёт колонки/строки/total', async () => {
await navigateSection('Склад');
@@ -57,4 +57,52 @@ export default async function({ navigateSection, openCommand, clickElement, clos
assert.equal(phoneField?.value, newPhone, 'Телефон должен сохраниться');
await closeForm();
});
await step('confirm-save-no: closeForm({save:false}) → изменения откатываются', async () => {
await navigateSection('Склад');
await openCommand('Контрагенты');
await clickElement('ООО Восток', { dblclick: true });
const before = await getFormState();
const origPhone = before.fields?.find(f => f.name === 'Телефон')?.value;
log(`origPhone='${origPhone}'`);
await fillField('Телефон', '+7 (000) 000-00-00');
const closed = await closeForm({ save: false });
assert.ok(closed.closed, 'Форма должна закрыться через "Нет"');
await navigateSection('Склад');
await openCommand('Контрагенты');
await clickElement('ООО Восток', { dblclick: true });
const state = await getFormState();
const phone = state.fields?.find(f => f.name === 'Телефон')?.value;
log(`Re-opened phone after save:false='${phone}'`);
assert.equal(phone, origPhone, 'Телефон не должен измениться (save:false откатил)');
await closeForm();
});
await step('confirm-pending: closeForm() без решения → confirmation в state', async () => {
await navigateSection('Склад');
await openCommand('Контрагенты');
await clickElement('ООО Север', { dblclick: true });
await fillField('Телефон', '+7 (123) 456-78-90');
const pending = await closeForm();
log(`pending: closed=${pending.closed} confirmation=${JSON.stringify(pending.confirmation)}`);
assert.ok(!pending.closed, 'Форма НЕ должна закрыться без решения');
assert.ok(pending.confirmation, 'state.confirmation должен присутствовать');
// Закрыть через явный отказ от сохранения
await closeForm({ save: false });
});
await step('more-menu: clickElement("Ещё") возвращает submenu[]', async () => {
await navigateSection('Склад');
await openCommand('Контрагенты');
const r = await clickElement('Ещё');
const items = r.submenu || [];
log(`submenu items: ${items.length} sample=${items.slice(0, 5).join(', ')}`);
assert.ok(Array.isArray(r.submenu), 'clickElement("Ещё") должен вернуть submenu[]');
assert.ok(items.length >= 1, 'submenu не должен быть пустым');
// Закрыть submenu
const page = await getPage();
await page.keyboard.press('Escape');
await closeForm();
});
}
+41
View File
@@ -52,6 +52,47 @@ export default async function({ navigateSection, openCommand, clickElement, fill
await closeForm({ save: false });
});
await step('clear: fillFields пустым значением очищает текстовое поле', async () => {
await navigateSection('Склад');
await openCommand('Контрагенты');
await clickElement('ООО Север', { dblclick: true });
const before = await getFormState();
const phoneBefore = findField(before, 'Телефон')?.value;
log(`phone before clear='${phoneBefore}'`);
const r = await fillFields({ 'Телефон': '' });
log('clear method: ' + r.filled[0]?.method);
assert.ok(r.filled[0]?.ok, 'clear должен вернуть ok=true');
assert.equal(r.filled[0]?.method, 'clear', 'method должен быть clear (Shift+F4)');
const state = await getFormState();
assert.equal(findField(state, 'Телефон')?.value, '', 'Телефон должен быть пустым');
await closeForm({ save: false });
});
await step('reference-non-quickchoice: fillFields на Контрагент (quickChoice=false)', async () => {
// Поле имеет DLB+CB → fillFields идёт через fillReferenceField (method=dropdown/typeahead).
// Чистый method='form' путь требует поля без DLB (hasPick && !hasSelect) — в синтетике
// такого поля нет, поэтому проверяем сам факт корректного заполнения через DLB.
await navigateSection('Склад');
await openCommand('Приходная накладная');
await clickElement('Создать');
const r = await fillFields({ 'Контрагент': 'ООО Север' });
log('reference method: ' + r.filled[0]?.method);
assert.ok(r.filled[0]?.ok, 'fillFields на Контрагент должен сработать');
assert.ok(['dropdown', 'typeahead', 'form'].includes(r.filled[0]?.method),
`method=${r.filled[0]?.method} должен быть один из dropdown|typeahead|form`);
const state = await getFormState();
const v = findField(state, 'Контрагент')?.value || '';
log(`Контрагент value='${v}'`);
assert.includes(v, 'Север', 'Контрагент должен содержать "Север"');
await closeForm({ save: false });
});
await step('radio: КатегорияЦены (RadioButtons) через fillFields, СпособУчёта (Tumbler) через clickElement', async () => {
// Tumbler-представление не парсится fillFields как radio-поле (см.
// upload/web-test-bugs.md пункт 5). Но варианты тумблера видны в
+19
View File
@@ -37,4 +37,23 @@ export default async function({ navigateSection, openCommand, clickElement, sele
await closeForm({ save: false });
});
await step('clear: selectValue с пустым search → Shift+F4', async () => {
await navigateSection('Склад');
await openCommand('Приходная накладная');
await clickElement('Создать');
await selectValue('Организация', 'Альфа');
const before = await selectValue('Организация', ''); // empty → clear
const field = findField(before, 'Организация');
log(`Организация after clear value='${field?.value}'`);
assert.equal(field?.value, '', 'Организация должна быть очищена');
await closeForm({ save: false });
});
}
// show-all-form ветка (P1 в матрице) требует quickChoice=true каталога с
// количеством > порога dropdown, чтобы появилась ссылка "Показать все".
// В текущей синтетике такого каталога нет (Организации ~2 элемента, остальные
// quickChoice=false). Откладывается до расширения синтетики.
+29
View File
@@ -48,6 +48,35 @@ export default async function({ navigateSection, openCommand, clickElement, fill
assert.equal(t.rows[1]['Цена'], '150,00', 'Цена строки 1 = 150');
});
await step('checkbox: переключить Согласовано в строке 1 через fillTableRow', async () => {
const r = await fillTableRow(
{ 'Согласовано': true },
{ table: 'Товары', row: 1 }
);
log(`checkbox result: ${JSON.stringify(r.filled || r)}`);
const t = await readTable({ table: 'Товары' });
log(`row 1 Согласовано='${t.rows[1]['Согласовано']}'`);
assert.equal(t.rows[1]['Согласовано'], 'true', 'Согласовано должно стать true');
});
await step('clear: очистить ссылочную ячейку Номенклатура через fillTableRow с пустым значением', async () => {
// Используем строку 0 (Товар 01)
const r = await fillTableRow(
{ 'Номенклатура': '' },
{ table: 'Товары', row: 0 }
);
log(`clear result: ${JSON.stringify(r.filled || r)}`);
const t = await readTable({ table: 'Товары' });
log(`row 0 Номенклатура after clear='${t.rows[0]['Номенклатура']}'`);
assert.equal(t.rows[0]['Номенклатура'], '', 'Номенклатура должна быть очищена (Shift+F4)');
// Восстанавливаем Товар 01 чтобы последующий delete мог работать с предсказуемым состоянием
await fillTableRow(
{ 'Номенклатура': 'Товар 01' },
{ table: 'Товары', row: 0 }
);
});
await step('delete: удалить первую строку', async () => {
await deleteTableRow(0, { table: 'Товары' });
const t = await readTable({ table: 'Товары' });