From c945edb5eae8d5ae1863cd84fd45b44776a0514b Mon Sep 17 00:00:00 2001 From: tmichela Date: Sat, 15 Aug 2026 04:36:03 +0200 Subject: [PATCH] fix: invalidate cached row height on deinitialize Clear heightInitialized when row cells are discarded so virtual renderers normalize recreated cells after large scroll jumps and column refits. Add unit and browser regressions for the stale cell-height cache. --- src/js/core/row/Row.js | 1 + test/e2e/cell-height-recycle.html | 56 ++++++++++++++++++++++++++++++ test/e2e/scroll-jump.spec.ts | 57 +++++++++++++++++++++++++++++++ test/unit/core/row/Row.spec.js | 14 ++++++++ 4 files changed, 128 insertions(+) create mode 100644 test/e2e/cell-height-recycle.html create mode 100644 test/unit/core/row/Row.spec.js diff --git a/src/js/core/row/Row.js b/src/js/core/row/Row.js index 946199c21..63393e9ad 100644 --- a/src/js/core/row/Row.js +++ b/src/js/core/row/Row.js @@ -113,6 +113,7 @@ export default class Row extends CoreFeature{ deinitialize(){ this.initialized = false; + this.heightInitialized = false; } deinitializeHeight(){ diff --git a/test/e2e/cell-height-recycle.html b/test/e2e/cell-height-recycle.html new file mode 100644 index 000000000..e6ec202ef --- /dev/null +++ b/test/e2e/cell-height-recycle.html @@ -0,0 +1,56 @@ + + + + + Tabulator Virtual Cell Height Recycling Test + + + + + +
+ + + + diff --git a/test/e2e/scroll-jump.spec.ts b/test/e2e/scroll-jump.spec.ts index 9467f663f..3e1f565a7 100644 --- a/test/e2e/scroll-jump.spec.ts +++ b/test/e2e/scroll-jump.spec.ts @@ -200,6 +200,63 @@ test.describe("Vertical scroll jumping with variable height rows (#3654)", () => }); }); +test.describe("Cell heights after virtual row recycling", () => { + test.beforeEach(async ({ page }) => { + await page.goto(`file://${join(__dirname, "cell-height-recycle.html")}`); + await page.waitForSelector(".tabulator-row"); + await page.locator(".tabulator-tableholder").hover(); + }); + + test("normalizes cells recreated after an offscreen column refit", async ({ + page, + }) => { + await scrollToBottom(page, 900); + await expect( + page + .locator('.tabulator-cell[tabulator-field="id"]') + .filter({ hasText: /^1$/ }), + ).toHaveCount(0); + + // A column refit makes the horizontal virtual renderer discard cells for + // rows outside its current vertical window. + await page.evaluate(() => window.testTable.getColumn("name").setWidth(1200)); + + // A direct jump takes the vertical renderer's full-fill path when it + // recreates the discarded cells. + await page.locator(".tabulator-tableholder").evaluate((holder) => { + holder.scrollTop = 0; + holder.dispatchEvent(new Event("scroll")); + }); + + await expect + .poll(() => + page + .locator(".tabulator-row") + .first() + .locator(".tabulator-cell") + .first() + .textContent(), + ) + .toBe("1"); + + const geometry = await page.locator(".tabulator-row").first().evaluate((row) => ({ + rowHeight: row.getBoundingClientRect().height, + cells: [...row.querySelectorAll(".tabulator-cell")].map((cell) => ({ + field: cell.getAttribute("tabulator-field"), + height: cell.getBoundingClientRect().height, + styledHeight: Number.parseFloat((cell as HTMLElement).style.height), + })), + })); + + expect(geometry.rowHeight).toBe(43); + expect(geometry.cells.length).toBeGreaterThan(0); + for (const cell of geometry.cells) { + expect(cell.height, cell.field).toBe(geometry.rowHeight); + expect(cell.styledHeight, cell.field).toBe(geometry.rowHeight); + } + }); +}); + test.describe("Vertical scroll jumping with grouped variable height rows (#3654)", () => { test.beforeEach(async ({ page }) => { await page.goto(`file://${join(__dirname, "scroll-jump-group.html")}`); diff --git a/test/unit/core/row/Row.spec.js b/test/unit/core/row/Row.spec.js new file mode 100644 index 000000000..5c4c1ec6c --- /dev/null +++ b/test/unit/core/row/Row.spec.js @@ -0,0 +1,14 @@ +import Row from "../../../../src/js/core/row/Row.js"; + +describe("Row", () => { + test("deinitialize invalidates the cached row height", () => { + const row = Object.create(Row.prototype); + row.initialized = true; + row.heightInitialized = true; + + row.deinitialize(); + + expect(row.initialized).toBe(false); + expect(row.heightInitialized).toBe(false); + }); +});