mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-09-03 08:40:52 +03:00
feat(web-test): поддержка гридов без шапки (headerless grids)
Единый источник деривации колонок для безголовых гридов (без .gridHead):
synthHeaderlessColumns (HEADERLESS_GRID_FN в _shared.mjs) — привязка по
colindex, имена Колонка{N}/(checkbox)/(picture), расщепление комбинированной
марк-ячейки (галка+текст в одной .gridBox) на (checkbox)+Колонка1 через subTarget.
Ветки head ? существующее : synth добавлены в:
- readTableScript, findGridCellScript, scanGridRowsScript,
findGridHeadCenterCoordsScript (grid.mjs)
- findCellCoordsByFieldsScript, findNextCellCoordsByKeyScript,
sortFieldKeysByColindexScript, readActiveGridCellScript (grid-edit.mjs)
- сводка таблиц в READ_FORM_FN (_shared.mjs)
Гейт — неизменная истинность head, headed-путь нетронут. Регресс 23/23 зелёный.
Новый tests/web-test/21-headerless.test.mjs. Live на БезшапочнаяТаблица +
МножественныйВыбор: readTable/getFormState/fillTableRow(тоггл (checkbox))/clickElement.
Follow-up: правка data-ячейки на безголовом ValueTable (edit-Tab-loop row-fill).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
08ea3f0806
commit
7c75424153
@@ -1,4 +1,4 @@
|
|||||||
// web-test dom shared v1.1 — embedded JS function constants
|
// web-test dom shared v1.2 — embedded JS function constants
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
/**
|
/**
|
||||||
* Shared function strings embedded into page.evaluate() generators.
|
* Shared function strings embedded into page.evaluate() generators.
|
||||||
@@ -43,6 +43,68 @@ export const ROW_CLICK_POINT_FN = `function rowClickPoint(line, body) {
|
|||||||
return { x: Math.round(pick.r.x + Math.min(pick.r.width / 2, 60)), y: Math.round(pick.r.y + pick.r.height / 2) };
|
return { x: Math.round(pick.r.x + Math.min(pick.r.width / 2, 60)), y: Math.round(pick.r.y + pick.r.height / 2) };
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single source of truth for column derivation on HEADERLESS grids (no `.gridHead`).
|
||||||
|
* 1C still puts `colindex` on body cells, so anchoring works without a header.
|
||||||
|
* Returns ordered descriptors consumed identically by readers (readTable, getFormState)
|
||||||
|
* and resolvers (findCellCoords, findGridCell, scanGridRows) so a synthesized name like
|
||||||
|
* "Колонка1" always maps to the same physical cell on both read and write.
|
||||||
|
*
|
||||||
|
* Descriptor: { name, kind:'data'|'checkbox'|'picture', colindex, subTarget:'checkbox'|'title'|'text'|null }
|
||||||
|
* - colindex — anchor: find the cell via line.children box with matching getAttribute('colindex').
|
||||||
|
* - subTarget — node inside that box: 'checkbox' → .checkbox, 'title' → .gridBoxTitle,
|
||||||
|
* 'text' → .gridBoxText, null → box itself.
|
||||||
|
*
|
||||||
|
* A COMBINED mark-box (one box holding BOTH .checkbox AND non-empty .gridBoxTitle, e.g. the
|
||||||
|
* value-list checkbox mark-lists) is split into TWO logical columns sharing one colindex:
|
||||||
|
* "(checkbox)" (subTarget:checkbox) + "КолонкаN" (subTarget:title). Data columns are numbered
|
||||||
|
* КолонкаN among themselves (checkbox/picture don't consume a number); duplicate
|
||||||
|
* "(checkbox)"/"(picture)" get a " 2", " 3" suffix.
|
||||||
|
*/
|
||||||
|
export const HEADERLESS_GRID_FN = `function synthHeaderlessColumns(grid) {
|
||||||
|
function picInfo(cell) {
|
||||||
|
if (!cell) return null;
|
||||||
|
if (cell.querySelector('.gridListH, .gridListV, [tree="true"], .gridBoxTree')) return null;
|
||||||
|
const dib = cell.querySelector('.gridBoxImg .dIB');
|
||||||
|
if (!dib) return null;
|
||||||
|
const bg = dib.style.backgroundImage || '';
|
||||||
|
if (!bg.includes('pictureCollection/picture/')) return null;
|
||||||
|
const m = bg.match(/[?&]gx=(\\d+)/);
|
||||||
|
return { gx: m ? m[1] : '0' };
|
||||||
|
}
|
||||||
|
const body = grid.querySelector('.gridBody');
|
||||||
|
if (!body) return [];
|
||||||
|
const line = body.querySelector('.gridLine');
|
||||||
|
if (!line) return [];
|
||||||
|
const cols = [];
|
||||||
|
let dataN = 0;
|
||||||
|
const uniq = (base) => {
|
||||||
|
if (!cols.some(c => c.name === base)) return base;
|
||||||
|
let n = 2; while (cols.some(c => c.name === base + ' ' + n)) n++;
|
||||||
|
return base + ' ' + n;
|
||||||
|
};
|
||||||
|
[...line.children].forEach(box => {
|
||||||
|
if (box.offsetWidth === 0) return;
|
||||||
|
const ci = box.getAttribute('colindex');
|
||||||
|
if (ci == null) return;
|
||||||
|
const chk = box.querySelector('.checkbox');
|
||||||
|
const titleEl = box.querySelector('.gridBoxTitle');
|
||||||
|
const textEl = box.querySelector('.gridBoxText');
|
||||||
|
const titleTxt = ((titleEl ? titleEl.innerText : '') || '').trim();
|
||||||
|
if (chk && titleTxt) {
|
||||||
|
cols.push({ name: uniq('(checkbox)'), kind: 'checkbox', colindex: ci, subTarget: 'checkbox' });
|
||||||
|
cols.push({ name: 'Колонка' + (++dataN), kind: 'data', colindex: ci, subTarget: 'title' });
|
||||||
|
} else if (chk) {
|
||||||
|
cols.push({ name: uniq('(checkbox)'), kind: 'checkbox', colindex: ci, subTarget: 'checkbox' });
|
||||||
|
} else if (picInfo(box)) {
|
||||||
|
cols.push({ name: uniq('(picture)'), kind: 'picture', colindex: ci, subTarget: null });
|
||||||
|
} else {
|
||||||
|
cols.push({ name: 'Колонка' + (++dataN), kind: 'data', colindex: ci, subTarget: textEl ? 'text' : (titleEl ? 'title' : null) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return cols;
|
||||||
|
}`;
|
||||||
|
|
||||||
/** Detect active form number. Picks form with most visible elements, skipping form0.
|
/** Detect active form number. Picks form with most visible elements, skipping form0.
|
||||||
* When modalSurface is visible — prefer the highest-numbered form (modal dialog). */
|
* When modalSurface is visible — prefer the highest-numbered form (modal dialog). */
|
||||||
export const DETECT_FORM_FN = HAS_VISIBLE_MODAL_FN + `
|
export const DETECT_FORM_FN = HAS_VISIBLE_MODAL_FN + `
|
||||||
@@ -80,7 +142,8 @@ function detectForms() {
|
|||||||
}`;
|
}`;
|
||||||
|
|
||||||
/** Read form state given prefix p. Returns { fields, buttons, tabs, texts, hyperlinks, table, iframes }. */
|
/** Read form state given prefix p. Returns { fields, buttons, tabs, texts, hyperlinks, table, iframes }. */
|
||||||
export const READ_FORM_FN = `function readForm(p) {
|
export const READ_FORM_FN = HEADERLESS_GRID_FN + `
|
||||||
|
function readForm(p) {
|
||||||
const result = {};
|
const result = {};
|
||||||
const fields = [];
|
const fields = [];
|
||||||
const buttons = [];
|
const buttons = [];
|
||||||
@@ -317,6 +380,9 @@ export const READ_FORM_FN = `function readForm(p) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (body) {
|
||||||
|
// Headerless grid — synthesize columns by colindex (single source).
|
||||||
|
synthHeaderlessColumns(grid).forEach(c => columns.push({ text: c.name, x: 0, right: 0, y: 0, h: 0 }));
|
||||||
}
|
}
|
||||||
const colNames = columns.map(c => c.text);
|
const colNames = columns.map(c => c.text);
|
||||||
const rowCount = body ? body.querySelectorAll('.gridLine').length : 0;
|
const rowCount = body ? body.querySelectorAll('.gridLine').length : 0;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// web-test dom/grid-edit v1.0 — DOM scripts for row-fill (grid edit-time operations)
|
// web-test dom/grid-edit v1.1 — DOM scripts for row-fill (grid edit-time operations)
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
//
|
//
|
||||||
|
import { HEADERLESS_GRID_FN } from './_shared.mjs';
|
||||||
|
//
|
||||||
// All helpers below accept an optional `gridSelector`. When passed, they target
|
// All helpers below accept an optional `gridSelector`. When passed, they target
|
||||||
// that exact grid; when null/undefined they pick the LAST visible `.grid` on
|
// that exact grid; when null/undefined they pick the LAST visible `.grid` on
|
||||||
// the page (this matches the implicit "current grid" used by row-fill).
|
// the page (this matches the implicit "current grid" used by row-fill).
|
||||||
@@ -24,18 +26,23 @@ function gridResolver(gridSelector) {
|
|||||||
*/
|
*/
|
||||||
export function sortFieldKeysByColindexScript(gridSelector, fieldKeys) {
|
export function sortFieldKeysByColindexScript(gridSelector, fieldKeys) {
|
||||||
return `(() => {
|
return `(() => {
|
||||||
|
${HEADERLESS_GRID_FN}
|
||||||
const grid = ${gridResolver(gridSelector)};
|
const grid = ${gridResolver(gridSelector)};
|
||||||
if (!grid) return null;
|
if (!grid) return null;
|
||||||
const head = grid.querySelector('.gridHead');
|
const head = grid.querySelector('.gridHead');
|
||||||
if (!head) return null;
|
|
||||||
const headLine = head.querySelector('.gridLine') || head;
|
|
||||||
const cols = [];
|
const cols = [];
|
||||||
[...headLine.children].forEach(box => {
|
if (head) {
|
||||||
if (box.offsetWidth === 0) return;
|
const headLine = head.querySelector('.gridLine') || head;
|
||||||
const t = ((box.querySelector('.gridBoxText') || box).innerText?.trim() || '').toLowerCase();
|
[...headLine.children].forEach(box => {
|
||||||
const ci = parseInt(box.getAttribute('colindex') || '-1');
|
if (box.offsetWidth === 0) return;
|
||||||
if (t) cols.push({ text: t, colindex: ci });
|
const t = ((box.querySelector('.gridBoxText') || box).innerText?.trim() || '').toLowerCase();
|
||||||
});
|
const ci = parseInt(box.getAttribute('colindex') || '-1');
|
||||||
|
if (t) cols.push({ text: t, colindex: ci });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Headerless: synthesized columns (КолонкаN/(checkbox)) ordered by colindex
|
||||||
|
synthHeaderlessColumns(grid).forEach(c => cols.push({ text: c.name.toLowerCase(), colindex: parseInt(c.colindex) }));
|
||||||
|
}
|
||||||
const keys = ${JSON.stringify(fieldKeys)};
|
const keys = ${JSON.stringify(fieldKeys)};
|
||||||
const mapped = keys.map(k => {
|
const mapped = keys.map(k => {
|
||||||
const exact = cols.find(c => c.text === k);
|
const exact = cols.find(c => c.text === k);
|
||||||
@@ -60,29 +67,34 @@ export function sortFieldKeysByColindexScript(gridSelector, fieldKeys) {
|
|||||||
*/
|
*/
|
||||||
export function findCellCoordsByFieldsScript(gridSelector, row, fieldKeys) {
|
export function findCellCoordsByFieldsScript(gridSelector, row, fieldKeys) {
|
||||||
return `(() => {
|
return `(() => {
|
||||||
|
${HEADERLESS_GRID_FN}
|
||||||
const grid = ${gridResolver(gridSelector)};
|
const grid = ${gridResolver(gridSelector)};
|
||||||
if (!grid) return { error: 'no_grid' };
|
if (!grid) return { error: 'no_grid' };
|
||||||
const head = grid.querySelector('.gridHead');
|
const head = grid.querySelector('.gridHead');
|
||||||
const body = grid.querySelector('.gridBody');
|
const body = grid.querySelector('.gridBody');
|
||||||
if (!head || !body) return { error: 'no_grid_body' };
|
if (!body) return { error: 'no_grid_body' };
|
||||||
|
|
||||||
// Read column headers to find target colindex
|
// Read columns to find target colindex (+ subTarget for headerless split mark-boxes)
|
||||||
const headLine = head.querySelector('.gridLine') || head;
|
|
||||||
const cols = [];
|
const cols = [];
|
||||||
[...headLine.children].forEach(box => {
|
if (head) {
|
||||||
if (box.offsetWidth === 0) return;
|
const headLine = head.querySelector('.gridLine') || head;
|
||||||
const t = box.querySelector('.gridBoxText');
|
[...headLine.children].forEach(box => {
|
||||||
const ci = box.getAttribute('colindex');
|
if (box.offsetWidth === 0) return;
|
||||||
cols.push({ colindex: ci, text: ((t || box).innerText?.trim() || '').toLowerCase() });
|
const t = box.querySelector('.gridBoxText');
|
||||||
});
|
const ci = box.getAttribute('colindex');
|
||||||
|
cols.push({ colindex: ci, text: ((t || box).innerText?.trim() || '').toLowerCase(), subTarget: null });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
synthHeaderlessColumns(grid).forEach(c => cols.push({ colindex: c.colindex, text: c.name.toLowerCase(), subTarget: c.subTarget }));
|
||||||
|
}
|
||||||
|
|
||||||
const keys = ${JSON.stringify(fieldKeys)};
|
const keys = ${JSON.stringify(fieldKeys)};
|
||||||
let targetColindex = null;
|
let targetColindex = null, targetSub = null;
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const exact = cols.find(c => c.text === key);
|
const exact = cols.find(c => c.text === key);
|
||||||
if (exact) { targetColindex = exact.colindex; break; }
|
if (exact) { targetColindex = exact.colindex; targetSub = exact.subTarget; break; }
|
||||||
const inc = cols.find(c => c.text.includes(key) || key.includes(c.text));
|
const inc = cols.find(c => c.text.includes(key) || key.includes(c.text));
|
||||||
if (inc) { targetColindex = inc.colindex; break; }
|
if (inc) { targetColindex = inc.colindex; targetSub = inc.subTarget; break; }
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = [...body.querySelectorAll('.gridLine')];
|
const rows = [...body.querySelectorAll('.gridLine')];
|
||||||
@@ -101,7 +113,12 @@ export function findCellCoordsByFieldsScript(gridSelector, row, fieldKeys) {
|
|||||||
}
|
}
|
||||||
if (!box) return { error: 'no_cell' };
|
if (!box) return { error: 'no_cell' };
|
||||||
box.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
box.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||||
const cell = box.querySelector('.gridBoxText') || box;
|
// subTarget-aware cell node: 'checkbox' → box itself (row-fill then uses findCheckboxAtPoint),
|
||||||
|
// 'title' → .gridBoxTitle, else (headed default / 'text') → .gridBoxText || box.
|
||||||
|
let cell;
|
||||||
|
if (targetSub === 'checkbox') cell = box;
|
||||||
|
else if (targetSub === 'title') cell = box.querySelector('.gridBoxTitle') || box;
|
||||||
|
else cell = box.querySelector('.gridBoxText') || box;
|
||||||
const r = cell.getBoundingClientRect();
|
const r = cell.getBoundingClientRect();
|
||||||
const currentText = (cell.innerText?.trim() || '').replace(/\\u00a0/g, ' ');
|
const currentText = (cell.innerText?.trim() || '').replace(/\\u00a0/g, ' ');
|
||||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), currentText };
|
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), currentText };
|
||||||
@@ -117,28 +134,33 @@ export function findCellCoordsByFieldsScript(gridSelector, row, fieldKeys) {
|
|||||||
*/
|
*/
|
||||||
export function findNextCellCoordsByKeyScript(gridSelector, row, key) {
|
export function findNextCellCoordsByKeyScript(gridSelector, row, key) {
|
||||||
return `(() => {
|
return `(() => {
|
||||||
|
${HEADERLESS_GRID_FN}
|
||||||
const grid = ${gridResolver(gridSelector)};
|
const grid = ${gridResolver(gridSelector)};
|
||||||
if (!grid) return null;
|
if (!grid) return null;
|
||||||
const head = grid.querySelector('.gridHead');
|
const head = grid.querySelector('.gridHead');
|
||||||
const body = grid.querySelector('.gridBody');
|
const body = grid.querySelector('.gridBody');
|
||||||
if (!head || !body) return null;
|
if (!body) return null;
|
||||||
const headLine = head.querySelector('.gridLine') || head;
|
|
||||||
const cols = [];
|
const cols = [];
|
||||||
[...headLine.children].forEach(box => {
|
if (head) {
|
||||||
if (box.offsetWidth === 0) return;
|
const headLine = head.querySelector('.gridLine') || head;
|
||||||
const t = box.querySelector('.gridBoxText');
|
[...headLine.children].forEach(box => {
|
||||||
const ci = box.getAttribute('colindex');
|
if (box.offsetWidth === 0) return;
|
||||||
cols.push({ colindex: ci, text: ((t || box).innerText?.trim() || '').toLowerCase() });
|
const t = box.querySelector('.gridBoxText');
|
||||||
});
|
const ci = box.getAttribute('colindex');
|
||||||
|
cols.push({ colindex: ci, text: ((t || box).innerText?.trim() || '').toLowerCase(), subTarget: null });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
synthHeaderlessColumns(grid).forEach(c => cols.push({ colindex: c.colindex, text: c.name.toLowerCase(), subTarget: c.subTarget }));
|
||||||
|
}
|
||||||
const kl = ${JSON.stringify(key.toLowerCase())};
|
const kl = ${JSON.stringify(key.toLowerCase())};
|
||||||
const klNoSpace = kl.replace(/[\\s\\-]+/g, '');
|
const klNoSpace = kl.replace(/[\\s\\-]+/g, '');
|
||||||
let targetColindex = null;
|
let targetColindex = null, targetSub = null;
|
||||||
const exact = cols.find(c => c.text === kl);
|
const exact = cols.find(c => c.text === kl);
|
||||||
if (exact) targetColindex = exact.colindex;
|
if (exact) { targetColindex = exact.colindex; targetSub = exact.subTarget; }
|
||||||
else {
|
else {
|
||||||
const inc = cols.find(c => c.text.includes(kl) || kl.includes(c.text)
|
const inc = cols.find(c => c.text.includes(kl) || kl.includes(c.text)
|
||||||
|| c.text.includes(klNoSpace) || klNoSpace.includes(c.text));
|
|| c.text.includes(klNoSpace) || klNoSpace.includes(c.text));
|
||||||
if (inc) targetColindex = inc.colindex;
|
if (inc) { targetColindex = inc.colindex; targetSub = inc.subTarget; }
|
||||||
}
|
}
|
||||||
if (targetColindex == null) return null;
|
if (targetColindex == null) return null;
|
||||||
const rows = [...body.querySelectorAll('.gridLine')];
|
const rows = [...body.querySelectorAll('.gridLine')];
|
||||||
@@ -147,7 +169,10 @@ export function findNextCellCoordsByKeyScript(gridSelector, row, key) {
|
|||||||
const box = [...line.children].find(b => b.offsetWidth > 0 && b.getAttribute('colindex') === targetColindex);
|
const box = [...line.children].find(b => b.offsetWidth > 0 && b.getAttribute('colindex') === targetColindex);
|
||||||
if (!box) return null;
|
if (!box) return null;
|
||||||
box.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
box.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||||
const cell = box.querySelector('.gridBoxText') || box;
|
let cell;
|
||||||
|
if (targetSub === 'checkbox') cell = box;
|
||||||
|
else if (targetSub === 'title') cell = box.querySelector('.gridBoxTitle') || box;
|
||||||
|
else cell = box.querySelector('.gridBoxText') || box;
|
||||||
const r = cell.getBoundingClientRect();
|
const r = cell.getBoundingClientRect();
|
||||||
const currentText = (cell.innerText?.trim() || '').replace(/\\u00a0/g, ' ');
|
const currentText = (cell.innerText?.trim() || '').replace(/\\u00a0/g, ' ');
|
||||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), currentText };
|
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2), currentText };
|
||||||
@@ -234,6 +259,7 @@ export function getGridEditCheckScript() {
|
|||||||
*/
|
*/
|
||||||
export function readActiveGridCellScript() {
|
export function readActiveGridCellScript() {
|
||||||
return `(() => {
|
return `(() => {
|
||||||
|
${HEADERLESS_GRID_FN}
|
||||||
const f = document.activeElement;
|
const f = document.activeElement;
|
||||||
if (!f) return { tag: 'none' };
|
if (!f) return { tag: 'none' };
|
||||||
if (f.tagName === 'INPUT' || f.tagName === 'TEXTAREA') {
|
if (f.tagName === 'INPUT' || f.tagName === 'TEXTAREA') {
|
||||||
@@ -254,6 +280,15 @@ export function readActiveGridCellScript() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!head) {
|
||||||
|
// Headerless: resolve the editing column name from the cell's colindex via synth.
|
||||||
|
let box = f; while (box && !box.classList?.contains('gridBox')) box = box.parentElement;
|
||||||
|
const ci = box?.getAttribute('colindex');
|
||||||
|
if (ci != null) {
|
||||||
|
const sc = synthHeaderlessColumns(grid).find(c => c.kind === 'data' && c.colindex === ci);
|
||||||
|
if (sc) headerText = sc.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Classify the cell's choice button (if any): ref (_DLB), calc/date (_CB iCalcB/iCalendB),
|
// Classify the cell's choice button (if any): ref (_DLB), calc/date (_CB iCalcB/iCalendB),
|
||||||
// or bare 'choice' (_CB iCB — value picked from a programmatic list, e.g. НачалоВыбора).
|
// or bare 'choice' (_CB iCB — value picked from a programmatic list, e.g. НачалоВыбора).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// web-test dom/grid v1.11 — grid resolution + table reading + edit-time helpers
|
// web-test dom/grid v1.12 — grid resolution + table reading + edit-time helpers
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
import { ROW_CLICK_POINT_FN } from './_shared.mjs';
|
import { ROW_CLICK_POINT_FN, HEADERLESS_GRID_FN } from './_shared.mjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a specific grid by semantic name (table parameter).
|
* Resolve a specific grid by semantic name (table parameter).
|
||||||
@@ -94,18 +94,92 @@ export function readTableScript(formNum, { maxRows = 20, offset = 0, gridSelecto
|
|||||||
const m = bg.match(/[?&]gx=(\\d+)/);
|
const m = bg.match(/[?&]gx=(\\d+)/);
|
||||||
return { gx: m ? m[1] : '0' };
|
return { gx: m ? m[1] : '0' };
|
||||||
}
|
}
|
||||||
|
${HEADERLESS_GRID_FN}
|
||||||
|
|
||||||
// DOM-based parsing: gridHead → columns, gridBody → gridLine rows → gridBox cells
|
// DOM-based parsing: gridHead → columns, gridBody → gridLine rows → gridBox cells
|
||||||
const head = grid.querySelector('.gridHead');
|
const head = grid.querySelector('.gridHead');
|
||||||
const body = grid.querySelector('.gridBody');
|
const body = grid.querySelector('.gridBody');
|
||||||
if (!head || !body) {
|
if (!body) {
|
||||||
// Fallback: innerText-based (for non-standard grids)
|
// Fallback: innerText-based (for non-standard grids without a body)
|
||||||
const gText = grid.innerText?.trim() || '';
|
const gText = grid.innerText?.trim() || '';
|
||||||
const lines = gText.split('\\n').filter(Boolean);
|
const lines = gText.split('\\n').filter(Boolean);
|
||||||
return { name, columns: [], rows: [], total: lines.length, offset: 0, shown: 0,
|
return { name, columns: [], rows: [], total: lines.length, offset: 0, shown: 0,
|
||||||
hint: 'Grid has no gridHead/gridBody structure' };
|
hint: 'Grid has no gridHead/gridBody structure' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HEADERLESS grid (body but no .gridHead) — synthesize columns by colindex.
|
||||||
|
// Single source: synthHeaderlessColumns. Headed path below is left untouched.
|
||||||
|
if (!head) {
|
||||||
|
const synth = synthHeaderlessColumns(grid);
|
||||||
|
const colNames = synth.map(c => c.name);
|
||||||
|
const allLines = body.querySelectorAll('.gridLine');
|
||||||
|
const total = allLines.length;
|
||||||
|
const rows = [];
|
||||||
|
const end = Math.min(${offset} + ${maxRows}, total);
|
||||||
|
for (let i = ${offset}; i < end; i++) {
|
||||||
|
const line = allLines[i];
|
||||||
|
if (!line) break;
|
||||||
|
const row = {};
|
||||||
|
const boxes = [...line.children].filter(b => b.offsetWidth > 0);
|
||||||
|
synth.forEach(c => {
|
||||||
|
const box = boxes.find(b => b.getAttribute('colindex') === c.colindex);
|
||||||
|
let val = '';
|
||||||
|
if (box) {
|
||||||
|
if (c.subTarget === 'checkbox') {
|
||||||
|
const chk = box.querySelector('.checkbox');
|
||||||
|
val = chk && chk.classList.contains('select') ? 'true' : 'false';
|
||||||
|
} else if (c.subTarget === 'title') {
|
||||||
|
val = (box.querySelector('.gridBoxTitle')?.innerText || '').trim().replace(/\\n/g, ' ');
|
||||||
|
} else if (c.subTarget === 'text') {
|
||||||
|
val = (box.querySelector('.gridBoxText')?.innerText || '').trim().replace(/\\n/g, ' ');
|
||||||
|
} else {
|
||||||
|
const pic = picInfo(box);
|
||||||
|
val = pic ? 'pic:' + pic.gx : ((box.innerText || '').trim().replace(/\\n/g, ' '));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row[c.name] = val;
|
||||||
|
});
|
||||||
|
// Row meta — mirrors the headed path (group/parent/tree/level/selected)
|
||||||
|
const imgBox = line.querySelector('.gridBoxImg');
|
||||||
|
if (imgBox) {
|
||||||
|
if (imgBox.querySelector('.gridListH')) row._kind = 'group';
|
||||||
|
else if (imgBox.querySelector('.gridListV')) row._kind = 'parent';
|
||||||
|
}
|
||||||
|
const treeBox = line.querySelector('.gridBoxTree');
|
||||||
|
if (treeBox) {
|
||||||
|
const treeIcon = imgBox?.querySelector('[tree="true"]');
|
||||||
|
if (treeIcon) { const bg = treeIcon.style.backgroundImage || ''; row._tree = bg.includes('gx=0') ? 'expanded' : 'collapsed'; }
|
||||||
|
row._level = imgBox ? imgBox.querySelectorAll('.dIB').length - 1 : 0;
|
||||||
|
}
|
||||||
|
if (line.classList.contains('selRow') || line.classList.contains('select')) row._selected = true;
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
// hasMore — mirrors the headed path
|
||||||
|
let hasMore;
|
||||||
|
const turnsBox = document.getElementById('vertButtonScroll_' + grid.id);
|
||||||
|
if (turnsBox && turnsBox.offsetHeight > 0) {
|
||||||
|
const upBtns = turnsBox.querySelectorAll('[data-home], [data-up]');
|
||||||
|
const dnBtns = turnsBox.querySelectorAll('[data-down], [data-end]');
|
||||||
|
hasMore = { above: [...upBtns].some(b => !b.classList.contains('disabled')),
|
||||||
|
below: [...dnBtns].some(b => !b.classList.contains('disabled')) };
|
||||||
|
} else {
|
||||||
|
const vs = document.getElementById('vertScroll_' + grid.id);
|
||||||
|
if (vs && vs.classList.contains('scrollV') && vs.offsetWidth > 0) {
|
||||||
|
const back = vs.querySelector('[data-track-back]')?.offsetHeight ?? 0;
|
||||||
|
const next = vs.querySelector('[data-track-next]')?.offsetHeight ?? 0;
|
||||||
|
hasMore = { above: back > 0, below: next > 0 };
|
||||||
|
} else {
|
||||||
|
hasMore = { below: body.scrollHeight > body.clientHeight };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const isTree = !!body.querySelector('.gridBoxTree');
|
||||||
|
const hasGroups = rows.some(r => r._kind === 'group');
|
||||||
|
const result = { name, columns: colNames, rows, total, offset: ${offset}, shown: rows.length, hasMore };
|
||||||
|
if (isTree) result.viewMode = 'tree';
|
||||||
|
if (hasGroups) result.hierarchical = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// Extract column headers with X-coordinates for alignment
|
// Extract column headers with X-coordinates for alignment
|
||||||
const columns = [];
|
const columns = [];
|
||||||
const headLine = head.querySelector('.gridLine') || head;
|
const headLine = head.querySelector('.gridLine') || head;
|
||||||
@@ -381,7 +455,13 @@ export function findGridHeadCenterCoordsScript(gridSelector) {
|
|||||||
const grid = ${gridResolver(gridSelector)};
|
const grid = ${gridResolver(gridSelector)};
|
||||||
if (!grid) return null;
|
if (!grid) return null;
|
||||||
const head = grid.querySelector('.gridHead');
|
const head = grid.querySelector('.gridHead');
|
||||||
if (!head) return null;
|
if (!head) {
|
||||||
|
// Headerless editable grid: no header to click for commit-defocus. Click the
|
||||||
|
// thin strip at the grid's very top edge (above the first row) so the active
|
||||||
|
// edit commits without landing on a .gridLine (which would re-enter edit).
|
||||||
|
const gr = grid.getBoundingClientRect();
|
||||||
|
return { x: Math.round(gr.x + gr.width / 2), y: Math.round(gr.y + 1) };
|
||||||
|
}
|
||||||
const r = head.getBoundingClientRect();
|
const r = head.getBoundingClientRect();
|
||||||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
||||||
})()`;
|
})()`;
|
||||||
@@ -429,6 +509,7 @@ export function getSelectedOrLastRowIndexScript(gridSelector) {
|
|||||||
export function scanGridRowsScript(formNum, search) {
|
export function scanGridRowsScript(formNum, search) {
|
||||||
return `(() => {
|
return `(() => {
|
||||||
${ROW_CLICK_POINT_FN}
|
${ROW_CLICK_POINT_FN}
|
||||||
|
${HEADERLESS_GRID_FN}
|
||||||
const p = 'form${formNum}_';
|
const p = 'form${formNum}_';
|
||||||
const grid = document.querySelector('[id^="' + p + '"].grid, [id^="' + p + '"] .grid');
|
const grid = document.querySelector('[id^="' + p + '"].grid, [id^="' + p + '"] .grid');
|
||||||
if (!grid) return null;
|
if (!grid) return null;
|
||||||
@@ -454,26 +535,35 @@ export function scanGridRowsScript(formNum, search) {
|
|||||||
} else if (isObj) {
|
} else if (isObj) {
|
||||||
// Resolve each key to a header column (fuzzy, normalised) — mirror resolveCol.
|
// Resolve each key to a header column (fuzzy, normalised) — mirror resolveCol.
|
||||||
const headLine = grid.querySelector('.gridHead .gridLine') || grid.querySelector('.gridHead');
|
const headLine = grid.querySelector('.gridHead .gridLine') || grid.querySelector('.gridHead');
|
||||||
const headers = [...(headLine ? headLine.children : [])]
|
let headers;
|
||||||
.filter(c => c.offsetWidth > 0)
|
if (headLine) {
|
||||||
.map(c => {
|
headers = [...headLine.children]
|
||||||
const t = (c.querySelector('.gridBoxText') || c).innerText || '';
|
.filter(c => c.offsetWidth > 0)
|
||||||
const title = c.getAttribute('title') || '';
|
.map(c => {
|
||||||
const r = c.getBoundingClientRect();
|
const t = (c.querySelector('.gridBoxText') || c).innerText || '';
|
||||||
return { name: disp(t) || disp(title), text: t, title, x: r.x, right: r.x + r.width };
|
const title = c.getAttribute('title') || '';
|
||||||
})
|
const r = c.getBoundingClientRect();
|
||||||
.filter(h => h.name);
|
return { name: disp(t) || disp(title), text: t, title, x: r.x, right: r.x + r.width };
|
||||||
|
})
|
||||||
|
.filter(h => h.name);
|
||||||
|
} else {
|
||||||
|
// Headerless: synthesized columns anchored by colindex.
|
||||||
|
headers = synthHeaderlessColumns(grid).map(c => ({ name: c.name, text: c.name, title: '', x: 0, right: 0, colindex: c.colindex }));
|
||||||
|
}
|
||||||
const resolveCol = name => {
|
const resolveCol = name => {
|
||||||
const n = norm(name);
|
const n = norm(name);
|
||||||
const cand = h => [h.text, h.title].filter(Boolean);
|
const cand = h => [h.text, h.title].filter(Boolean);
|
||||||
return headers.find(h => cand(h).some(t => norm(t) === n))
|
return headers.find(h => cand(h).some(t => norm(t) === n))
|
||||||
|| headers.find(h => cand(h).some(t => norm(t).includes(n)));
|
|| headers.find(h => cand(h).some(t => norm(t).includes(n)));
|
||||||
};
|
};
|
||||||
const cellAtCol = (line, col) => visCells(line).find(b => {
|
const cellAtCol = (line, col) => {
|
||||||
const r = b.getBoundingClientRect();
|
if (col.colindex != null) return visCells(line).find(b => b.getAttribute('colindex') === col.colindex);
|
||||||
const cx = r.x + r.width / 2;
|
return visCells(line).find(b => {
|
||||||
return cx >= col.x && cx < col.right;
|
const r = b.getBoundingClientRect();
|
||||||
});
|
const cx = r.x + r.width / 2;
|
||||||
|
return cx >= col.x && cx < col.right;
|
||||||
|
});
|
||||||
|
};
|
||||||
const keys = Object.keys(search);
|
const keys = Object.keys(search);
|
||||||
const cols = {};
|
const cols = {};
|
||||||
for (const k of keys) {
|
for (const k of keys) {
|
||||||
@@ -559,25 +649,33 @@ export function findGridCellScript(formNum, gridSelector, { row, column }) {
|
|||||||
if (!grid) return { error: 'no_grid' };
|
if (!grid) return { error: 'no_grid' };
|
||||||
const head = grid.querySelector('.gridHead');
|
const head = grid.querySelector('.gridHead');
|
||||||
const body = grid.querySelector('.gridBody');
|
const body = grid.querySelector('.gridBody');
|
||||||
if (!head || !body) return { error: 'no_grid_structure' };
|
if (!body) return { error: 'no_grid_structure' };
|
||||||
|
${HEADERLESS_GRID_FN}
|
||||||
|
const isHeadless = !head;
|
||||||
|
|
||||||
// Header X-ranges (mirror of readTableScript logic, simplified). We also
|
// Header X-ranges (mirror of readTableScript logic, simplified). We also
|
||||||
// remember whether each header is frozen (gridBoxFix) — frozen and scrollable
|
// remember whether each header is frozen (gridBoxFix) — frozen and scrollable
|
||||||
// columns can share X coordinates after horizontal scroll, so cell matching
|
// columns can share X coordinates after horizontal scroll, so cell matching
|
||||||
// must respect the frozen/scrollable partition.
|
// must respect the frozen/scrollable partition.
|
||||||
const headLine = head.querySelector('.gridLine') || head;
|
let headers;
|
||||||
const headers = [...headLine.children]
|
if (head) {
|
||||||
.filter(c => c.offsetWidth > 0)
|
const headLine = head.querySelector('.gridLine') || head;
|
||||||
.map(c => {
|
headers = [...headLine.children]
|
||||||
const textEl = c.querySelector('.gridBoxText');
|
.filter(c => c.offsetWidth > 0)
|
||||||
const text = (textEl || c).innerText?.trim().replace(/\\n/g, ' ') || '';
|
.map(c => {
|
||||||
// Picture/icon columns have no header text — fall back to the title tooltip
|
const textEl = c.querySelector('.gridBoxText');
|
||||||
// (mirrors readTable naming) so they can still be targeted for clicking.
|
const text = (textEl || c).innerText?.trim().replace(/\\n/g, ' ') || '';
|
||||||
const title = (c.getAttribute('title') || '').trim();
|
// Picture/icon columns have no header text — fall back to the title tooltip
|
||||||
const r = c.getBoundingClientRect();
|
// (mirrors readTable naming) so they can still be targeted for clicking.
|
||||||
return { text, title, name: text || title, x: r.x, right: r.x + r.width, fixed: c.classList.contains('gridBoxFix') };
|
const title = (c.getAttribute('title') || '').trim();
|
||||||
})
|
const r = c.getBoundingClientRect();
|
||||||
.filter(h => h.name);
|
return { text, title, name: text || title, x: r.x, right: r.x + r.width, fixed: c.classList.contains('gridBoxFix') };
|
||||||
|
})
|
||||||
|
.filter(h => h.name);
|
||||||
|
} else {
|
||||||
|
// Headerless: synthesized columns anchored by colindex (cellAtColX matches by colindex).
|
||||||
|
headers = synthHeaderlessColumns(grid).map(c => ({ text: c.name, title: '', name: c.name, x: 0, right: 0, fixed: false, colindex: c.colindex, subTarget: c.subTarget }));
|
||||||
|
}
|
||||||
|
|
||||||
const resolveCol = (name) => {
|
const resolveCol = (name) => {
|
||||||
const suffix = ' / ' + name;
|
const suffix = ' / ' + name;
|
||||||
@@ -598,13 +696,20 @@ export function findGridCellScript(formNum, gridSelector, { row, column }) {
|
|||||||
// fixed/scrollable kind as the header. After horizontal scroll a scrollable
|
// fixed/scrollable kind as the header. After horizontal scroll a scrollable
|
||||||
// cell may have the same x as a frozen one — without this guard cellAtColX
|
// cell may have the same x as a frozen one — without this guard cellAtColX
|
||||||
// would silently return the frozen cell for a scrollable header.
|
// would silently return the frozen cell for a scrollable header.
|
||||||
const cellAtColX = (line, c) => [...line.children]
|
const cellAtColX = (line, c) => {
|
||||||
.filter(b => b.offsetWidth > 0 && b.classList.contains('gridBoxFix') === c.fixed)
|
// Headerless columns carry colindex → match the body cell directly (robust,
|
||||||
.find(b => {
|
// and returns the same box for both logical columns of a combined mark-box).
|
||||||
const r = b.getBoundingClientRect();
|
if (c.colindex != null) {
|
||||||
const cx = r.x + r.width / 2;
|
return [...line.children].find(b => b.offsetWidth > 0 && b.getAttribute('colindex') === c.colindex);
|
||||||
return cx >= c.x && cx < c.right;
|
}
|
||||||
});
|
return [...line.children]
|
||||||
|
.filter(b => b.offsetWidth > 0 && b.classList.contains('gridBoxFix') === c.fixed)
|
||||||
|
.find(b => {
|
||||||
|
const r = b.getBoundingClientRect();
|
||||||
|
const cx = r.x + r.width / 2;
|
||||||
|
return cx >= c.x && cx < c.right;
|
||||||
|
});
|
||||||
|
};
|
||||||
const cellText = (b) => norm(b?.querySelector('.gridBoxText')?.innerText || b?.innerText || '');
|
const cellText = (b) => norm(b?.querySelector('.gridBoxText')?.innerText || b?.innerText || '');
|
||||||
|
|
||||||
const target = ${JSON.stringify(row)};
|
const target = ${JSON.stringify(row)};
|
||||||
@@ -644,6 +749,20 @@ export function findGridCellScript(formNum, gridSelector, { row, column }) {
|
|||||||
|
|
||||||
const cell = cellAtColX(line, col);
|
const cell = cellAtColX(line, col);
|
||||||
if (!cell) return { error: 'cell_not_in_dom', column: col.name, rowIdx };
|
if (!cell) return { error: 'cell_not_in_dom', column: col.name, rowIdx };
|
||||||
|
// Headerless: click coords target the subTarget node (checkbox image / title),
|
||||||
|
// no frozen/scroll partition in these narrow grids → trivially visible.
|
||||||
|
if (isHeadless) {
|
||||||
|
let node = cell;
|
||||||
|
if (col.subTarget === 'checkbox') node = cell.querySelector('.checkbox') || cell;
|
||||||
|
else if (col.subTarget === 'title') node = cell.querySelector('.gridBoxTitle') || cell;
|
||||||
|
else if (col.subTarget === 'text') node = cell.querySelector('.gridBoxText') || cell;
|
||||||
|
const rr = node.getBoundingClientRect();
|
||||||
|
const gb = grid.getBoundingClientRect();
|
||||||
|
return { x: Math.round(rr.x + rr.width / 2), y: Math.round(rr.y + rr.height / 2),
|
||||||
|
cellX: Math.round(rr.x), cellRight: Math.round(rr.x + rr.width),
|
||||||
|
gridX: Math.round(gb.x), gridRight: Math.round(gb.x + gb.width), scrollableLeft: Math.round(gb.x),
|
||||||
|
columnText: col.name, rowIdx, isFixed: false, cellText: cellText(cell), visible: true };
|
||||||
|
}
|
||||||
const r = cell.getBoundingClientRect();
|
const r = cell.getBoundingClientRect();
|
||||||
const gridBox = grid.getBoundingClientRect();
|
const gridBox = grid.getBoundingClientRect();
|
||||||
// Frozen columns (.gridBoxFix) stay pinned at the left edge of the grid even
|
// Frozen columns (.gridBoxFix) stay pinned at the left edge of the grid even
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
export const name = 'headerless grids: readTable / getFormState / fillTableRow / clickElement без шапки';
|
||||||
|
export const tags = ['headerless', 'table', 'smoke'];
|
||||||
|
export const timeout = 180000;
|
||||||
|
|
||||||
|
// Покрывает поддержку гридов без .gridHead (фаза 2 мультивыбора):
|
||||||
|
// - БезшапочнаяТаблица (ValueTable, отдельная колонка-чекбокс)
|
||||||
|
// - МножественныйВыбор → Через флажки (марк-список, комбинированная ячейка галка+текст)
|
||||||
|
// Деривация колонок едина (synthHeaderlessColumns): Колонка{N} + (checkbox), привязка по colindex.
|
||||||
|
// Правка data-ячейки на безголовом ValueTable — follow-up (edit-Tab-loop row-fill), здесь не тестируется.
|
||||||
|
|
||||||
|
export default async function({
|
||||||
|
navigateLink, clickElement, fillTableRow, readTable, getFormState, wait, assert, step, log, closeForm
|
||||||
|
}) {
|
||||||
|
|
||||||
|
// ── ValueTable (отдельная колонка-чекбокс) ──────────────────────────────────
|
||||||
|
await step('readTable: ValueTable без шапки → Колонка1..3 + (checkbox)', async () => {
|
||||||
|
await navigateLink('Обработка.БезшапочнаяТаблица');
|
||||||
|
await wait(1.2);
|
||||||
|
const t = await readTable({ maxRows: 10 });
|
||||||
|
log(`columns: ${JSON.stringify(t.columns)}`);
|
||||||
|
assert.deepEqual(t.columns, ['Колонка1', 'Колонка2', 'Колонка3', '(checkbox)'],
|
||||||
|
'синтетические колонки по colindex');
|
||||||
|
assert.equal(t.total, 3, '3 строки');
|
||||||
|
assert.equal(t.rows[0]['Колонка1'], 'Позиция 001', 'текст data-колонки');
|
||||||
|
assert.ok(t.rows[0]['(checkbox)'] === 'true' || t.rows[0]['(checkbox)'] === 'false',
|
||||||
|
'(checkbox) читается как true/false');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('getFormState: сводка таблицы показывает synth-колонки + rowCount', async () => {
|
||||||
|
const fs = await getFormState();
|
||||||
|
const tbl = (fs.tables || []).find(x => (x.columns || []).includes('(checkbox)'));
|
||||||
|
log(`tables: ${JSON.stringify(fs.tables)}`);
|
||||||
|
assert.ok(tbl, 'таблица с (checkbox) в сводке');
|
||||||
|
assert.deepEqual(tbl.columns, ['Колонка1', 'Колонка2', 'Колонка3', '(checkbox)'], 'колонки сводки совпадают с readTable');
|
||||||
|
assert.equal(tbl.rowCount, 3, 'rowCount');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('fillTableRow: тоггл (checkbox) по индексу строки', async () => {
|
||||||
|
const before = (await readTable({ maxRows: 5 })).rows.map(r => r['(checkbox)']);
|
||||||
|
const target = before[1] === 'true' ? 'false' : 'true'; // строка 1 — переключаем в противоположное
|
||||||
|
const r = await fillTableRow({ '(checkbox)': target }, { row: 1 });
|
||||||
|
log(`filled: ${JSON.stringify(r.filled)}`);
|
||||||
|
assert.ok(r.filled?.[0]?.ok && r.filled[0].method === 'toggle', 'method=toggle, ok');
|
||||||
|
const after = (await readTable({ maxRows: 5 })).rows.map(r => r['(checkbox)']);
|
||||||
|
assert.equal(after[1], target, `строка 1 переключена в ${target}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('fillTableRow: тоггл (checkbox) по фильтру {Колонка1}', async () => {
|
||||||
|
const r = await fillTableRow({ '(checkbox)': 'true' }, { row: { 'Колонка1': 'Позиция 002' } });
|
||||||
|
log(`filled: ${JSON.stringify(r.filled)}`);
|
||||||
|
assert.ok(r.filled?.[0]?.ok, 'ok через row-фильтр {Колонка1}');
|
||||||
|
const t = await readTable({ maxRows: 5 });
|
||||||
|
const row = t.rows.find(x => x['Колонка1'] === 'Позиция 002');
|
||||||
|
assert.equal(row['(checkbox)'], 'true', 'Позиция 002 отмечена');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('clickElement: клик по ячейке (checkbox) тогглит галку', async () => {
|
||||||
|
const t0 = await readTable({ maxRows: 5 });
|
||||||
|
const r0 = t0.rows.find(x => x['Колонка1'] === 'Позиция 003');
|
||||||
|
const want = r0['(checkbox)'] === 'true' ? 'false' : 'true';
|
||||||
|
await clickElement({ row: { 'Колонка1': 'Позиция 003' }, column: '(checkbox)' });
|
||||||
|
await wait(0.4);
|
||||||
|
const t1 = await readTable({ maxRows: 5 });
|
||||||
|
const r1 = t1.rows.find(x => x['Колонка1'] === 'Позиция 003');
|
||||||
|
assert.equal(r1['(checkbox)'], want, `клик переключил галку Позиция 003 в ${want}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Марк-список (комбинированная ячейка галка+текст в одной .gridBox) ────────
|
||||||
|
await step('марк-список: readTable → (checkbox) + Колонка1, расщепление одной ячейки', async () => {
|
||||||
|
await navigateLink('Обработка.МножественныйВыбор');
|
||||||
|
await clickElement('Через флажки');
|
||||||
|
await wait(1.2);
|
||||||
|
const t = await readTable({ maxRows: 10 });
|
||||||
|
log(`columns: ${JSON.stringify(t.columns)} rows: ${JSON.stringify(t.rows)}`);
|
||||||
|
assert.deepEqual(t.columns, ['(checkbox)', 'Колонка1'], 'комбинированная ячейка → 2 колонки');
|
||||||
|
assert.ok(t.rows.some(r => r['Колонка1'] === 'Альфа'), 'Альфа в Колонка1');
|
||||||
|
assert.ok(t.rows.every(r => r['(checkbox)'] === 'true' || r['(checkbox)'] === 'false'), '(checkbox) булев');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('марк-список: fillTableRow отмечает по фильтру {Колонка1: Альфа}', async () => {
|
||||||
|
const r = await fillTableRow({ '(checkbox)': 'true' }, { row: { 'Колонка1': 'Альфа' } });
|
||||||
|
log(`filled: ${JSON.stringify(r.filled)}`);
|
||||||
|
assert.ok(r.filled?.[0]?.ok && r.filled[0].method === 'toggle', 'method=toggle, ok');
|
||||||
|
const t = await readTable({ maxRows: 10 });
|
||||||
|
const alpha = t.rows.find(x => x['Колонка1'] === 'Альфа');
|
||||||
|
assert.equal(alpha['(checkbox)'], 'true', 'Альфа отмечена');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('cleanup: закрыть форму ввода значений', async () => {
|
||||||
|
await closeForm({ save: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user