From 71e3691cf170d35830b5983fb003355985d07333 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sun, 10 May 2026 15:40:27 +0300 Subject: [PATCH] =?UTF-8?q?test(web-test):=20M3=20P1=20batch=201=20?= =?UTF-8?q?=E2=80=94=20confirm-save-no/pending,=20more-menu,=20clear/ref-f?= =?UTF-8?q?orm,=20table=20checkbox/clear?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- tests/web-test/02-crud.test.mjs | 50 +++++++++++++++++++++++++- tests/web-test/03-fillfields.test.mjs | 41 +++++++++++++++++++++ tests/web-test/04-selectvalue.test.mjs | 19 ++++++++++ tests/web-test/05-table.test.mjs | 29 +++++++++++++++ 4 files changed, 138 insertions(+), 1 deletion(-) diff --git a/tests/web-test/02-crud.test.mjs b/tests/web-test/02-crud.test.mjs index b208c45f..d4fa9f9b 100644 --- a/tests/web-test/02-crud.test.mjs +++ b/tests/web-test/02-crud.test.mjs @@ -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(); + }); } diff --git a/tests/web-test/03-fillfields.test.mjs b/tests/web-test/03-fillfields.test.mjs index d23fdcb9..06b7e7f5 100644 --- a/tests/web-test/03-fillfields.test.mjs +++ b/tests/web-test/03-fillfields.test.mjs @@ -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). Но варианты тумблера видны в diff --git a/tests/web-test/04-selectvalue.test.mjs b/tests/web-test/04-selectvalue.test.mjs index 9cdc282c..4dd7edce 100644 --- a/tests/web-test/04-selectvalue.test.mjs +++ b/tests/web-test/04-selectvalue.test.mjs @@ -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). Откладывается до расширения синтетики. diff --git a/tests/web-test/05-table.test.mjs b/tests/web-test/05-table.test.mjs index 9aa18258..3285c5e7 100644 --- a/tests/web-test/05-table.test.mjs +++ b/tests/web-test/05-table.test.mjs @@ -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: 'Товары' });