From 449e2f667e4187f84b24b249e5d506f4acf24dab Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 3 Mar 2026 14:11:43 +0300 Subject: [PATCH] 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) {