From 449e2f667e4187f84b24b249e5d506f4acf24dab Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 3 Mar 2026 14:11:43 +0300 Subject: [PATCH 1/5] feat(web-test): add openFile() for opening external processors (EPF/ERF) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opens EPF/ERF files via Ctrl+O → 1C file dialog → native file picker. Handles security confirmation dialog with up to 2 attempts for re-open. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/web-test/SKILL.md | 7 ++ .claude/skills/web-test/scripts/browser.mjs | 87 +++++++++++++++++++++ .claude/skills/web-test/scripts/run.mjs | 2 +- 3 files changed, 95 insertions(+), 1 deletion(-) 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..b8f13f86 100644 --- a/.claude/skills/web-test/scripts/browser.mjs +++ b/.claude/skills/web-test/scripts/browser.mjs @@ -400,6 +400,93 @@ 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 form appeared or we need to retry + const formAfter = await page.evaluate(detectFormScript()); + if (formAfter != null && formAfter !== formBefore) { + const state = await getFormState(); + state.opened = { file: absPath, attempt: attempt + 1 }; + return state; + } + // Form didn't open — security dialog asked to re-open, retry + continue; + } + + // No security dialog — check if form appeared + const formAfter = await page.evaluate(detectFormScript()); + if (formAfter != null && formAfter !== formBefore) { + const state = await getFormState(); + state.opened = { file: absPath, attempt: attempt + 1 }; + return state; + } + + // If modal error appeared instead of a form + if (err?.modal) { + throw new Error(`Error opening file: ${err.modal.message}`); + } + } + + 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/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) { From 2af73d25bf75b2d9f2adaeb9fffdb93a91898a82 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 3 Mar 2026 14:17:33 +0300 Subject: [PATCH 2/5] fix(web-test): handle follow-up info dialog after security confirmation in openFile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After clicking "Да" on the security confirmation, check for a second informational modal ("please re-open the file"). If present, dismiss it and retry the full Ctrl+O → filechooser cycle. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/web-test/scripts/browser.mjs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/.claude/skills/web-test/scripts/browser.mjs b/.claude/skills/web-test/scripts/browser.mjs index b8f13f86..d8922728 100644 --- a/.claude/skills/web-test/scripts/browser.mjs +++ b/.claude/skills/web-test/scripts/browser.mjs @@ -459,29 +459,36 @@ export async function openFile(filePath) { } await waitForStable(formBefore); } - // After confirmation, check if form appeared or we need to retry + // After confirmation, check for a follow-up informational dialog (OK button) + // 1C may show "file opened in safe mode, please re-open" modal + const err2 = await checkForErrors(); + if (err2?.modal) { + await dismissPendingErrors(); + await waitForStable(formBefore); + // Dismissed the info dialog — retry the whole open cycle + continue; + } + // Check if the EPF form appeared const formAfter = await page.evaluate(detectFormScript()); if (formAfter != null && formAfter !== formBefore) { const state = await getFormState(); state.opened = { file: absPath, attempt: attempt + 1 }; return state; } - // Form didn't open — security dialog asked to re-open, retry + // 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; } - - // If modal error appeared instead of a form - if (err?.modal) { - throw new Error(`Error opening file: ${err.modal.message}`); - } } throw new Error(`Form did not open after ${MAX_ATTEMPTS} attempts for: ${absPath}`); From 8b0664b18dca37df91d0e6c0ef11acd0c8472508 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 3 Mar 2026 14:19:24 +0300 Subject: [PATCH 3/5] fix(web-test): avoid confusing small EPF form with info dialog in openFile Check form change before checkForErrors() to prevent misidentifying a small EPF form (< 20 elements) as an informational modal dialog. Only call checkForErrors() on suspiciously tiny new forms. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/web-test/scripts/browser.mjs | 25 ++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/.claude/skills/web-test/scripts/browser.mjs b/.claude/skills/web-test/scripts/browser.mjs index d8922728..2831f084 100644 --- a/.claude/skills/web-test/scripts/browser.mjs +++ b/.claude/skills/web-test/scripts/browser.mjs @@ -459,18 +459,23 @@ export async function openFile(filePath) { } await waitForStable(formBefore); } - // After confirmation, check for a follow-up informational dialog (OK button) - // 1C may show "file opened in safe mode, please re-open" modal - const err2 = await checkForErrors(); - if (err2?.modal) { - await dismissPendingErrors(); - await waitForStable(formBefore); - // Dismissed the info dialog — retry the whole open cycle - continue; - } - // Check if the EPF form appeared + // 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; From 1b37bad33168e9e2de3f1c039262a648d24b8654 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 3 Mar 2026 14:21:05 +0300 Subject: [PATCH 4/5] docs(web-test): add openFile to testing guide Co-Authored-By: Claude Opus 4.6 --- docs/web-test-guide.md | 9 +++++++++ 1 file changed, 9 insertions(+) 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 | ### Чтение From 90b3d5d2e9e91027d6f04f45d54d9f7924c80f13 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 3 Mar 2026 14:29:57 +0300 Subject: [PATCH 5/5] fix(web-test): require Message element for confirmation detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forms with multiple buttons but no form{N}_Message element were falsely detected as confirmation dialogs. Real 1C confirmations (Да/Нет) always have a Message element. Now skip forms without it. Fixes false positives on small EPF forms with custom buttons. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/web-test/scripts/dom.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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;