diff --git a/.claude/skills/web-test/scripts/browser.mjs b/.claude/skills/web-test/scripts/browser.mjs index 21deeb7e..5e21cf57 100644 --- a/.claude/skills/web-test/scripts/browser.mjs +++ b/.claude/skills/web-test/scripts/browser.mjs @@ -2054,20 +2054,27 @@ export async function fillTableRow(fields, { tab, add, row } = {}) { const info = pending.get(matchedKey); const text = info.value; - // === Fill this cell: clipboard paste (trusted event) === - await page.keyboard.press('Control+A'); - await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(text)})`); - await page.keyboard.press('Control+V'); - await page.waitForTimeout(1500); - - // Check if paste was rejected (composite-type cell blocks text input until type is selected) - const inputAfterPaste = await page.evaluate(`document.activeElement?.value || ''`); - if (!inputAfterPaste && text) { - // Paste rejected — cell requires type selection first via F4 - if (info.type) { - await page.keyboard.press('F4'); - await page.waitForTimeout(1000); - const typeForm = await page.evaluate(`(() => { + // If user specified a type, always clear and use type selection flow + if (info.type) { + await page.keyboard.press('Shift+F4'); // Clear cell to reset any inherited type + await page.waitForTimeout(500); + await page.keyboard.press('F4'); + await page.waitForTimeout(1000); + const typeForm = await page.evaluate(`(() => { + const forms = {}; + document.querySelectorAll('[id]').forEach(el => { + if (el.offsetWidth === 0 && el.offsetHeight === 0) return; + const m = el.id.match(/^form(\\d+)_/); + if (m) forms[m[1]] = true; + }); + const nums = Object.keys(forms).map(Number).filter(n => n > ${formNum}); + return nums.length > 0 ? Math.max(...nums) : null; + })()`); + if (typeForm !== null && await isTypeDialog(typeForm)) { + await pickFromTypeDialog(typeForm, info.type); + await waitForStable(typeForm); + // After type selection, check if a selection form opened (ref types) + const selForm = await page.evaluate(`(() => { const forms = {}; document.querySelectorAll('[id]').forEach(el => { if (el.offsetWidth === 0 && el.offsetHeight === 0) return; @@ -2077,57 +2084,101 @@ export async function fillTableRow(fields, { tab, add, row } = {}) { const nums = Object.keys(forms).map(Number).filter(n => n > ${formNum}); return nums.length > 0 ? Math.max(...nums) : null; })()`); - if (typeForm !== null && await isTypeDialog(typeForm)) { - await pickFromTypeDialog(typeForm, info.type); - await waitForStable(typeForm); - // After type selection, the selection form should open - const selForm = await page.evaluate(`(() => { - const forms = {}; - document.querySelectorAll('[id]').forEach(el => { - if (el.offsetWidth === 0 && el.offsetHeight === 0) return; - const m = el.id.match(/^form(\\d+)_/); - if (m) forms[m[1]] = true; - }); - const nums = Object.keys(forms).map(Number).filter(n => n > ${formNum}); - return nums.length > 0 ? Math.max(...nums) : null; + if (selForm === null) { + // No selection form — primitive type (Number, Date, String). + // After type pick a popup may appear (with delay): + // - Calculator (.calculate) for Number + // - Calendar (.frameCalendar) for Date + // - Direct INPUT for String + // Wait for popup, dismiss with Escape, then paste value. + await page.waitForTimeout(800); + let hasPopup = await page.evaluate(`(() => { + const calc = document.querySelector('.calculate'); + if (calc && calc.offsetWidth > 0) return 'calculator'; + const cal = document.querySelector('.frameCalendar'); + if (cal && cal.offsetWidth > 0) return 'calendar'; + return null; })()`); - if (selForm === null) { - info.filled = true; - results.push({ field: matchedKey, cell: cell.fullName, - error: 'no_selection_form', - message: `After selecting type "${info.type}", no selection form opened` }); - continue; + if (!hasPopup) { + await page.waitForTimeout(600); + hasPopup = await page.evaluate(`(() => { + const calc = document.querySelector('.calculate'); + if (calc && calc.offsetWidth > 0) return 'calculator'; + const cal = document.querySelector('.frameCalendar'); + if (cal && cal.offsetWidth > 0) return 'calendar'; + return null; + })()`); } - const pickResult = await pickFromSelectionForm(selForm, matchedKey, text, formNum); + if (hasPopup) { + await page.keyboard.press('Escape'); + await page.waitForTimeout(500); + } + // Ensure we are in an editable INPUT for this cell + const afterEl = await page.evaluate(`(() => { + const f = document.activeElement; + return { tag: f?.tagName, id: f?.id || '' }; + })()`); + if (afterEl.tag !== 'INPUT' && afterEl.tag !== 'TEXTAREA') { + const cellRect = await page.evaluate(`(() => { + const el = document.getElementById(${JSON.stringify(cell.id)}); + if (!el) return null; + const r = el.getBoundingClientRect(); + return { x: r.x + r.width / 2, y: r.y + r.height / 2 }; + })()`); + if (cellRect) { + await page.mouse.dblclick(cellRect.x, cellRect.y); + await page.waitForTimeout(500); + } + } + await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(text)})`); + await page.keyboard.press('Control+a'); + await page.keyboard.press('Control+v'); + await page.waitForTimeout(500); + await page.keyboard.press('Tab'); + await page.waitForTimeout(500); info.filled = true; - results.push(pickResult.ok - ? { field: matchedKey, cell: cell.fullName, ok: true, method: 'form', type: info.type } - : { field: matchedKey, cell: cell.fullName, - error: pickResult.error, message: pickResult.message }); + results.push({ field: matchedKey, cell: cell.fullName, ok: true, method: 'type-direct', type: info.type }); continue; } - // F4 opened something but not a type dialog — close and report - if (typeForm !== null) { - await page.keyboard.press('Escape'); - await page.waitForTimeout(300); - } + const pickResult = await pickFromSelectionForm(selForm, matchedKey, text, formNum); info.filled = true; - results.push({ field: matchedKey, cell: cell.fullName, - error: 'paste_rejected', - message: `Cell "${matchedKey}" rejected text input. F4 did not open type dialog` }); - await page.keyboard.press('Tab'); - await page.waitForTimeout(500); - continue; - } else { - // No type specified — can't fill this composite-type cell - info.filled = true; - results.push({ field: matchedKey, cell: cell.fullName, - error: 'type_required', - message: `Cell "${matchedKey}" rejected text input (composite-type). Use { value: '...', type: 'Тип' } syntax` }); - await page.keyboard.press('Tab'); - await page.waitForTimeout(500); + results.push(pickResult.ok + ? { field: matchedKey, cell: cell.fullName, ok: true, method: 'form', type: info.type } + : { field: matchedKey, cell: cell.fullName, + error: pickResult.error, message: pickResult.message }); continue; } + // F4 opened something but not a type dialog — close and report + if (typeForm !== null) { + await page.keyboard.press('Escape'); + await page.waitForTimeout(300); + } + info.filled = true; + results.push({ field: matchedKey, cell: cell.fullName, + error: 'type_dialog_failed', + message: `Cell "${matchedKey}": F4 did not open type dialog for type "${info.type}"` }); + await page.keyboard.press('Tab'); + await page.waitForTimeout(500); + continue; + } + + // === Fill this cell: clipboard paste (trusted event) === + await page.keyboard.press('Control+A'); + await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(text)})`); + await page.keyboard.press('Control+V'); + await page.waitForTimeout(1500); + + // Check if paste was rejected (composite-type cell blocks text input until type is selected) + const inputAfterPaste = await page.evaluate(`document.activeElement?.value || ''`); + if (!inputAfterPaste && text) { + // No type specified — can't fill this composite-type cell + info.filled = true; + results.push({ field: matchedKey, cell: cell.fullName, + error: 'type_required', + message: `Cell "${matchedKey}" rejected text input (composite-type). Use { value: '...', type: 'Тип' } syntax` }); + await page.keyboard.press('Tab'); + await page.waitForTimeout(500); + continue; } // Check for EDD autocomplete (indicates reference field) @@ -2313,10 +2364,61 @@ export async function fillTableRow(fields, { tab, add, row } = {}) { return nums.length > 0 ? Math.max(...nums) : null; })()`); if (selForm === null) { + // No selection form — primitive type (Number, Date, String). + // After type pick a popup may appear (with delay): + // - Calculator (.calculate) for Number + // - Calendar (.frameCalendar) for Date + // - Direct INPUT for String + // Wait for popup, dismiss with Escape, then paste value. + await page.waitForTimeout(800); + let hasPopup = await page.evaluate(`(() => { + const calc = document.querySelector('.calculate'); + if (calc && calc.offsetWidth > 0) return 'calculator'; + const cal = document.querySelector('.frameCalendar'); + if (cal && cal.offsetWidth > 0) return 'calendar'; + return null; + })()`); + if (!hasPopup) { + // Second check after extra wait + await page.waitForTimeout(600); + hasPopup = await page.evaluate(`(() => { + const calc = document.querySelector('.calculate'); + if (calc && calc.offsetWidth > 0) return 'calculator'; + const cal = document.querySelector('.frameCalendar'); + if (cal && cal.offsetWidth > 0) return 'calendar'; + return null; + })()`); + } + if (hasPopup) { + await page.keyboard.press('Escape'); + await page.waitForTimeout(500); + } + // Ensure we are in an editable INPUT for this cell + const afterEl = await page.evaluate(`(() => { + const f = document.activeElement; + return { tag: f?.tagName, id: f?.id || '' }; + })()`); + if (afterEl.tag !== 'INPUT' && afterEl.tag !== 'TEXTAREA') { + // Focus lost — re-enter edit mode via dblclick + const cellRect = await page.evaluate(`(() => { + const el = document.getElementById(${JSON.stringify(cell.id)}); + if (!el) return null; + const r = el.getBoundingClientRect(); + return { x: r.x + r.width / 2, y: r.y + r.height / 2 }; + })()`); + if (cellRect) { + await page.mouse.dblclick(cellRect.x, cellRect.y); + await page.waitForTimeout(500); + } + } + await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(text)})`); + await page.keyboard.press('Control+a'); + await page.keyboard.press('Control+v'); + await page.waitForTimeout(500); + await page.keyboard.press('Tab'); + await page.waitForTimeout(500); info.filled = true; - results.push({ field: matchedKey, cell: cell.fullName, - error: 'no_selection_form', - message: `After selecting type "${info.type}", no selection form opened` }); + results.push({ field: matchedKey, cell: cell.fullName, ok: true, method: 'type-direct', type: info.type }); continue; } const pickResult = await pickFromSelectionForm(selForm, matchedKey, text, formNum);