diff --git a/.claude/skills/web-test/SKILL.md b/.claude/skills/web-test/SKILL.md index 1a3b1922..8b032083 100644 --- a/.claude/skills/web-test/SKILL.md +++ b/.claude/skills/web-test/SKILL.md @@ -129,7 +129,7 @@ Switch to an already-open tab/window (fuzzy match). ### Reading form state -#### `getFormState()` → `{ form, formCount, openForms, fields, buttons, tabs, navigation?, table, tables, filters, reportSettings? }` +#### `getFormState()` → `{ form, formCount, openForms, title, fields, buttons, tabs, navigation?, table, tables, filters, reportSettings? }` Returns current form structure. This is the primary way to understand what's on screen. **form** — active form number, or `null` when no form is open (desktop). @@ -142,6 +142,8 @@ Returns current form structure. This is the primary way to understand what's on **openTabs** — array of `{ name, active? }` from the open-windows tab bar. Only present when the tab bar is enabled in 1C settings. Do NOT rely on this — use `formCount`/`openForms` instead. +**title** — caption of the active form (`"Контрагенты"`, `"Заказ поставщику ТД00-000052 от 05.07.2022"`). Read from the form's own header, which does not depend on the open-windows tab bar; when the form shows no header, falls back to the active tab's caption, and is `null` when neither is available. + **fields** — each field has: `name`, `value`, `label?`, `actions?` (select, clear, open), `required?` (true for unfilled mandatory fields), `disabled?` (control is unavailable). `buttons[]` carry `disabled?` too. **navigation** — form navigation panel links (for objects with subordinate catalogs): `[{ name, active? }]`. Clickable via `clickElement()`. Only present when the form has a navigation panel (e.g. "Основное", "Объекты метаданных", "Подсистемы"). diff --git a/.claude/skills/web-test/regress.md b/.claude/skills/web-test/regress.md index e72f000c..fec0eb76 100644 --- a/.claude/skills/web-test/regress.md +++ b/.claude/skills/web-test/regress.md @@ -186,8 +186,8 @@ assert.match(string, regex, msg?) // regex.test(string) await assert.throws(asyncFn, msg?) // passes if fn throws (use await) // 1C-specific — operate on getFormState() / readTable() output -assert.formHasField(state, 'Контрагент', msg?) // state.fields[name] exists -assert.formTitle(state, expected, msg?) // state.title includes expected +assert.formHasField(state, 'Контрагент', msg?) // fields[] contains a field with that name +assert.formTitle(state, expected, msg?) // state.title includes expected (null title → fails saying so) assert.tableHasRow(table, predicate, msg?) // predicate: object (partial match) or fn(row) => bool // object form: { 'Наименование': 'Тест' } // fn form: r => r['Сумма'] > 100 @@ -318,7 +318,7 @@ export default async function({ clerk, manager, step, assert }) { }); await step('Кладовщик видит новый статус', async () => { const s = await clerk.getFormState(); - assert.equal(s.fields['Статус']?.value, 'Утверждён'); + assert.equal(s.fields.find(f => f.name === 'Статус')?.value, 'Утверждён'); }); await step('Освободить сессию кладовщика', async () => { await manager.closeContext('clerk'); // free a 1C license for the next test @@ -341,7 +341,7 @@ export default async function({ openCommand, clickElement, getFormState, assert, await clickElement('Создать'); await clickElement('Провести'); const s = await getFormState(); - assert.ok(s.errorModal || s.fields['Контрагент']?.required, + assert.ok(s.errorModal || s.fields.find(f => f.name === 'Контрагент')?.required, 'Должна быть ошибка валидации или поле помечено обязательным'); } ``` @@ -361,7 +361,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)); } ``` diff --git a/.claude/skills/web-test/scripts/cli/test-runner/assertions.mjs b/.claude/skills/web-test/scripts/cli/test-runner/assertions.mjs index 23afb2d0..720e1633 100644 --- a/.claude/skills/web-test/scripts/cli/test-runner/assertions.mjs +++ b/.claude/skills/web-test/scripts/cli/test-runner/assertions.mjs @@ -1,4 +1,4 @@ -// web-test cli/test-runner/assertions v1.0 — ctx.assert API +// web-test cli/test-runner/assertions v1.1 — ctx.assert API // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills export function createAssertions() { @@ -37,11 +37,23 @@ export function createAssertions() { throw new AssertionError(msg || 'Expected function to throw'); }, // 1C-specific + // `fields` is an ARRAY of { name, value, ... } — indexing it by field name yields undefined, + // which used to make this assertion throw on every call, including the valid ones. formHasField(state, fieldName, msg) { - if (!state?.fields?.[fieldName]) throw new AssertionError(msg || `Field "${fieldName}" not found in form. Available: ${Object.keys(state?.fields || {}).join(', ')}`, null, fieldName); + const names = (state?.fields || []).map(f => f.name); + if (!names.includes(fieldName)) { + throw new AssertionError(msg || `Field "${fieldName}" not found in form. Available: ${names.join(', ')}`, null, fieldName); + } }, formTitle(state, expected, msg) { - if (!state?.title?.includes(expected)) throw new AssertionError(msg || `Form title "${state?.title}" does not contain "${expected}"`, state?.title, expected); + // `title` is null when the form exposes no caption and the open-windows panel is off — + // say so instead of reporting a mismatch against "null". + if (state?.title == null) { + throw new AssertionError(msg || `Form title is not available (state.title is null), expected it to contain "${expected}"`, null, expected); + } + if (!state.title.includes(expected)) { + throw new AssertionError(msg || `Form title "${state.title}" does not contain "${expected}"`, state.title, expected); + } }, tableHasRow(table, predicate, msg) { const rows = table?.rows || []; diff --git a/.claude/skills/web-test/scripts/cli/test-runner/discover.mjs b/.claude/skills/web-test/scripts/cli/test-runner/discover.mjs index 76650fb0..b1c669b0 100644 --- a/.claude/skills/web-test/scripts/cli/test-runner/discover.mjs +++ b/.claude/skills/web-test/scripts/cli/test-runner/discover.mjs @@ -1,4 +1,4 @@ -// web-test cli/test-runner/discover v1.3 — test file discovery + state reset between tests +// web-test cli/test-runner/discover v1.4 — test file discovery + state reset between tests // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import { existsSync, readdirSync } from 'fs'; import { resolve } from 'path'; @@ -73,7 +73,9 @@ export async function resetState(ctx) { return { clean: false, attempts, lastError, form: state.form, - title: state.activeTab || null, + // state.title is the form's own caption; activeTab reads the open-windows panel, which the + // user can switch off — keep it only as the fallback it always was. + title: state.title || state.activeTab || null, modal: !!state.modal, }; } catch (e) { diff --git a/.claude/skills/web-test/scripts/dom/form-state.mjs b/.claude/skills/web-test/scripts/dom/form-state.mjs index 346471e0..b284bf2b 100644 --- a/.claude/skills/web-test/scripts/dom/form-state.mjs +++ b/.claude/skills/web-test/scripts/dom/form-state.mjs @@ -1,4 +1,4 @@ -// web-test dom/form-state v1.0 — combined detectForm + readForm + open tabs +// web-test dom/form-state v1.1 — combined detectForm + readForm + open tabs + form caption // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import { DETECT_FORM_FN, DETECT_FORMS_FN, READ_FORM_FN } from './_shared.mjs'; @@ -26,7 +26,27 @@ export function getFormStateScript() { openTabs.push(entry); }); const activeTab = openTabs.find(t => t.active)?.name || null; - const result = { form: formNum, activeTab, openForms: meta.allForms, formCount: meta.formCount, ...formData }; + // Caption of the ACTIVE form. Lives in an attribute, not in text — the div itself is empty: + //