diff --git a/tests/skills/integration/build-webtest-config.test.mjs b/tests/skills/integration/build-webtest-config.test.mjs index 9d265769..918adcb4 100644 --- a/tests/skills/integration/build-webtest-config.test.mjs +++ b/tests/skills/integration/build-webtest-config.test.mjs @@ -603,21 +603,44 @@ export const steps = [ }, // ── 4. DCS for report ── + // Сначала добавляем макет ОсновнаяСхемаКомпоновкиДанных к отчёту (регистрируется + // в Reports/ОстаткиТоваров.xml + автоматически выставляется MainDataCompositionSchema), + // затем skd-compile наполняет его содержимым. + { + name: 'template-add: ОсновнаяСхемаКомпоновкиДанных к отчёту ОстаткиТоваров', + script: 'template-add/scripts/add-template', + args: { + '-ObjectName': 'ОстаткиТоваров', + '-TemplateName': 'ОсновнаяСхемаКомпоновкиДанных', + '-TemplateType': 'DataCompositionSchema', + '-SrcDir': '{workDir}/Reports', + }, + }, { name: 'skd-compile: Схема отчёта ОстаткиТоваров', script: 'skd-compile/scripts/skd-compile', input: { dataSets: [{ name: 'НаборДанных', - type: 'Query', - query: 'SELECT Номенклатура, Количество, Цена, Сумма FROM Document.ПриходнаяНакладная.Товары', + query: 'ВЫБРАТЬ\n\tТовары.Ссылка КАК Документ,\n\tТовары.Номенклатура КАК Номенклатура,\n\tТовары.Количество КАК Количество,\n\tТовары.Цена КАК Цена,\n\tТовары.Сумма КАК Сумма\nИЗ\n\tДокумент.ПриходнаяНакладная.Товары КАК Товары', + fields: [ + { field: 'Документ', title: 'Документ', type: 'DocumentRef.ПриходнаяНакладная' }, + { field: 'Номенклатура', title: 'Номенклатура', type: 'CatalogRef.Номенклатура' }, + { field: 'Количество', title: 'Количество', type: 'decimal(15,3)' }, + { field: 'Цена', title: 'Цена', type: 'decimal(15,2)' }, + { field: 'Сумма', title: 'Сумма', type: 'decimal(15,2)' }, + ], + }], + totalFields: ['Количество: Сумма', 'Сумма: Сумма'], + settingsVariants: [{ + name: 'Основной', + title: 'Остатки товаров', + settings: { + selection: ['Номенклатура', 'Количество', 'Сумма', 'Auto'], + filter: ['Номенклатура = _ @off @user @quickAccess'], + structure: 'Номенклатура > details', + }, }], - fields: [ - { name: 'Номенклатура', title: 'Номенклатура' }, - { name: 'Количество', title: 'Количество' }, - { name: 'Цена', title: 'Цена' }, - { name: 'Сумма', title: 'Сумма' }, - ], }, args: { '-DefinitionFile': '{inputFile}', '-OutputPath': '{workDir}/Reports/ОстаткиТоваров/Templates/ОсновнаяСхемаКомпоновкиДанных/Ext/Template.xml' }, validate: { script: 'skd-validate/scripts/skd-validate', flag: '-TemplatePath', path: 'Reports/ОстаткиТоваров/Templates/ОсновнаяСхемаКомпоновкиДанных/Ext/Template.xml' }, diff --git a/tests/web-test/11-report.test.mjs b/tests/web-test/11-report.test.mjs new file mode 100644 index 00000000..f68e7d94 --- /dev/null +++ b/tests/web-test/11-report.test.mjs @@ -0,0 +1,103 @@ +export const name = 'DCS-отчёт: structured smoke + быстрый пользовательский фильтр'; +export const tags = ['report', 'smoke']; +export const timeout = 90000; + +export default async function({ navigateSection, openCommand, getFormState, getCommands, clickElement, selectValue, fillFields, readSpreadsheet, closeForm, wait, assert, step, log }) { + + await step('navigation: команда отчёта зарегистрирована в подсистеме Склад', async () => { + const r = await navigateSection('Склад'); + const flat = (r.commands || []).flat(); + log(`commands: ${JSON.stringify(flat)}`); + assert.ok(flat.includes('Остатки товаров'), 'В подсистеме Склад есть команда «Остатки товаров»'); + }); + + await step('open: openCommand отрывает форму отчёта с кнопкой Сформировать', async () => { + const s = await openCommand('Остатки товаров'); + log(`form=${s.form} formCount=${s.formCount} buttons=${s.buttons?.map(b => b.name).join(',')}`); + assert.equal(s.formCount, 1, 'Открыта одна форма'); + const submit = s.buttons?.find(b => b.name === 'Сформировать'); + assert.ok(submit, 'Есть кнопка «Сформировать»'); + assert.equal(submit.default, true, '«Сформировать» — кнопка по умолчанию'); + }); + + await step('reset: сброс пользовательских настроек к стандартным', async () => { + // 1С хранит пользовательские настройки между сессиями — сбрасываем к дефолту, + // чтобы тест был идемпотентным независимо от предыдущих прогонов. + await clickElement('Еще'); + await clickElement('Установить стандартные настройки'); + }); + + await step('quickAccess: быстрый фильтр Номенклатура виден и выключен по умолчанию', async () => { + const s = await getFormState(); + log(`reportSettings: ${JSON.stringify(s.reportSettings)}`); + assert.ok(Array.isArray(s.reportSettings) && s.reportSettings.length === 1, 'Один быстрый фильтр в reportSettings'); + const f = s.reportSettings[0]; + assert.equal(f.name, 'Номенклатура', 'Имя фильтра — заголовок DCS-поля'); + assert.equal(f.enabled, false, '@off — выключен по умолчанию'); + assert.equal(f.value, '', 'Значение пустое'); + assert.ok(Array.isArray(f.actions) && f.actions.includes('select'), 'Доступно действие select'); + }); + + let baseRowCount = 0; + let baseTotalSum = ''; + + await step('generate: отчёт без фильтра возвращает все строки', async () => { + await clickElement('Сформировать'); + await wait(3); + const r = await readSpreadsheet(); + log(`headers=${JSON.stringify(r.headers)} total=${r.total} totals=${JSON.stringify(r.totals)}`); + assert.deepEqual(r.headers, ['Номенклатура', 'Количество', 'Сумма'], 'Заголовки колонок отчёта'); + assert.ok(r.data?.length >= 2, 'В отчёте есть строки данных'); + assert.ok(r.totals?.['Сумма'], 'Есть итог по Сумме'); + baseRowCount = r.data.length; + baseTotalSum = r.totals['Сумма']; + }); + + await step('apply filter: selectValue включает чекбокс и подставляет значение', async () => { + const r = await selectValue('Номенклатура', 'Товар 02'); + log(`selected: ${JSON.stringify(r.selected)}`); + assert.ok(r.selected, 'selectValue вернул объект selected'); + const after = await getFormState(); + const f = after.reportSettings?.[0]; + log(`after filter: ${JSON.stringify(f)}`); + assert.equal(f.enabled, true, 'Чекбокс быстрого фильтра автоматически включился'); + assert.equal(f.value, 'Товар 02', 'Подставилось выбранное значение'); + }); + + await step('regenerate: отчёт с фильтром возвращает только подходящие строки', async () => { + await clickElement('Сформировать'); + await wait(3); + const r = await readSpreadsheet(); + log(`filtered total=${r.total} rows=${r.data?.length} totals=${JSON.stringify(r.totals)}`); + assert.ok(r.data.length < baseRowCount, `Строк меньше чем без фильтра (${r.data.length} < ${baseRowCount})`); + const named = r.data.filter(row => row['Номенклатура']); + assert.ok(named.length >= 1, 'Есть хотя бы одна именованная строка'); + assert.ok(named.every(row => row['Номенклатура'] === 'Товар 02'), 'Все именованные строки относятся к «Товар 02»'); + const sumKey = Object.keys(r.totals).find(k => k.includes('Сумма')); + assert.ok(sumKey, 'В totals есть колонка Суммы (платформа дописывает контекст фильтра)'); + assert.notEqual(r.totals[sumKey], baseTotalSum, 'Итог по Сумме изменился после применения фильтра'); + }); + + await step('clear filter: выключение чекбокса возвращает полный набор данных', async () => { + // Снять быстрый фильтр через toggle off — fillFields с 'false' выключает чекбокс, + // value сохраняется (платформа помнит последний выбор для повторного включения), + // но данные при перерасчёте возвращаются к нефильтрованному набору. + const r = await fillFields({ 'Номенклатура': 'false' }); + log(`toggle off: ${JSON.stringify(r.filled)}`); + const after = await getFormState(); + assert.equal(after.reportSettings[0].enabled, false, 'Чекбокс выключен'); + + await clickElement('Сформировать'); + await wait(3); + const report = await readSpreadsheet(); + log(`after clear: rows=${report.data?.length} totals=${JSON.stringify(report.totals)}`); + assert.equal(report.data.length, baseRowCount, 'Восстановилось исходное число строк'); + assert.equal(report.totals['Сумма'], baseTotalSum, 'Восстановился исходный итог по Сумме'); + }); + + await step('cleanup: закрываем форму отчёта', async () => { + const r = await closeForm(); + log(`closed=${r.closed} formCount=${r.formCount}`); + assert.equal(r.closed, true, 'Форма закрылась'); + }); +}