fix(web-test): handle calculator/calendar popups and type inheritance in fillTableRow

When filling composite-type cells (String+Number+Date), the previous code
tried paste first and only fell back to type selection if paste was rejected.
This failed when rows inherited a type from previous rows — paste succeeded
but the value was silently converted to the wrong type.

Now when { value, type } is specified:
1. Always clear the cell first (Shift+F4) to reset inherited type
2. Open type dialog (F4) and select the requested type
3. Dismiss calculator/calendar popups with Escape
4. Re-enter edit mode if focus was lost, then paste the value

Tested with primitive types (Число/Строка/Дата) and reference types
(Физическое лицо via selection form). Regression-tested existing
composite-type flow in Операции.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-03-05 08:16:37 +03:00
parent b1187865ec
commit 53219cae6c
+162 -60
View File
@@ -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);