diff --git a/.claude/skills/web-test/SKILL.md b/.claude/skills/web-test/SKILL.md index 9be01253..3cd44344 100644 --- a/.claude/skills/web-test/SKILL.md +++ b/.claude/skills/web-test/SKILL.md @@ -106,6 +106,13 @@ await navigateLink('РегистрНакопления.ЗаказыКлиент await navigateLink('Справочник.Контрагенты'); ``` +#### `openFile(path)` → form state +Open an external data processor or report (EPF/ERF) via File → Open. Handles the security confirmation dialog automatically. +```js +const form = await openFile('C:\\WS\\build\\МояОбработка.epf'); +const form = await openFile('build/МояОбработка.epf'); // relative paths work too +``` + #### `switchTab(name)` → form state Switch to an already-open tab/window (fuzzy match). diff --git a/.claude/skills/web-test/scripts/browser.mjs b/.claude/skills/web-test/scripts/browser.mjs index a51fbc86..2831f084 100644 --- a/.claude/skills/web-test/scripts/browser.mjs +++ b/.claude/skills/web-test/scripts/browser.mjs @@ -400,6 +400,105 @@ function normalizeE1cibUrl(url) { return `e1cib/list/${url}`; } +/** + * Open an external data processor or report (EPF/ERF) via File → Open menu. + * Handles the security confirmation dialog on first open. + * @param {string} filePath - path to EPF/ERF file (absolute or relative to cwd) + * @returns {Promise} form state of the opened processor/report + */ +export async function openFile(filePath) { + ensureConnected(); + await dismissPendingErrors(); + const absPath = pathResolve(filePath); + + const MAX_ATTEMPTS = 2; // 1st may trigger security dialog, 2nd is the real open + for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { + const formBefore = await page.evaluate(detectFormScript()); + + // 1. Ctrl+O opens 1C's "Выбор файлов" dialog + await page.keyboard.press('Control+o'); + + // 2. Wait for the file selection dialog + const dialogOk = await waitForCondition(`(() => { + const ok = document.querySelector('#fileSelectDialogOk'); + return ok && ok.offsetWidth > 0 ? true : false; + })()`, 3000); + if (!dialogOk) throw new Error("File selection dialog did not open (Ctrl+O)"); + + // 3. Click "выберите с диска" to trigger the native OS file picker + let fileChooser; + try { + [fileChooser] = await Promise.all([ + page.waitForEvent('filechooser', { timeout: 5000 }), + page.click('a.underline.pointer'), + ]); + } catch (e) { + // Try closing the dialog before throwing + await page.keyboard.press('Escape'); + throw new Error(`File chooser did not appear: ${e.message}`); + } + + // 4. Set the file path and click OK + await fileChooser.setFiles(absPath); + await page.waitForTimeout(500); + await page.click('#fileSelectDialogOk'); + await waitForStable(formBefore); + + // 5. Check for security dialog + const err = await checkForErrors(); + if (err?.confirmation) { + // Security confirmation — click the positive button (Продолжить/Да/OK) + const positiveBtn = err.confirmation.buttons.find(b => + /продолжить|да|ok|yes|открыть/i.test(b) + ) || err.confirmation.buttons[0]; + if (positiveBtn) { + const btns = await page.$$(`#form${err.confirmation.formNum}_container a.press.pressButton`); + for (const b of btns) { + const txt = (await b.textContent())?.trim(); + if (txt === positiveBtn) { await b.click(); break; } + } + await waitForStable(formBefore); + } + // After confirmation, check if EPF form appeared or a follow-up dialog showed. + // Check form change FIRST — avoids confusing a small EPF form with a modal dialog. + const formAfter = await page.evaluate(detectFormScript()); + if (formAfter != null && formAfter !== formBefore) { + // New form appeared — but is it the EPF or an informational dialog? + // Informational "re-open" dialogs are tiny (< 20 elements). + const elCount = await page.evaluate(`document.querySelectorAll('[id^="form${formAfter}_"]').length`); + if (elCount < 20) { + // Likely an info dialog — check and dismiss + const err2 = await checkForErrors(); + if (err2?.modal) { + await dismissPendingErrors(); + await waitForStable(formBefore); + continue; // retry open cycle + } + } + // It's the real EPF form + const state = await getFormState(); + state.opened = { file: absPath, attempt: attempt + 1 }; + return state; + } + // Form didn't appear — retry + continue; + } + + // No security dialog — check if form appeared + if (err?.modal) { + throw new Error(`Error opening file: ${err.modal.message}`); + } + const formAfter = await page.evaluate(detectFormScript()); + if (formAfter != null && formAfter !== formBefore) { + const state = await getFormState(); + state.opened = { file: absPath, attempt: attempt + 1 }; + return state; + } + } + + throw new Error(`Form did not open after ${MAX_ATTEMPTS} attempts for: ${absPath}`); +} + /** Navigate to a 1C navigation link via Shift+F11 dialog. Returns new form state. */ export async function navigateLink(url) { ensureConnected(); diff --git a/.claude/skills/web-test/scripts/dom.mjs b/.claude/skills/web-test/scripts/dom.mjs index dfb34376..b931d4ca 100644 --- a/.claude/skills/web-test/scripts/dom.mjs +++ b/.claude/skills/web-test/scripts/dom.mjs @@ -889,8 +889,11 @@ export function checkErrorsScript() { if (elCount > 100) continue; // Skip large content forms if (buttons.length > 1) { // Confirmation dialog (multiple buttons: Да/Нет, OK/Отмена, etc.) + // Must have a Message element — real 1C confirmations always have form{N}_Message. + // Without it, this is just a regular form with multiple buttons (e.g. EPF form). const msgEl = document.getElementById(p + 'Message'); - const message = msgEl?.innerText?.trim() || ''; + if (!msgEl || msgEl.offsetWidth === 0) continue; + const message = msgEl.innerText?.trim() || ''; const btnNames = buttons.map(el => { const b = { name: el.innerText?.trim() || '' }; if (el.classList.contains('pressDefault')) b.default = true; diff --git a/.claude/skills/web-test/scripts/run.mjs b/.claude/skills/web-test/scripts/run.mjs index 496874e9..6e61265b 100644 --- a/.claude/skills/web-test/scripts/run.mjs +++ b/.claude/skills/web-test/scripts/run.mjs @@ -119,7 +119,7 @@ async function executeScript(code) { // and stop execution immediately with diagnostic info const ACTION_FNS = [ 'clickElement', 'fillFields', 'selectValue', 'fillTableRow', - 'deleteTableRow', 'openCommand', 'navigateSection', 'navigateLink', + 'deleteTableRow', 'openCommand', 'navigateSection', 'navigateLink', 'openFile', 'closeForm', 'filterList', 'unfilterList' ]; for (const name of ACTION_FNS) { diff --git a/docs/web-test-guide.md b/docs/web-test-guide.md index 78afef25..fb39a840 100644 --- a/docs/web-test-guide.md +++ b/docs/web-test-guide.md @@ -88,6 +88,14 @@ Claude напишет сценарий, который сформирует от Claude загрузит расширение через `/db-load-xml`, затем через `/web-test` откроет форму и проверит ожидаемое поведение. +### Открытие внешней обработки + +``` +> Открой обработку build/РедакторДвижений.epf в веб-клиенте и покажи что на форме +``` + +Claude откроет EPF через Ctrl+O, автоматически обработает диалог безопасности (если есть) и прочитает форму. + ### Пошаговая отладка ``` @@ -190,6 +198,7 @@ await closeForm({ save: false }); | `navigateSection(name)` | Перейти в раздел (fuzzy match) | `{ sections, commands }` | | `openCommand(name)` | Открыть команду из панели функций | form state | | `navigateLink(path)` | Открыть по пути метаданных (`Документ.ЗаказКлиента`) | form state | +| `openFile(path)` | Открыть внешнюю обработку/отчёт (EPF/ERF) через «Файл → Открыть» | form state | | `switchTab(name)` | Переключить открытую вкладку | form state | ### Чтение