diff --git a/.claude/skills/web-test/SKILL.md b/.claude/skills/web-test/SKILL.md index d5c969cd..d3da6d29 100644 --- a/.claude/skills/web-test/SKILL.md +++ b/.claude/skills/web-test/SKILL.md @@ -12,41 +12,18 @@ allowed-tools: # /web-test — Browser automation for 1C web client -Writes and runs automation scripts for 1C web client via Playwright. +Automates user interactions with 1C:Enterprise web client via Playwright — navigating sections, filling forms, reading tables and reports, filtering lists. -## Usage +## Quick start -``` -/web-test Открой Платежное поручение, заполни сумму 5000, скриншот -/web-test Создай "Поступление товаров", организация "Конфетпром" -/web-test Проверь список контрагентов — прочитай таблицу -``` - -## Setup (first time) - -```bash -cd .claude/skills/web-test/scripts && npm install -``` - -Requires Node.js 18+. `npm install` downloads Playwright and Chromium browser. - -## Workflow - -Runner: `.claude/skills/web-test/scripts/run.mjs` - -Use `RUN` shorthand in all commands: ```bash RUN=".claude/skills/web-test/scripts/run.mjs" -``` -### Autonomous mode (preferred for complete scenarios) +# One-shot: opens browser → runs script → closes browser → exits +node $RUN run http://localhost:8081/bpdemo test-scenario.js -Single command — opens browser, runs script, closes browser, exits: - -```bash -node $RUN run test-scenario.js -# or pipe from stdin: -cat <<'SCRIPT' | node $RUN run - +# Or pipe inline: +cat <<'SCRIPT' | node $RUN run http://localhost:8081/bpdemo - await navigateSection('Продажи'); await openCommand('Заказы клиентов'); await clickElement('Создать'); @@ -55,221 +32,333 @@ await clickElement('Провести и закрыть'); SCRIPT ``` -Process exits when done. No session files, no HTTP server. Ideal for subagents and CI. +## Setup (first time) + +```bash +cd .claude/skills/web-test/scripts && npm install +``` + +Requires Node.js 18+. `npm install` downloads Playwright and Chromium. + +## URL resolution + +Read `.v8-project.json` from project root. Each database has `id` and optional `webUrl`. +Construct URL as `http://localhost:8081/` or use `webUrl` if set. +Use `/web-publish` first if the database is not published. + +## Execution modes + +### Autonomous mode (preferred for complete scenarios) + +```bash +node $RUN run script.js # exits when done, no session +``` ### Interactive mode (step-by-step development) ```bash -# 1. Start browser session (run_in_background=true, prints JSON when ready) +# 1. Start session (run_in_background=true, prints JSON when ready) node $RUN start # 2. Execute scripts against running session cat <<'SCRIPT' | node $RUN exec - const form = await getFormState(); -console.log(JSON.stringify(form.fields, null, 2)); +console.log(JSON.stringify(form, null, 2)); SCRIPT -# 3. Screenshot anytime +# 3. Screenshot node $RUN shot result.png -# 4. Stop when done (logout + close browser) +# 4. Stop (logout + close) node $RUN stop ``` -`start` outputs "Browser ready" JSON and keeps running (HTTP server). Use `exec`/`shot`/`stop` from other commands. +`start` runs an HTTP server in background. Use `exec`/`shot`/`stop` from other shells. -## URL +### Writing exec scripts -Read `.v8-project.json` from project root. Each database has `id` and optional `webUrl`. -Construct URL as `http://localhost:8081/` or use `webUrl` if set. -Use `/web-publish` skill first if the database is not published yet. - -## Writing exec scripts - -In `exec` sandbox, all browser.mjs functions are available as globals — no `import` needed. -`console.log()` output is captured and returned in the JSON response. -`writeFileSync` and `readFileSync` are also available. +All browser.mjs exports are globals — no `import` needed. +`console.log()` output is captured in the JSON response. +`writeFileSync` / `readFileSync` also available. ## API reference ### Navigation -| Function | Description | -|----------|-------------| -| `navigateSection(name)` | Go to section (fuzzy match). Returns `{ sections, commands }` | -| `openCommand(name)` | Open command from function panel (fuzzy). Returns form state | -| `navigateLink(url)` | Open 1C navigation link via Shift+F11 dialog. Returns form state | -| `switchTab(name)` | Switch to open tab/window (fuzzy). Returns form state | - -### Reading - -| Function | Description | -|----------|-------------| -| `getFormState()` | Current form: fields (with `required` flag for unfilled mandatory fields), buttons, tabs, table meta (columns + rowCount), filters | -| `readTable({maxRows, offset})` | Table row data: `{ columns, rows: [{col: val}], total }`. Use this to read grid contents | -| `readSpreadsheet()` | Read report output (SpreadsheetDocument): `{ rows: string[][], total }`. Use after clicking "Сформировать" | -| `getSections()` | Sections + commands of active section | -| `getPageState()` | Sections + open tabs | -| `getCommands()` | Commands of current section | - -### Actions - -| Function | Description | -|----------|-------------| -| `clickElement(text, {dblclick?})` | Click button/link/tab (fuzzy). `{dblclick:true}` to open items from lists. If returns `submenu[]` — click again with item name | -| `fillFields({name: value})` | Fill form fields (fuzzy by name or label). Auto-detects checkboxes, radio, reference fields | -| `selectValue(field, search)` | Select from reference field via dropdown/selection form | -| `fillTableRow(fields, opts)` | Fill table row cells via Tab navigation. See below | -| `deleteTableRow(row, {tab?})` | Delete row by 0-based index | -| `closeForm({save})` | Close form via Escape. `save: false` auto-clicks "Нет", `save: true` auto-clicks "Да", omit — returns confirmation for caller | -| `filterList(text, opts)` | Filter list. Simple (text only) or advanced (text + field). See below | -| `unfilterList({field?})` | Clear filters. All or specific badge | - -### Utility - -| Function | Description | -|----------|-------------| -| `screenshot()` | Returns PNG Buffer | -| `wait(seconds)` | Wait N seconds, returns form state | -| `getPage()` | Raw Playwright Page for advanced scripting | - -## Key patterns - -### Fill fields - +#### `navigateSection(name)` → `{ navigated, sections, commands }` +Go to a top-level section (fuzzy match). Returns list of commands in that section. ```js -await fillFields({ - 'Организация': 'Конфетпром', // reference — auto type-ahead - 'Сумма': '5000', // plain text — clipboard paste - 'Оплачено': 'true', // checkbox — "true"/"false"/"да"/"нет" - 'Вид операции': 'Оплата поставщику' // radio — fuzzy label match -}); -// Returns: { filled: [{ field, ok, value, method }], form: {...} } +await navigateSection('Продажи'); +// { navigated: 'Продажи', sections: [...], commands: ['Заказы клиентов', ...] } ``` -### Fill table row - +#### `openCommand(name)` → form state +Open a command from the function panel (fuzzy). Returns form state of the opened form. ```js -await fillTableRow( - { 'Номенклатура': 'Бумага', 'Количество': '10', 'Цена': '100' }, - { tab: 'Товары', add: true } // add:true = new row -); -// Edit existing: { row: 0 } instead of { add: true } +const form = await openCommand('Заказы клиентов'); ``` -- Tab-based sequential navigation — field order set by 1C form config -- Fuzzy cell match: "Количество" matches "ТоварыКоличество" -- Reference cells auto-detected by autocomplete popup - -### Filter - -```js -await filterList('КП00-000018'); // simple — all columns -await filterList('Мишка', { field: 'Наименование' }); // advanced — specific column -await filterList('Мишка', { field: 'Наименование', exact: true }); // exact match -await unfilterList(); // clear all -await unfilterList({ field: 'Наименование' }); // clear specific badge -``` - -### Open item from list - -```js -// Double-click to open a document/catalog item from a list -await clickElement('0000-000227', { dblclick: true }); -// Returns the opened form state (fields, table, buttons) -``` - -Single `clickElement(text)` only selects the row. To open — always use `{dblclick: true}`. - -### Hierarchical lists (catalogs) - -Both simple and advanced search work on hierarchical catalogs (Контрагенты, Номенклатура, etc.): - -```js -await filterList('Конфетпром'); // simple search — flattens hierarchical view -await filterList('Конфетпром', { field: 'Наименование' }); // advanced — specific column -await clickElement('Конфетпром ООО', { dblclick: true }); // open found item -await closeForm(); // close item -await unfilterList(); // restore hierarchical view -``` - -Hint: if `readTable()` returns `hierarchical: true`, the list has groups. - -### Closing forms - -| Action | Method | -|--------|--------| -| Post & close document | `clickElement('Провести и закрыть')` | -| Save & close catalog | `clickElement('Записать и закрыть')` | -| Close without saving | `closeForm({ save: false })` — auto-dismisses "save changes?" | -| Close and save | `closeForm({ save: true })` — auto-confirms save | -| Close (manual confirm) | `closeForm()` — returns `confirmation` field if dialog appears | - -`closeForm()` is preferred over `clickElement('×')` — close buttons on tabs are ambiguous. - -### Keyboard shortcuts - -| Key | Context | Action | -|-----|---------|--------| -| `F8` | Reference field focused | Open creation form for the field's catalog | -| `Shift+F4` | Reference field focused | Clear field value | -| `F4` | Reference field focused | Open selection form | -| `Alt+F` | List/table form | Open advanced search dialog | - -### Navigation links - +#### `navigateLink(url)` → form state +Open any 1C object by metadata path (Shift+F11 dialog). Bypasses section/command navigation. ```js await navigateLink('Документ.ЗаказКлиента'); await navigateLink('РегистрНакопления.ЗаказыКлиентов'); await navigateLink('Справочник.Контрагенты'); ``` -Bypasses section/command navigation. Useful for registers, journals, and any form with a known path. +#### `switchTab(name)` → form state +Switch to an already-open tab/window (fuzzy match). -### Submenu navigation +### Reading form state + +#### `getFormState()` → `{ fields, buttons, tabs, table, filters, reportSettings? }` +Returns current form structure. This is the primary way to understand what's on screen. + +**fields** — each field has: `name`, `value`, `label?`, `actions?` (select, clear, open), `required?` (true for unfilled mandatory fields) + +**table** — summary only: `{ name, columns, rowCount }`. Use `readTable()` for actual data. + +**reportSettings** — for DCS reports: human-readable filter settings instead of raw technical names: +```js +const form = await getFormState(); +// form.reportSettings = [ +// { name: "Склад", enabled: true, value: "Склад бытовой техники", actions: ["select"] }, +// { name: "Номенклатура", enabled: false, value: "" } +// ] +``` + +**errorModal** — if present, 1C showed an error dialog. Read the message and decide how to proceed. + +**confirmation** — if present, a Yes/No dialog is shown. Call `clickElement('Да')` or `clickElement('Нет')`. + +### Reading data + +#### `readTable({ maxRows?, offset? })` → `{ columns, rows, total, shown, offset }` +Read actual grid data with pagination. Each row is `{ columnName: value }`. + +| Option | Default | Description | +|--------|---------|-------------| +| `maxRows` | 20 | Max rows to return per call | +| `offset` | 0 | Skip first N rows | + +Special row fields: +- `_kind: 'group'` — hierarchical group row +- `_kind: 'parent'` — parent row in hierarchy +- `_tree: 'expanded'|'collapsed'` — tree node state +- `_level: N` — nesting depth in tree view +- `hierarchical: true` — list has groups (on result object) +- `viewMode: 'tree'` — tree view active (on result object) ```js -const r = await clickElement('Ещё'); -// r.submenu = ['Расширенный поиск', 'Настройки', ...] -await clickElement('Расширенный поиск'); // click submenu item +const t = await readTable({ maxRows: 50 }); +console.log('Columns:', t.columns); +console.log('Rows:', t.rows.length, 'of', t.total); +// Pagination: +const page2 = await readTable({ maxRows: 50, offset: 50 }); ``` -## Response format (exec) +#### `readSpreadsheet()` → `{ title?, headers?, data?, totals?, rows?, total }` +Read report output (SpreadsheetDocument) after clicking "Сформировать". + +Returns structured data when header row is detected: +```js +await clickElement('Сформировать'); +await wait(5); +const report = await readSpreadsheet(); +// { title: "Остатки товаров", headers: ["Номенклатура", "Склад", "Количество"], +// data: [{ "Номенклатура": "Бумага", "Склад": "Основной", "Количество": "150" }, ...], +// totals: { "Количество": "1250" }, total: 42 } +``` + +Falls back to `{ rows: string[][], total }` when headers can't be detected. + +#### `getSections()` → `{ activeSection, sections, commands }` +Read section panel and commands without navigating. + +#### `getCommands()` → `string[]` +Commands of the current section. + +#### `getPageState()` → `{ activeSection, activeTab, sections, tabs }` +Sections + all open tabs. + +### Actions + +#### `clickElement(text, { dblclick? })` → form state +Click button, hyperlink, tab, or grid row (fuzzy match). + +- Single click selects a row in a list. **Double-click opens** the item: + ```js + await clickElement('0000-000227', { dblclick: true }); // opens document + ``` +- Returns `submenu[]` when a menu opens — click again with item name: + ```js + const r = await clickElement('Ещё'); + // r.submenu = ['Расширенный поиск', 'Настройки', ...] + await clickElement('Расширенный поиск'); + ``` +- Handles tree nodes: clicking a tree icon expands/collapses. + +#### `fillFields({ name: value })` → `{ filled, form }` +Fill form fields by label (fuzzy match). Auto-detects field type. + +| Value | Field type | Method | +|-------|-----------|--------| +| `'Конфетпром'` | Reference | Clipboard paste + typeahead | +| `'5000'` | Plain text | Clipboard paste | +| `'true'` / `'да'` | Checkbox | Toggle | +| `'Оплата поставщику'` | Radio | Fuzzy label match | + +**DCS report filters**: use human-readable label names. Checkbox is auto-enabled: +```js +await fillFields({ + 'Склад': 'Склад бытовой техники', // auto-enables "Склад" checkbox + fills value + 'Номенклатура': 'Вентилятор' // same: enables checkbox + fills +}); +``` + +Returns `{ filled: [{ field, ok, value, method }], form: {...} }`. +Method is one of: `'toggle'` | `'radio'` | `'paste'` | `'dropdown'` | `'form'` | `'typeahead'` + +#### `selectValue(field, search)` → form state with `selected` +Select a value from reference field via dropdown or selection form. More reliable than `fillFields` for reference fields that need exact selection from a catalog. + +```js +await selectValue('Организация', 'Конфетпром'); +// result.selected = { field: 'Организация', search: 'Конфетпром', method: 'dropdown'|'form' } +``` + +Also supports DCS labels — auto-enables the paired checkbox. + +#### `fillTableRow(fields, opts)` → form state +Fill table row cells via Tab navigation. + +```js +// Add new row: +await fillTableRow( + { 'Номенклатура': 'Бумага', 'Количество': '10', 'Цена': '100' }, + { tab: 'Товары', add: true } +); +// Edit existing row: +await fillTableRow( + { 'Количество': '20' }, + { tab: 'Товары', row: 0 } +); +``` + +- Tab-based sequential navigation — field order set by 1C form config +- Fuzzy cell match: "Количество" matches "ТоварыКоличество" +- Reference cells auto-detected by autocomplete popup + +#### `deleteTableRow(row, { tab? })` → form state +Delete row by 0-based index. + +#### `closeForm({ save? })` → form state +Close the current form via Escape. + +| Argument | Behavior | +|----------|----------| +| `{ save: false }` | Auto-clicks "Нет" on confirmation | +| `{ save: true }` | Auto-clicks "Да" on confirmation | +| `{}` (omitted) | Returns `confirmation` field if dialog appears | + +Preferred over `clickElement('×')` — close buttons on tabs are ambiguous. + +#### `filterList(text, opts?)` → form state +Filter list. Simple mode searches all columns, advanced mode targets a specific field. + +```js +await filterList('КП00-000018'); // simple — all columns +await filterList('Мишка', { field: 'Наименование' }); // advanced — specific column +await filterList('Мишка', { field: 'Наименование', exact: true }); // exact match +``` + +Works on hierarchical catalogs too (flattens the view). + +#### `unfilterList({ field? })` → form state +Clear filters. Without arguments clears all, with `{ field }` clears specific badge. + +### Utility + +#### `screenshot()` → PNG Buffer +#### `wait(seconds)` → form state +#### `getPage()` → Playwright Page (raw, for advanced scripting) + +## Common patterns + +### Create and save a document + +```js +await navigateSection('Продажи'); +await openCommand('Заказы клиентов'); +await clickElement('Создать'); +await fillFields({ 'Организация': 'Конфетпром', 'Контрагент': 'Альфа' }); +await fillTableRow({ 'Номенклатура': 'Бумага', 'Количество': '10' }, { tab: 'Товары', add: true }); +await clickElement('Провести и закрыть'); +``` + +### Open item from list + +```js +await clickElement('КП00-000227', { dblclick: true }); +// Always use { dblclick: true } — single click only selects the row +``` + +### Work with hierarchical lists + +```js +await filterList('Конфетпром'); // flatten + search +await clickElement('Конфетпром ООО', { dblclick: true }); // open +await closeForm(); +await unfilterList(); // restore hierarchy +``` + +### Generate and read a report + +```js +// Fill report filters using readable labels +await fillFields({ 'Склад': 'Основной склад' }); +await clickElement('Сформировать'); +await wait(5); +const report = await readSpreadsheet(); +console.log('Title:', report.title); +console.log('Data rows:', report.data?.length); +``` + +### Keyboard shortcuts (via `page.keyboard.press`) + +| Key | Context | Action | +|-----|---------|--------| +| `F8` | Reference field focused | Create new catalog item | +| `Shift+F4` | Reference field focused | Clear field value | +| `F4` | Reference field focused | Open selection form | +| `Alt+F` | List/table form | Open advanced search dialog | + +### Closing forms — which method to use + +| Goal | Method | +|------|--------| +| Post & close document | `clickElement('Провести и закрыть')` | +| Save & close catalog | `clickElement('Записать и закрыть')` | +| Close without saving | `closeForm({ save: false })` | +| Close and save | `closeForm({ save: true })` | +| Close (manual confirm) | `closeForm()` — returns `confirmation` if dialog appears | + +## Exec response format -Success: ```json -{ "ok": true, "output": "...console.log...", "elapsed": 3.2 } +{ "ok": true, "output": "...console.log output...", "elapsed": 3.2 } ``` -Error (with auto-screenshot): +On error (auto-screenshot taken): ```json { "ok": false, "error": "Element not found", "output": "...", "screenshot": "error-shot.png", "elapsed": 1.5 } ``` -## Script template - -```js -// Navigate to section and open list -await navigateSection('Банк и касса'); -await openCommand('Платежные поручения'); - -// Create new document -await clickElement('Создать'); - -// Fill and save -await fillFields({ 'Организация': 'Конфетпром', 'Сумма': '5000' }); -await clickElement('Провести и закрыть'); - -console.log('done'); -``` - -Run: `node $RUN run http://localhost:8081/bpdemo script.js` - -## Important +## Important notes - **Headed mode** — 1C requires visible browser, no headless -- **1C loads 30-60s** on initial connect (wait is built into `start`) -- **Fuzzy match** — all name lookups use fuzzy search (exact > startsWith > includes) -- **errorModal** — if response contains `errorModal`, 1C showed an error dialog -- **Clipboard paste** — all fields filled via Ctrl+V (triggers 1C events properly) -- **Stdin pipe for Cyrillic** — use `cat <<'SCRIPT' | node $RUN exec -` to avoid bash escaping +- **Startup time** — 1C loads 30-60s on initial connect (built into `start`) +- **Fuzzy matching** — all name lookups: exact > startsWith > includes +- **Clipboard paste** — all text fields filled via Ctrl+V (triggers 1C events properly) +- **Cyrillic in bash** — use `cat <<'SCRIPT' | node $RUN exec -` to avoid escaping issues +- **Non-breaking spaces** — 1C uses `\u00a0` instead of regular spaces. All matching is normalized internally