diff --git a/.claude/skills/web-test/SKILL.md b/.claude/skills/web-test/SKILL.md index 26d3a73a..d81826e9 100644 --- a/.claude/skills/web-test/SKILL.md +++ b/.claude/skills/web-test/SKILL.md @@ -340,6 +340,22 @@ await selectValue('Документ', '0000-000601', { type: 'Реализаци Also supports DCS labels — auto-enables the paired checkbox. +**Multi-select (value-list fields)** — pass an **array** to select several values at once. Works +across all "список значений" surfaces (checkbox form, intermediate pool + Подбор, inline cloud +dropdown, catalog multi-row select) — the right gesture is auto-detected. Always **replace** +semantics (the field ends up with exactly the given set): +```js +await selectValue('Наименование компании', ['Альфа ООО', 'Бета АО']); +// result.selected = { field: 'Наименование компании', values: ['Альфа ООО', 'Бета АО'] } + +// Values not offered by the field land in notSelected (the call does not throw): +const r = await selectValue('Организации', ['Альфа ООО', 'Несуществующая']); +// r.selected = { field: 'Организации', values: ['Альфа ООО'], +// notSelected: [{ value: 'Несуществующая', reason: 'not_in_list' }] } +``` +Array elements are strings (the displayed value); objects `{ col: value }` are passed through as +the catalog search (for surfaces that open a selection catalog). + #### `fillTableRow(fields, opts)` → form state with `filled` (+ optional `notFilled`) Fill table row cells via Tab navigation. Value is a plain string, `{ value, type }` for composite-type cells, or `''`/`null` to clear (Shift+F4). diff --git a/.claude/skills/web-test/scripts/dom.mjs b/.claude/skills/web-test/scripts/dom.mjs index 25cc5e0b..cb2e8c06 100644 --- a/.claude/skills/web-test/scripts/dom.mjs +++ b/.claude/skills/web-test/scripts/dom.mjs @@ -89,6 +89,7 @@ export { export { readSubmenuScript, clickPopupItemScript, + readCloudDDScript, } from './dom/submenu.mjs'; export { checkErrorsScript } from './dom/errors.mjs'; diff --git a/.claude/skills/web-test/scripts/dom/submenu.mjs b/.claude/skills/web-test/scripts/dom/submenu.mjs index 337c3b27..cf9f496d 100644 --- a/.claude/skills/web-test/scripts/dom/submenu.mjs +++ b/.claude/skills/web-test/scripts/dom/submenu.mjs @@ -1,4 +1,4 @@ -// web-test dom/submenu v1.0 — popup/submenu reading and clicking +// web-test dom/submenu v1.1 — popup/submenu reading and clicking // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills /** @@ -147,3 +147,32 @@ export function clickPopupItemScript(text) { return null; })()`; } + +/** + * Read a platform "cloud dropdown" checkbox list (`.cloudDD`) — the inline + * quick-choice multi-select dropdown (e.g. a catalog with QuickChoice opened via F4). + * NOT handled by readSubmenuScript (which reads `.eddText`/`.cloud`/popup `a.press`). + * + * Returns `[{ text, checked, x, y }]` — `checked` from `.checkbox.select`, `x/y` = + * the checkbox center (click there to toggle). Toggle via page.mouse.click (a + * `.surface` backdrop swallows selector clicks); confirm by clicking OUTSIDE the panel. + * Returns `{ error: 'no_clouddd' }` when no visible `.cloudDD` panel. + */ +export function readCloudDDScript() { + return `(() => { + const norm = s => (s?.trim().replace(/\\u00a0/g, ' ') || '').replace(/\\s+/g, ' ').replace(/ё/gi, 'е'); + const panel = [...document.querySelectorAll('.cloudDD')].find(p => p.offsetWidth > 0 && p.offsetHeight > 0); + if (!panel) return { error: 'no_clouddd' }; + const items = []; + panel.querySelectorAll('.checkbox').forEach(chk => { + if (chk.offsetWidth === 0) return; + // The text label sits beside the checkbox — climb until a container has text. + let row = chk; + for (let k = 0; k < 4 && row.parentElement; k++) { row = row.parentElement; if ((row.innerText || '').trim()) break; } + const r = chk.getBoundingClientRect(); + items.push({ text: norm(row.innerText || ''), checked: chk.classList.contains('select'), + x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) }); + }); + return items; + })()`; +} diff --git a/.claude/skills/web-test/scripts/engine/forms/select-value.mjs b/.claude/skills/web-test/scripts/engine/forms/select-value.mjs index 840bd9a7..ad2ef6b3 100644 --- a/.claude/skills/web-test/scripts/engine/forms/select-value.mjs +++ b/.claude/skills/web-test/scripts/engine/forms/select-value.mjs @@ -1,4 +1,4 @@ -// web-test forms/select-value v1.25 — Reference & composite-type value selection: selectValue, fillReferenceField, selection/type-dialog pickers. +// web-test forms/select-value v1.26 — Reference & composite-type value selection: selectValue (+ array multi-select), fillReferenceField, selection/type-dialog pickers. // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import { @@ -6,7 +6,7 @@ import { } from '../core/state.mjs'; import { detectFormScript, findFieldButtonScript, resolveFieldsScript, - readSubmenuScript, checkErrorsScript, + readSubmenuScript, checkErrorsScript, readCloudDDScript, findSearchInputScript, findNamedButtonScript, findCompareTypeRadioScript, isFormVisibleScript, findPatternInputIdScript, isTypeDialogScript, isNotInListCloudVisibleScript, findChildFormByButtonScript, readTypeDialogVisibleRowsScript, @@ -22,7 +22,11 @@ import { } from '../core/helpers.mjs'; import { pasteText } from '../core/clipboard.mjs'; import { getFormState } from './state.mjs'; -import { filterList } from '../table/filter.mjs'; +import { filterList, unfilterList } from '../table/filter.mjs'; +import { clickElement } from '../core/click.mjs'; +import { fillTableRow } from '../table/row-fill.mjs'; +import { closeForm } from './close.mjs'; +import { readTable } from '../table/grid.mjs'; /** * Scan a selection-form grid for the row matching `search` (string, or a @@ -620,7 +624,255 @@ export async function fillReferenceField(selector, fieldName, value, formNum) { * B) DLB opens dropdown with history — click "Показать все" or F4 to open selection form * C) DLB opens a separate selection form directly — search + dblclick in grid */ +// Button labels for the value-list multi-select surfaces. Single source for BOTH +// surface detection and clicking (clickElement matches a button by its technical +// name OR tooltip), so detect and click never drift apart. +const MULTI_BTN = { + uncheckAll: 'СписокСнятьФлажки', // tooltip "Снять пометки со всех строк" + confirm: 'ОК', + podbor: 'Подбор', + choose: 'Выбрать', + yes: 'Да', +}; +// Recognized selection surfaces a value-list field can open. +const MULTI_SURFACE = { + checkboxForm: 'checkbox-form', // modal form: all candidates as checkboxes (+ ОК) + poolPodbor: 'pool-podbor', // intermediate "pool" form + Подбор + cloudDropdown: 'cloud-dropdown', // inline .cloudDD quick-choice checkbox dropdown + catalogMultiRow: 'catalog-multirow', // catalog selection form, Ctrl multi-row + Выбрать +}; + +/** + * Multi-select for "value-list" fields: `selectValue(field, [v1, v2, ...])`. + * Always REPLACE semantics (select exactly the given set). Dispatches across the + * 4 selection surfaces (see upload/webtest-multiselect-design.md §7a.1) — a checkbox + * form, an intermediate pool + Подбор catalog, an inline cloud dropdown, and a + * Ctrl-multi-row catalog form. Composes existing API + * (clickElement/fillTableRow/closeForm/filterList/readTable/getFormState). + * Returns flat form state + `selected: { field, values:[…selected…], notSelected?:[{value,reason}] }`. + * Surface name / step detail go to console (exec output) only, NOT the structured result. + */ +async function selectValuesMulti(fieldName, values, { type } = {}) { + ensureConnected(); + await dismissPendingErrors(); + const baseForm = await page.evaluate(detectFormScript()); + if (baseForm === null) throw new Error('selectValue(multi): no form found'); + + const valStr = (el) => typeof el === 'string' ? el : String(Object.values(el)[0]); + const targets = values.map(v => ({ raw: v, str: valStr(v) })); + const selected = [], notSelected = []; + const eqYo = (a, b) => normYo(String(a).toLowerCase()) === normYo(String(b).toLowerCase()); + const incYo = (hay, needle) => normYo(String(hay).toLowerCase()).includes(normYo(String(needle).toLowerCase())); + + // Resolve field button (DLB→CB) + DCS-checkbox auto-enable (mirror selectValue). + let btn = await page.evaluate(findFieldButtonScript(baseForm, fieldName, 'DLB')); + if (btn?.error === 'button_not_found') btn = await page.evaluate(findFieldButtonScript(baseForm, fieldName, 'CB')); + if (btn?.error) { + return returnFormState({ selected: { field: fieldName, values: [], error: 'field_not_found', + notSelected: targets.map(t => ({ value: t.str, reason: 'field_not_found' })) } }); + } + if (btn.dcsCheckbox) { + const cbSel = `[id="${btn.dcsCheckbox.inputId}"]`; + const isChecked = await page.$eval(cbSel, el => + el.classList.contains('checked') || el.classList.contains('checkboxOn') || el.classList.contains('select')).catch(() => false); + if (!isChecked) { await page.click(cbSel).catch(() => {}); await waitForStable(); } + } + + const cloudDDVisible = () => page.evaluate(`!![...document.querySelectorAll('.cloudDD')].find(p => p.offsetWidth > 0 && p.offsetHeight > 0)`); + + // ── Open + detect (F4-first) ── + await clickElement(fieldName).catch(() => {}); + await waitForStable(baseForm); + let formNum = await helperDetectNewForm(baseForm); + if (formNum === null) { + const inputId = await findFieldInputId(baseForm, btn.fieldName); + if (inputId) { await page.click(`[id="${inputId}"]`).catch(() => {}); await page.waitForTimeout(200); } + await page.keyboard.press('F4'); + await page.waitForTimeout(ACTION_WAIT); + await waitForStable(baseForm); + formNum = await helperDetectNewForm(baseForm); + } + + let surface = null; + if (formNum !== null) { + const fs = await getFormState(); + const buttons = fs.buttons || []; + const hasButton = (label) => buttons.some(b => (b.name || '').includes(label) || (b.tooltip || '').includes(label)); + if (hasButton(MULTI_BTN.podbor)) surface = MULTI_SURFACE.poolPodbor; + else if (hasButton(MULTI_BTN.choose) && !hasButton(MULTI_BTN.confirm)) surface = MULTI_SURFACE.catalogMultiRow; + else if (hasButton(MULTI_BTN.uncheckAll) || hasButton(MULTI_BTN.confirm)) surface = MULTI_SURFACE.checkboxForm; + } else if (await cloudDDVisible()) { + surface = MULTI_SURFACE.cloudDropdown; + } + console.log(`[selectValuesMulti] field="${fieldName}" surface=${surface} form=${formNum} values=${JSON.stringify(targets.map(t => t.str))}`); + + if (!surface) { + return returnFormState({ selected: { field: fieldName, values: [], error: 'surface_unrecognized', + notSelected: targets.map(t => ({ value: t.str, reason: 'open_failed' })) } }); + } + + // ── Per-surface handlers ── + const ok = (v) => selected.push(v); + const fail = (v, reason) => notSelected.push({ value: v, reason }); + + // Toggle a candidate's (checkbox) in a headerless checkbox grid by its Колонка1 text. + async function toggleByText(text, want) { + let done = false; + try { + const r = await fillTableRow({ '(checkbox)': want ? 'true' : 'false' }, { row: { 'Колонка1': text } }); + done = !!r.filled?.[0]?.ok; + } catch { done = false; } + if (!done) { + // off-window — narrow via search then retry + try { await filterList(text); } catch {} + try { + const r = await fillTableRow({ '(checkbox)': want ? 'true' : 'false' }, { row: { 'Колонка1': text } }); + done = !!r.filled?.[0]?.ok; + } catch { done = false; } + try { await unfilterList(); } catch {} + } + return done; + } + + // Surface: modal checkbox form listing ALL candidates. Replace = diff (touch only + // what changes) when the whole list is visible; bulk-uncheck + check for long lists. + async function selectInCheckboxForm() { + const t = await readTable({ maxRows: 200 }); + const listFullyVisible = !(t.hasMore && t.hasMore.below); + if (listFullyVisible) { + const checkedNow = new Set((t.rows || []).filter(r => r['(checkbox)'] === 'true').map(r => r['Колонка1'])); + const targetSet = new Set(targets.map(x => x.str)); + for (const v of checkedNow) if (!targetSet.has(v)) await toggleByText(v, false); + for (const tg of targets) { + if (checkedNow.has(tg.str)) { ok(tg.str); continue; } + (await toggleByText(tg.str, true)) ? ok(tg.str) : fail(tg.str, 'not_in_list'); + } + } else { + // Virtualized list — can't read full current state to diff; bulk-uncheck then check targets. + await clickElement(MULTI_BTN.uncheckAll).catch(() => {}); + for (const tg of targets) (await toggleByText(tg.str, true)) ? ok(tg.str) : fail(tg.str, 'not_in_list'); + } + await clickElement(MULTI_BTN.confirm); + } + + // Surface: intermediate "pool" form (already-picked rows) + Подбор to add from a catalog. + // Pool rows persist, so replace = delete all rows (not uncheck). Pool may carry + // checkboxes (custom) or be plain rows (platform — membership itself = selected). + async function selectViaPool() { + await clearPool(); + const poolHasCheckbox = (t) => (t.columns || []).includes('(checkbox)'); + const isSelectedInPool = (t, v) => { + const row = (t.rows || []).find(r => eqYo(r['Колонка1'], v)); + return !!row && (!poolHasCheckbox(t) || row['(checkbox)'] !== 'false'); + }; + let pool = await readTable({ maxRows: 200 }); + const needPodbor = []; + for (const tg of targets) { + const existing = (pool.rows || []).find(r => eqYo(r['Колонка1'], tg.str)); + if (existing) { + if (poolHasCheckbox(pool) && existing['(checkbox)'] !== 'true') { + (await toggleByText(tg.str, true)) ? ok(tg.str) : fail(tg.str, 'pool_mark_failed'); + } else ok(tg.str); // platform pool: row present = selected; or already checked + } else needPodbor.push(tg); + } + // Add the rest via a SINGLE Подбор session (catalog stays open in Подбор mode). + if (needPodbor.length) { + await clickElement(MULTI_BTN.podbor); + await waitForStable(formNum); + if (await helperDetectNewForm(formNum) !== null) { + for (const tg of needPodbor) { + try { await filterList(tg.str); } catch {} + try { await clickElement(tg.str, { dblclick: true }); } catch {} + } + await closeForm({ save: false }); + pool = await readTable({ maxRows: 200 }); + for (const tg of needPodbor) isSelectedInPool(pool, tg.str) ? ok(tg.str) : fail(tg.str, 'not_found_in_catalog'); + } else { + needPodbor.forEach(tg => fail(tg.str, 'podbor_not_opened')); + } + } + await clickElement(MULTI_BTN.confirm); + } + + // Empty the pool by selecting every row (Ctrl+A covers off-screen rows) and deleting. + async function clearPool() { + const t0 = await readTable({ maxRows: 200 }); + if (!(t0.rows || []).length) return; + try { await clickElement({ row: 0, column: 'Колонка1' }); } catch {} // focus grid (make list active) + await page.keyboard.press('Control+a'); + await page.waitForTimeout(200); + await page.keyboard.press('Delete'); + await waitForStable(formNum); + const t1 = await readTable({ maxRows: 50 }); + if ((t1.rows || []).length) await clickElement(MULTI_BTN.uncheckAll).catch(() => {}); // fallback + } + + // Surface: inline .cloudDD quick-choice dropdown. Coordinate clicks (a .surface + // backdrop swallows selector clicks); confirm by clicking OUTSIDE (never Escape/Enter/ОК). + async function selectInCloudDropdown() { + const readItems = () => page.evaluate(readCloudDDScript()); + let items = await readItems(); + if (!Array.isArray(items)) { targets.forEach(t => fail(t.str, 'dropdown_unreadable')); return; } + for (const it of items.filter(i => i.checked)) { await page.mouse.click(it.x, it.y); await page.waitForTimeout(150); } + for (const tg of targets) { + items = await readItems(); + const it = items.find(i => eqYo(i.text, tg.str)) || items.find(i => incYo(i.text, tg.str)); + if (!it) { fail(tg.str, 'not_in_list'); continue; } + if (!it.checked) { await page.mouse.click(it.x, it.y); await page.waitForTimeout(200); } + const after = (await readItems()).find(i => eqYo(i.text, tg.str)); + after?.checked ? ok(tg.str) : fail(tg.str, 'not_toggled'); + } + await clickOutsideCloudDropdown(); + } + + async function clickOutsideCloudDropdown() { + const rect = await page.evaluate(`(() => { const p = [...document.querySelectorAll('.cloudDD')].find(e => e.offsetWidth > 0); if (!p) return null; const r = p.getBoundingClientRect(); return { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height), vw: window.innerWidth }; })()`); + if (!rect) return; + const outX = (rect.x + rect.w + 80 <= rect.vw) ? rect.x + rect.w + 80 : Math.max(10, rect.x - 40); + await page.mouse.click(outX, rect.y + Math.round(rect.h / 2)); + await page.waitForTimeout(300); + } + + // Surface: catalog selection form with multi-row selection (Ctrl-click rows, then Выбрать). + async function selectInCatalogMultiRow() { + for (let i = 0; i < targets.length; i++) { + const tg = targets[i]; + try { + await clickElement(tg.str, i === 0 ? {} : { modifier: 'ctrl' }); + ok(tg.str); + } catch { fail(tg.str, 'row_not_found'); } + } + await clickElement(MULTI_BTN.choose); + await waitForStable(baseForm); + const fs = await getFormState(); + if (fs.confirmation) await clickElement(MULTI_BTN.yes).catch(() => {}); // deletion-marked element confirm + } + + try { + if (surface === MULTI_SURFACE.checkboxForm) await selectInCheckboxForm(); + else if (surface === MULTI_SURFACE.poolPodbor) await selectViaPool(); + else if (surface === MULTI_SURFACE.cloudDropdown) await selectInCloudDropdown(); + else if (surface === MULTI_SURFACE.catalogMultiRow) await selectInCatalogMultiRow(); + } catch (e) { + // Hard failure mid-flow — surface what we have; real 1C errors propagate via wrapper. + const st = await getFormState(); + st.selected = { field: fieldName, values: selected, error: e.message, + notSelected: [...notSelected, ...targets.filter(t => !selected.includes(t.str) && !notSelected.some(n => n.value === t.str)).map(t => ({ value: t.str, reason: 'aborted: ' + e.message }))] }; + const err = await checkForErrors(); + if (err) st.errors = err; + return st; + } + + await waitForStable(baseForm); + const selres = { field: fieldName, values: selected }; + if (notSelected.length) selres.notSelected = notSelected; + return returnFormState({ selected: selres }); +} + export async function selectValue(fieldName, searchText, { type } = {}) { + // Multi-select: an array of values → dispatch to the value-list multi handler + // (5 surfaces). Single-value path below is unchanged. + if (Array.isArray(searchText)) return selectValuesMulti(fieldName, searchText, arguments[2] || {}); ensureConnected(); await dismissPendingErrors(); const formNum = await page.evaluate(detectFormScript()); diff --git a/tests/web-test/22-multiselect-listfield.test.mjs b/tests/web-test/22-multiselect-listfield.test.mjs new file mode 100644 index 00000000..cdb167fa --- /dev/null +++ b/tests/web-test/22-multiselect-listfield.test.mjs @@ -0,0 +1,86 @@ +export const name = 'multi-select: selectValue(field, [v1,v2]) across 5 value-list surfaces'; +export const tags = ['multiselect', 'selectvalue', 'smoke']; +export const timeout = 420000; + +// selectValue с массивом значений → мультивыбор в полях типа «список значений». +// Стенд: обработка МножественныйВыбор, 5 поверхностей: +// ЧерезФлажки → checkbox-form, ЧерезПодбор → pool+Подбор, ОрганизацииСписок → cloudDD, +// КонтрагентыСписок → catalog multi-row (Ctrl), СписокПлатформенный → platform pool+Подбор. + +const sortq = a => [...a].sort(); + +export default async function({ navigateLink, selectValue, getFormState, wait, assert, step, log }) { + + const fieldValue = async (name) => { + const fs = await getFormState(); + return (fs.fields || []).find(f => f.name === name)?.value || ''; + }; + + await step('setup: открыть обработку МножественныйВыбор', async () => { + await navigateLink('Обработка.МножественныйВыбор'); + await wait(1); + }); + + // ── A: custom checkbox form (all candidates) ──────────────────────────────── + await step('A (checkbox-form): выбрать [Альфа, Бета]', async () => { + const r = await selectValue('Через флажки', ['Альфа', 'Бета']); + log(`A selected: ${JSON.stringify(r.selected)}`); + assert.deepEqual(sortq(r.selected.values), sortq(['Альфа', 'Бета']), 'оба значения выбраны'); + assert.ok(!r.selected.notSelected, 'notSelected отсутствует'); + assert.equal(await fieldValue('ЧерезФлажки'), 'Альфа, Бета', 'значение поля = набор'); + }); + + await step('A: replace на подмножество + несуществующее [Бета, Гамма]', async () => { + const r = await selectValue('Через флажки', ['Бета', 'Гамма']); + log(`A2 selected: ${JSON.stringify(r.selected)}`); + assert.deepEqual(r.selected.values, ['Бета'], 'осталась только Бета (Альфа снята)'); + assert.ok(r.selected.notSelected?.some(n => n.value === 'Гамма'), 'Гамма → notSelected'); + assert.equal(await fieldValue('ЧерезФлажки'), 'Бета', 'replace: поле = Бета'); + }); + + // ── B: custom pool + Подбор (catalog Номенклатура) ────────────────────────── + await step('B (pool+Подбор): выбрать [Товар 01, Товар 02]', async () => { + const r = await selectValue('Через подбор', ['Товар 01', 'Товар 02']); + log(`B selected: ${JSON.stringify(r.selected)}`); + assert.deepEqual(sortq(r.selected.values), sortq(['Товар 01', 'Товар 02']), 'оба товара подобраны'); + assert.ok(!r.selected.notSelected, 'notSelected отсутствует'); + }); + + await step('B: replace на [Товар 02] (очистка пула Ctrl+A+Delete)', async () => { + const r = await selectValue('Через подбор', ['Товар 02']); + log(`B2 selected: ${JSON.stringify(r.selected)}`); + assert.deepEqual(r.selected.values, ['Товар 02'], 'остался только Товар 02'); + assert.equal(await fieldValue('ЧерезПодбор'), 'Товар 02', 'replace: поле = Товар 02'); + }); + + // ── C: platform cloudDD checkbox dropdown ─────────────────────────────────── + await step('C (cloudDD): выбрать [Альфа, Бета]', async () => { + const r = await selectValue('Организации (список)', ['Альфа', 'Бета']); + log(`C selected: ${JSON.stringify(r.selected)}`); + assert.deepEqual(sortq(r.selected.values), sortq(['Альфа', 'Бета']), 'оба выбраны в дропдауне'); + assert.equal(await fieldValue('ОрганизацииСписок'), 'Альфа, Бета', 'значение поля = набор'); + }); + + await step('C: replace на [Бета] (снятие + клик-вне)', async () => { + const r = await selectValue('Организации (список)', ['Бета']); + log(`C2 selected: ${JSON.stringify(r.selected)}`); + assert.deepEqual(r.selected.values, ['Бета'], 'осталась только Бета'); + assert.equal(await fieldValue('ОрганизацииСписок'), 'Бета', 'replace в cloudDD'); + }); + + // ── D: platform catalog multi-row select (Ctrl) ───────────────────────────── + await step('D (catalog multi-row): выбрать [АО Запад, ООО Восток]', async () => { + const r = await selectValue('Контрагенты (список)', ['АО Запад', 'ООО Восток']); + log(`D selected: ${JSON.stringify(r.selected)}`); + assert.deepEqual(sortq(r.selected.values), sortq(['АО Запад', 'ООО Восток']), 'обе строки выбраны Ctrl-кликом'); + assert.equal(await fieldValue('КонтрагентыСписок'), 'АО Запад, ООО Восток', 'значение поля = набор'); + }); + + // ── E: platform pool + Подбор (catalog Контрагенты) ───────────────────────── + await step('E (platform pool+Подбор): выбрать [ООО Север, ООО Юг]', async () => { + const r = await selectValue('Без расширенного', ['ООО Север', 'ООО Юг']); + log(`E selected: ${JSON.stringify(r.selected)}`); + assert.deepEqual(sortq(r.selected.values), sortq(['ООО Север', 'ООО Юг']), 'оба контрагента подобраны'); + assert.ok(!r.selected.notSelected, 'notSelected отсутствует'); + }); +}