Operand

pro bo? no.

gram: docs

> ./e2e/tests/issue-763-caret-multipage-table.spec.ts

/**
* Caret position inside a table cell that spans pages (issue #763).
*
* When a row breaks mid-content across a page boundary the painter renders the
* cell content in both fragments with identical `data-pm-start`/`data-pm-end`.
* The caret lookup used to return the first (page-1, clipped) copy, leaving the
* cursor on the previous page. It must land on the page where the cursor's text
* is actually visible. Core fix (clickToPositionDom) → covers React and Vue.
*
* Background: https://github.com/eigenpal/docx-editor/issues/763
*/
import { test, expect } from '@playwright/test';
import { EditorPage } from '../helpers/editor-page';
test('caret follows a table cell across a page break (#763)', async ({ page }) => {
const editor = new EditorPage(page);
await editor.goto();
await editor.waitForReady();
// Build a single-cell table whose content is tall enough to break across a
// page boundary, insert it at the top of the body, and put the cursor on the
// LAST line (which lands on the continuation page).
const setup = await page.evaluate(() => {
type V = { state: any; dispatch: (tr: unknown) => void; focus: () => void };
const view = (
window as unknown as { __DOCX_EDITOR_E2E__: { getView: () => V | null } }
).__DOCX_EDITOR_E2E__.getView();
if (!view) throw new Error('no view');
const schema = view.state.schema;
const LINES = 70;
const paras = [];
for (let i = 0; i < LINES; i++) {
paras.push(schema.nodes.paragraph.create(null, schema.text(`Line ${i + 1}`)));
}
const border = { style: 'single', size: 4, color: { rgb: '000000' } };
const cell = schema.nodes.tableCell.create(
{
colspan: 1,
rowspan: 1,
width: 9000,
widthType: 'dxa',
borders: { top: border, bottom: border, left: border, right: border },
},
paras
);
const row = schema.nodes.tableRow.create({ height: 360, heightRule: 'atLeast' }, [cell]);
const table = schema.nodes.table.create(
{ columnWidths: [9000], width: 9000, widthType: 'dxa', tableLayout: 'fixed' },
[row]
);
const tr = view.state.tr.insert(0, table);
view.dispatch(tr);
return { lines: LINES };
});
expect(setup.lines).toBe(70);
// Let the painter paginate the tall table.
await page.waitForTimeout(500);
// Place the cursor at the end of the last line in the cell.
await page.evaluate(() => {
type V = { state: any; dispatch: (tr: unknown) => void; focus: () => void };
const view = (
window as unknown as { __DOCX_EDITOR_E2E__: { getView: () => V | null } }
).__DOCX_EDITOR_E2E__.getView();
if (!view) throw new Error('no view');
// Locate the first table's range, then the last text position within it.
let tableStart = -1;
let tableEnd = -1;
view.state.doc.descendants((node: any, pos: number) => {
if (tableStart >= 0) return false;
if (node.type.name === 'table') {
tableStart = pos;
tableEnd = pos + node.nodeSize;
return false;
}
return true;
});
let lastTextEnd = tableStart + 1;
view.state.doc.nodesBetween(tableStart, tableEnd, (node: any, pos: number) => {
if (node.isText) lastTextEnd = pos + node.nodeSize;
return true;
});
const TS = view.state.selection.constructor;
view.dispatch(view.state.tr.setSelection(TS.create(view.state.doc, lastTextEnd)));
view.focus();
});
await page.waitForTimeout(300);
const result = await page.evaluate(() => {
const caretEl = document.querySelector('[data-testid="caret"]') as HTMLElement | null;
if (!caretEl) return { error: 'no caret' };
const caret = caretEl.getBoundingClientRect();
const caretMidY = caret.top + caret.height / 2;
const pageIndexAt = (y: number) => {
const pages = Array.from(document.querySelectorAll('.layout-page')) as HTMLElement[];
for (const p of pages) {
const r = p.getBoundingClientRect();
if (y >= r.top && y <= r.bottom) return Number(p.dataset.pageNumber || '1') - 1;
}
return -1;
};
// The visible "Line 70" copy: the one whose box is inside its table fragment.
let line70Page = -1;
let line70Y = -1;
document.querySelectorAll('.layout-page-content .layout-table .layout-line').forEach((el) => {
if (!/(^|\b)Line 70\b/.test(el.textContent || '')) return;
const r = (el as HTMLElement).getBoundingClientRect();
const tableEl = (el as HTMLElement).closest('.layout-table') as HTMLElement | null;
if (!tableEl) return;
const t = tableEl.getBoundingClientRect();
const mid = (r.top + r.bottom) / 2;
const visible = mid >= t.top - 1 && mid <= t.bottom + 1;
if (visible) {
line70Page = pageIndexAt(mid);
line70Y = r.top;
}
});
return {
caretPage: pageIndexAt(caretMidY),
caretTop: caret.top,
line70Page,
line70Y,
};
});
expect(result.error).toBeUndefined();
// The cell genuinely spans onto a continuation page.
expect(result.line70Page).toBeGreaterThanOrEqual(1);
// The caret is on the same page as the visible last line, not page 1.
expect(result.caretPage).toBe(result.line70Page);
// And vertically aligned with that visible line.
expect(Math.abs(result.caretTop! - result.line70Y!)).toBeLessThan(12);
});