From 251acc1a2c10eb2679af4763c2abfe9e7b9dae0e Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 23 Sep 2026 10:46:45 +0800 Subject: [PATCH 1/5] test(vtable): cover transpose frozen columns --- ...spose-frozen-columns_2026-09-23-11-00.json | 11 +++++ .../options/listTable-frozen.test.ts | 40 +++++++++++++++++++ .../issue-4111-transpose-frozen-columns.ts | 34 ++++++++++++++++ packages/vtable/examples/menu.ts | 4 ++ 4 files changed, 89 insertions(+) create mode 100644 common/changes/@visactor/vtable/test-issue-4111-transpose-frozen-columns_2026-09-23-11-00.json create mode 100644 packages/vtable/examples/frozen/issue-4111-transpose-frozen-columns.ts diff --git a/common/changes/@visactor/vtable/test-issue-4111-transpose-frozen-columns_2026-09-23-11-00.json b/common/changes/@visactor/vtable/test-issue-4111-transpose-frozen-columns_2026-09-23-11-00.json new file mode 100644 index 0000000000..4cd2edcd66 --- /dev/null +++ b/common/changes/@visactor/vtable/test-issue-4111-transpose-frozen-columns_2026-09-23-11-00.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "test: cover zero-width mounting and add a demo for frozen columns in transpose mode (GitHub #4111)", + "type": "none" + } + ], + "packageName": "@visactor/vtable", + "email": "892739385@qq.com" +} diff --git a/packages/vtable/__tests__/options/listTable-frozen.test.ts b/packages/vtable/__tests__/options/listTable-frozen.test.ts index 47ed49543c..97a61fb4e3 100644 --- a/packages/vtable/__tests__/options/listTable-frozen.test.ts +++ b/packages/vtable/__tests__/options/listTable-frozen.test.ts @@ -92,3 +92,43 @@ describe('listTable-frozen init test', () => { listTable.release(); }); }); + +describe('listTable frozen columns in transpose mode', () => { + test('keeps configured columns frozen after a zero-width mount and scrolling', () => { + const containerDom: HTMLElement = createDiv(); + containerDom.style.position = 'relative'; + containerDom.style.width = '0px'; + containerDom.style.height = '300px'; + + const transposeTable = new ListTable(containerDom, { + columns: [ + { field: 'name', title: 'Name', width: 120 }, + { field: 'value', title: 'Value', width: 120 } + ], + records: Array.from({ length: 10 }, (_, index) => ({ + name: `name-${index}`, + value: `value-${index}` + })), + defaultColWidth: 120, + frozenColCount: 2, + transpose: true + }); + + expect(transposeTable.frozenColCount).toBe(2); + expect(transposeTable.scenegraph.getColGroup(0).parent).toBe(transposeTable.scenegraph.rowHeaderGroup); + expect(transposeTable.scenegraph.getColGroup(1).parent).toBe(transposeTable.scenegraph.rowHeaderGroup); + expect(transposeTable.scenegraph.getColGroup(2).parent).toBe(transposeTable.scenegraph.bodyGroup); + + containerDom.style.width = '400px'; + transposeTable.resize(); + transposeTable.setScrollLeft(240); + + expect(transposeTable.scrollLeft).toBeGreaterThan(0); + expect(transposeTable.scenegraph.getColGroup(0).parent).toBe(transposeTable.scenegraph.rowHeaderGroup); + expect(transposeTable.scenegraph.getColGroup(1).parent).toBe(transposeTable.scenegraph.rowHeaderGroup); + expect(transposeTable.scenegraph.getColGroup(2).parent).toBe(transposeTable.scenegraph.bodyGroup); + + transposeTable.stateManager.clearFrozenObserver(); + transposeTable.release(); + }); +}); diff --git a/packages/vtable/examples/frozen/issue-4111-transpose-frozen-columns.ts b/packages/vtable/examples/frozen/issue-4111-transpose-frozen-columns.ts new file mode 100644 index 0000000000..211b32f20d --- /dev/null +++ b/packages/vtable/examples/frozen/issue-4111-transpose-frozen-columns.ts @@ -0,0 +1,34 @@ +import * as VTable from '../../src'; + +const CONTAINER_ID = 'vTable'; + +function generateRecords(count: number) { + return Array.from({ length: count }, (_, index) => ({ + name: `Person ${index + 1}`, + age: 20 + (index % 30), + gender: index % 2 === 0 ? 'Male' : 'Female', + hobby: ['Basketball', 'Reading', 'Travel', 'Music'][index % 4] + })); +} + +export function createTable() { + const container = document.getElementById(CONTAINER_ID); + if (!container) { + return; + } + + const tableInstance = new VTable.ListTable(container, { + columns: [ + { field: 'name', title: 'Name', width: 120 }, + { field: 'age', title: 'Age', width: 120 }, + { field: 'gender', title: 'Gender', width: 120 }, + { field: 'hobby', title: 'Hobby', width: 120 } + ], + records: generateRecords(100), + defaultColWidth: 120, + frozenColCount: 2, + transpose: true + }); + + window.tableInstance = tableInstance; +} diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts index a3b679f1e5..89ad3677d0 100644 --- a/packages/vtable/examples/menu.ts +++ b/packages/vtable/examples/menu.ts @@ -1227,6 +1227,10 @@ export const menus = [ { menu: 'frozen', children: [ + { + path: 'frozen', + name: 'issue-4111-transpose-frozen-columns' + }, { path: 'frozen', name: 'list-table-bottom-frozen' From c08e5f1e3efad7ac674df223acd65266e7d74a0d Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 23 Sep 2026 14:56:20 +0800 Subject: [PATCH 2/5] fix(vtable): release frozen observer on destroy --- ...ssue-4111-transpose-frozen-columns_2026-09-23-11-00.json | 4 ++-- packages/vtable/__tests__/options/listTable-frozen.test.ts | 6 +++++- packages/vtable/src/core/BaseTable.ts | 1 + packages/vtable/src/state/state.ts | 4 ++++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/common/changes/@visactor/vtable/test-issue-4111-transpose-frozen-columns_2026-09-23-11-00.json b/common/changes/@visactor/vtable/test-issue-4111-transpose-frozen-columns_2026-09-23-11-00.json index 4cd2edcd66..d281c25426 100644 --- a/common/changes/@visactor/vtable/test-issue-4111-transpose-frozen-columns_2026-09-23-11-00.json +++ b/common/changes/@visactor/vtable/test-issue-4111-transpose-frozen-columns_2026-09-23-11-00.json @@ -2,8 +2,8 @@ "changes": [ { "packageName": "@visactor/vtable", - "comment": "test: cover zero-width mounting and add a demo for frozen columns in transpose mode (GitHub #4111)", - "type": "none" + "comment": "fix(vtable): release frozen-column observers when a table is destroyed", + "type": "patch" } ], "packageName": "@visactor/vtable", diff --git a/packages/vtable/__tests__/options/listTable-frozen.test.ts b/packages/vtable/__tests__/options/listTable-frozen.test.ts index 97a61fb4e3..26730121cf 100644 --- a/packages/vtable/__tests__/options/listTable-frozen.test.ts +++ b/packages/vtable/__tests__/options/listTable-frozen.test.ts @@ -128,7 +128,11 @@ describe('listTable frozen columns in transpose mode', () => { expect(transposeTable.scenegraph.getColGroup(1).parent).toBe(transposeTable.scenegraph.rowHeaderGroup); expect(transposeTable.scenegraph.getColGroup(2).parent).toBe(transposeTable.scenegraph.bodyGroup); - transposeTable.stateManager.clearFrozenObserver(); + const observer = transposeTable.stateManager._frozenObserver; + const disconnect = jest.spyOn(observer, 'disconnect'); transposeTable.release(); + + expect(disconnect).toHaveBeenCalledTimes(1); + expect(transposeTable.stateManager._frozenObserver).toBeNull(); }); }); diff --git a/packages/vtable/src/core/BaseTable.ts b/packages/vtable/src/core/BaseTable.ts index c43a2b34cc..8f880d7481 100644 --- a/packages/vtable/src/core/BaseTable.ts +++ b/packages/vtable/src/core/BaseTable.ts @@ -2734,6 +2734,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { internalProps.handler?.release?.(); // internalProps.scrollable?.release?.(); this.eventManager.release(); + this.stateManager.release(); internalProps.focusControl?.release?.(); internalProps.legends?.forEach(legend => { legend?.release(); diff --git a/packages/vtable/src/state/state.ts b/packages/vtable/src/state/state.ts index f5f9060f8f..826ea4792d 100644 --- a/packages/vtable/src/state/state.ts +++ b/packages/vtable/src/state/state.ts @@ -1105,6 +1105,10 @@ export class StateManager { } } + release() { + this.clearFrozenObserver(); + } + setFrozenCol(col: number) { if (col !== this.frozen.col) { // const oldFrozenCol = this.frozen.col; From e136fe4ba64db0ad35afc2cb74d65e295b34bba0 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 23 Sep 2026 19:10:31 +0800 Subject: [PATCH 3/5] test(vtable): cover frozen observer recovery --- .../options/listTable-frozen.test.ts | 150 ++++++++++++++---- .../issue-4111-transpose-frozen-columns.ts | 5 + packages/vtable/src/state/state.ts | 13 +- 3 files changed, 136 insertions(+), 32 deletions(-) diff --git a/packages/vtable/__tests__/options/listTable-frozen.test.ts b/packages/vtable/__tests__/options/listTable-frozen.test.ts index 26730121cf..b692f748ba 100644 --- a/packages/vtable/__tests__/options/listTable-frozen.test.ts +++ b/packages/vtable/__tests__/options/listTable-frozen.test.ts @@ -94,45 +94,135 @@ describe('listTable-frozen init test', () => { }); describe('listTable frozen columns in transpose mode', () => { - test('keeps configured columns frozen after a zero-width mount and scrolling', () => { + function mockResizeObserver() { + const originalResizeObserver = global.ResizeObserver; + const observer = { + observe: jest.fn(), + disconnect: jest.fn() + }; + let callback: ResizeObserverCallback; + + global.ResizeObserver = jest.fn(function (resizeObserverCallback: ResizeObserverCallback) { + callback = resizeObserverCallback; + return observer; + }); + + return { + callback: () => callback, + observer, + restore: () => { + global.ResizeObserver = originalResizeObserver; + } + }; + } + + function mockTimers() { + const originalSetTimeout = global.setTimeout; + const originalClearTimeout = global.clearTimeout; + const timers = new Map void>(); + let timerId = 0; + + global.setTimeout = jest.fn((callback: () => void) => { + timerId += 1; + timers.set(timerId, callback); + return timerId as unknown as ReturnType; + }); + global.clearTimeout = jest.fn((id: number) => { + timers.delete(id); + }); + + return { + runAll: () => { + for (const [id, callback] of timers) { + timers.delete(id); + callback(); + } + }, + restore: () => { + global.setTimeout = originalSetTimeout; + global.clearTimeout = originalClearTimeout; + } + }; + } + + function createZeroWidthTransposeTable() { const containerDom: HTMLElement = createDiv(); containerDom.style.position = 'relative'; containerDom.style.width = '0px'; containerDom.style.height = '300px'; - const transposeTable = new ListTable(containerDom, { - columns: [ - { field: 'name', title: 'Name', width: 120 }, - { field: 'value', title: 'Value', width: 120 } - ], - records: Array.from({ length: 10 }, (_, index) => ({ - name: `name-${index}`, - value: `value-${index}` - })), - defaultColWidth: 120, - frozenColCount: 2, - transpose: true - }); + return { + containerDom, + transposeTable: new ListTable(containerDom, { + columns: [ + { field: 'name', title: 'Name', width: 120 }, + { field: 'value', title: 'Value', width: 120 } + ], + records: Array.from({ length: 10 }, (_, index) => ({ + name: `name-${index}`, + value: `value-${index}` + })), + defaultColWidth: 120, + frozenColCount: 2, + transpose: true + }) + }; + } + + test('keeps configured columns frozen after a zero-width mount and scrolling', () => { + const resizeObserver = mockResizeObserver(); + const { containerDom, transposeTable } = createZeroWidthTransposeTable(); + const timers = mockTimers(); - expect(transposeTable.frozenColCount).toBe(2); - expect(transposeTable.scenegraph.getColGroup(0).parent).toBe(transposeTable.scenegraph.rowHeaderGroup); - expect(transposeTable.scenegraph.getColGroup(1).parent).toBe(transposeTable.scenegraph.rowHeaderGroup); - expect(transposeTable.scenegraph.getColGroup(2).parent).toBe(transposeTable.scenegraph.bodyGroup); + try { + expect(resizeObserver.observer.observe).toHaveBeenCalledWith(containerDom); + const resize = jest.spyOn(transposeTable, 'resize'); + containerDom.style.width = '400px'; + resizeObserver.callback()([{ contentRect: { width: 400 } } as ResizeObserverEntry], resizeObserver.observer); - containerDom.style.width = '400px'; - transposeTable.resize(); - transposeTable.setScrollLeft(240); + expect(resize).toHaveBeenCalledTimes(1); + timers.runAll(); - expect(transposeTable.scrollLeft).toBeGreaterThan(0); - expect(transposeTable.scenegraph.getColGroup(0).parent).toBe(transposeTable.scenegraph.rowHeaderGroup); - expect(transposeTable.scenegraph.getColGroup(1).parent).toBe(transposeTable.scenegraph.rowHeaderGroup); - expect(transposeTable.scenegraph.getColGroup(2).parent).toBe(transposeTable.scenegraph.bodyGroup); + expect(transposeTable.frozenColCount).toBe(2); + expect(transposeTable.scenegraph.getColGroup(0).parent).toBe(transposeTable.scenegraph.rowHeaderGroup); + expect(transposeTable.scenegraph.getColGroup(1).parent).toBe(transposeTable.scenegraph.rowHeaderGroup); + expect(transposeTable.scenegraph.getColGroup(2).parent).toBe(transposeTable.scenegraph.bodyGroup); - const observer = transposeTable.stateManager._frozenObserver; - const disconnect = jest.spyOn(observer, 'disconnect'); - transposeTable.release(); + transposeTable.setScrollLeft(240); + + expect(transposeTable.scrollLeft).toBeGreaterThan(0); + expect(transposeTable.scenegraph.getColGroup(0).parent).toBe(transposeTable.scenegraph.rowHeaderGroup); + expect(transposeTable.scenegraph.getColGroup(1).parent).toBe(transposeTable.scenegraph.rowHeaderGroup); + expect(transposeTable.scenegraph.getColGroup(2).parent).toBe(transposeTable.scenegraph.bodyGroup); + expect(resizeObserver.observer.disconnect).toHaveBeenCalledTimes(1); + } finally { + transposeTable.release(); + resizeObserver.restore(); + timers.restore(); + } + }); - expect(disconnect).toHaveBeenCalledTimes(1); - expect(transposeTable.stateManager._frozenObserver).toBeNull(); + test('cancels the queued frozen-column check when released', () => { + const resizeObserver = mockResizeObserver(); + const { containerDom, transposeTable } = createZeroWidthTransposeTable(); + const timers = mockTimers(); + + try { + const checkFrozen = jest.spyOn(transposeTable.stateManager, 'checkFrozen'); + containerDom.style.width = '400px'; + resizeObserver.callback()([{ contentRect: { width: 400 } } as ResizeObserverEntry], resizeObserver.observer); + + const checksBeforeRelease = checkFrozen.mock.calls.length; + transposeTable.release(); + timers.runAll(); + + expect(checkFrozen).toHaveBeenCalledTimes(checksBeforeRelease); + } finally { + if (!transposeTable.isReleased) { + transposeTable.release(); + } + resizeObserver.restore(); + timers.restore(); + } }); }); diff --git a/packages/vtable/examples/frozen/issue-4111-transpose-frozen-columns.ts b/packages/vtable/examples/frozen/issue-4111-transpose-frozen-columns.ts index 211b32f20d..8ab920e069 100644 --- a/packages/vtable/examples/frozen/issue-4111-transpose-frozen-columns.ts +++ b/packages/vtable/examples/frozen/issue-4111-transpose-frozen-columns.ts @@ -17,6 +17,7 @@ export function createTable() { return; } + container.style.width = '0px'; const tableInstance = new VTable.ListTable(container, { columns: [ { field: 'name', title: 'Name', width: 120 }, @@ -31,4 +32,8 @@ export function createTable() { }); window.tableInstance = tableInstance; + + requestAnimationFrame(() => { + container.style.width = ''; + }); } diff --git a/packages/vtable/src/state/state.ts b/packages/vtable/src/state/state.ts index 826ea4792d..b6af718f19 100644 --- a/packages/vtable/src/state/state.ts +++ b/packages/vtable/src/state/state.ts @@ -205,6 +205,7 @@ export class StateManager { _clearVerticalScrollBar: any; _clearHorizontalScrollBar: any; _frozenObserver?: ResizeObserver; + _frozenObserverCheckTimer?: ReturnType; fastScrolling: boolean = false; @@ -983,6 +984,9 @@ export class StateManager { // 使用 ResizeObserver 监听容器尺寸变化 this._frozenObserver = new ResizeObserver(entries => { + if (this.table.isReleased) { + return; + } for (const entry of entries) { // 检查容器宽度是否变为可见 if (entry.contentRect.width > 0) { @@ -991,8 +995,11 @@ export class StateManager { this.table.resize(); - setTimeout(() => { - this.checkFrozen(); + this._frozenObserverCheckTimer = setTimeout(() => { + this._frozenObserverCheckTimer = undefined; + if (!this.table.isReleased) { + this.checkFrozen(); + } }, 0); return; } @@ -1107,6 +1114,8 @@ export class StateManager { release() { this.clearFrozenObserver(); + clearTimeout(this._frozenObserverCheckTimer); + this._frozenObserverCheckTimer = undefined; } setFrozenCol(col: number) { From d348b45c4b622882c12c9fe42237f306374b2f5b Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 24 Sep 2026 11:12:42 +0800 Subject: [PATCH 4/5] test(vtable): add transpose frozen observer case --- .../issue-4111-transpose-frozen-observer.ts | 114 ++++++++++++++++++ packages/vtable/examples/menu.ts | 4 + 2 files changed, 118 insertions(+) create mode 100644 packages/vtable/examples/debug/issue-4111-transpose-frozen-observer.ts diff --git a/packages/vtable/examples/debug/issue-4111-transpose-frozen-observer.ts b/packages/vtable/examples/debug/issue-4111-transpose-frozen-observer.ts new file mode 100644 index 0000000000..d6d6b3146e --- /dev/null +++ b/packages/vtable/examples/debug/issue-4111-transpose-frozen-observer.ts @@ -0,0 +1,114 @@ +import * as VTable from '../../src'; + +interface Issue4111Window extends Window { + BUGSERVER_SCREENSHOT?: () => void; + __issue_4111_error__?: ErrorEvent | PromiseRejectionEvent; + __issue_4111_table__?: VTable.ListTable; + __issue_4111_race_table__?: VTable.ListTable; +} + +const CONTAINER_ID = 'vTable'; +const STATUS_ID = 'issue4111Status'; + +const columns = [ + { field: 'name', title: 'Name', width: 120 }, + { field: 'value', title: 'Value', width: 120 } +]; +const records = Array.from({ length: 10 }, (_, index) => ({ + name: `name-${index}`, + value: `value-${index}` +})); + +function createTable(container: HTMLElement) { + return new VTable.ListTable(container, { + columns, + records, + defaultColWidth: 120, + frozenColCount: 2, + transpose: true + }); +} + +export function createTable() { + const container = document.getElementById(CONTAINER_ID); + if (!container) { + throw new Error(`Missing #${CONTAINER_ID} container`); + } + + const issueWindow = window as Issue4111Window; + delete issueWindow.__issue_4111_error__; + + const onError = (event: ErrorEvent | PromiseRejectionEvent) => { + issueWindow.__issue_4111_error__ = event; + }; + window.addEventListener('error', onError); + window.addEventListener('unhandledrejection', onError); + + document.getElementById(STATUS_ID)?.remove(); + const status = document.createElement('div'); + status.id = STATUS_ID; + status.style.cssText = 'margin: 0 0 12px; font: 600 14px/1.5 sans-serif;'; + status.textContent = 'RUNNING'; + container.parentElement?.insertBefore(status, container); + + container.style.width = '0px'; + container.style.height = '320px'; + const tableInstance = createTable(container); + issueWindow.__issue_4111_table__ = tableInstance; + + const raceContainer = document.createElement('div'); + raceContainer.style.cssText = 'position: absolute; width: 0; height: 1px; overflow: hidden;'; + container.parentElement?.appendChild(raceContainer); + const raceTable = createTable(raceContainer); + issueWindow.__issue_4111_race_table__ = raceTable; + + const release = tableInstance.release.bind(tableInstance); + tableInstance.release = () => { + window.removeEventListener('error', onError); + window.removeEventListener('unhandledrejection', onError); + if (!raceTable.isReleased) { + raceTable.release(); + } + raceContainer.remove(); + delete issueWindow.__issue_4111_error__; + delete issueWindow.__issue_4111_table__; + delete issueWindow.__issue_4111_race_table__; + document.getElementById(STATUS_ID)?.remove(); + release(); + }; + + requestAnimationFrame(() => { + container.style.width = '640px'; + raceContainer.style.width = '640px'; + + requestAnimationFrame(() => { + const raceObserverRecovered = !raceTable.stateManager._frozenObserver; + raceTable.release(); + + requestAnimationFrame(() => { + tableInstance.setScrollLeft(240); + const frozenColumnsRemainFixed = + tableInstance.frozenColCount === 2 && + tableInstance.scenegraph.getColGroup(0).parent === tableInstance.scenegraph.rowHeaderGroup && + tableInstance.scenegraph.getColGroup(1).parent === tableInstance.scenegraph.rowHeaderGroup && + tableInstance.scrollLeft > 0; + const visibleObserverRecovered = !tableInstance.stateManager._frozenObserver; + const pass = + visibleObserverRecovered && + raceObserverRecovered && + frozenColumnsRemainFixed && + !issueWindow.__issue_4111_error__; + + const observerRecovery = visibleObserverRecovered && raceObserverRecovered ? 'yes' : 'no'; + status.textContent = + `${pass ? 'PASS' : 'FAIL'} | frozen=${tableInstance.frozenColCount} | ` + + `scrollLeft=${tableInstance.scrollLeft} | observer recovery=${observerRecovery} | ` + + 'released observer has no async error'; + status.style.color = pass ? '#237804' : '#a8071a'; + status.style.borderLeft = `4px solid ${pass ? '#52c41a' : '#ff4d4f'}`; + status.style.paddingLeft = '8px'; + issueWindow.BUGSERVER_SCREENSHOT?.(); + }); + }); + }); +} diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts index 89ad3677d0..a6ad9e99ac 100644 --- a/packages/vtable/examples/menu.ts +++ b/packages/vtable/examples/menu.ts @@ -94,6 +94,10 @@ export const menus = [ path: 'debug', name: 'issue-4186-nested-field-edit' }, + { + path: 'debug', + name: 'issue-4111-transpose-frozen-observer' + }, { path: 'debug', name: 'issue-5036-master-detail-search' From 29e32d6eb374a5272ced4c607a3f327bc0cd2c1b Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 24 Sep 2026 13:03:01 +0800 Subject: [PATCH 5/5] test(vtable): make frozen observer race reproducible --- .../issue-4111-transpose-frozen-observer.ts | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/vtable/examples/debug/issue-4111-transpose-frozen-observer.ts b/packages/vtable/examples/debug/issue-4111-transpose-frozen-observer.ts index d6d6b3146e..214b6ea52f 100644 --- a/packages/vtable/examples/debug/issue-4111-transpose-frozen-observer.ts +++ b/packages/vtable/examples/debug/issue-4111-transpose-frozen-observer.ts @@ -62,10 +62,40 @@ export function createTable() { const raceTable = createTable(raceContainer); issueWindow.__issue_4111_race_table__ = raceTable; + const originalSetTimeout = window.setTimeout; + const originalClearTimeout = window.clearTimeout; + const frozenCheckTimers = new Map, () => void>(); + let captureFrozenCheck = false; + let timerId = 0; + let checkFrozenRanAfterRelease = false; + const restoreTimers = () => { + window.setTimeout = originalSetTimeout; + window.clearTimeout = originalClearTimeout; + }; + window.setTimeout = ((callback: TimerHandler, timeout?: number) => { + if (captureFrozenCheck && timeout === 0) { + const id = (timerId += 1) as unknown as ReturnType; + frozenCheckTimers.set(id, () => { + if (typeof callback === 'function') { + callback(); + } + }); + return id; + } + return originalSetTimeout(callback, timeout); + }) as typeof window.setTimeout; + window.clearTimeout = ((id?: ReturnType) => { + if (id && frozenCheckTimers.delete(id)) { + return; + } + originalClearTimeout(id); + }) as typeof window.clearTimeout; + const release = tableInstance.release.bind(tableInstance); tableInstance.release = () => { window.removeEventListener('error', onError); window.removeEventListener('unhandledrejection', onError); + restoreTimers(); if (!raceTable.isReleased) { raceTable.release(); } @@ -79,11 +109,19 @@ export function createTable() { requestAnimationFrame(() => { container.style.width = '640px'; + captureFrozenCheck = true; raceContainer.style.width = '640px'; requestAnimationFrame(() => { const raceObserverRecovered = !raceTable.stateManager._frozenObserver; + raceTable.stateManager.checkFrozen = () => { + checkFrozenRanAfterRelease = true; + }; + const queuedFrozenCheckBeforeRelease = frozenCheckTimers.size; raceTable.release(); + const remainingTimerCallbacks = Array.from(frozenCheckTimers.values()); + restoreTimers(); + remainingTimerCallbacks.forEach(callback => callback()); requestAnimationFrame(() => { tableInstance.setScrollLeft(240); @@ -96,6 +134,8 @@ export function createTable() { const pass = visibleObserverRecovered && raceObserverRecovered && + queuedFrozenCheckBeforeRelease > 0 && + !checkFrozenRanAfterRelease && frozenColumnsRemainFixed && !issueWindow.__issue_4111_error__; @@ -103,7 +143,7 @@ export function createTable() { status.textContent = `${pass ? 'PASS' : 'FAIL'} | frozen=${tableInstance.frozenColCount} | ` + `scrollLeft=${tableInstance.scrollLeft} | observer recovery=${observerRecovery} | ` + - 'released observer has no async error'; + `queued check cancelled=${queuedFrozenCheckBeforeRelease > 0 && !checkFrozenRanAfterRelease ? 'yes' : 'no'}`; status.style.color = pass ? '#237804' : '#a8071a'; status.style.borderLeft = `4px solid ${pass ? '#52c41a' : '#ff4d4f'}`; status.style.paddingLeft = '8px';