From 67560506839fb91d2bd5194ff08a29ba6f8a06eb Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Mon, 21 Sep 2026 19:39:36 +0800 Subject: [PATCH 01/10] test(react-vtable): cover detached layout controls Add regression coverage and a demo for Link and Button graphics before stage attachment. Co-Authored-By: Claude Sonnet 4.6 --- ...36-link-button-stage_2026-09-21-16-30.json | 11 ++ .../custom-layout-components.test.tsx | 31 ++++++ packages/react-vtable/demo/src/App.tsx | 2 + .../issue-4836-link-button-stage.tsx | 102 ++++++++++++++++++ 4 files changed, 146 insertions(+) create mode 100644 common/changes/@visactor/react-vtable/test-issue-4836-link-button-stage_2026-09-21-16-30.json create mode 100644 packages/react-vtable/__tests__/custom-layout-components.test.tsx create mode 100644 packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx diff --git a/common/changes/@visactor/react-vtable/test-issue-4836-link-button-stage_2026-09-21-16-30.json b/common/changes/@visactor/react-vtable/test-issue-4836-link-button-stage_2026-09-21-16-30.json new file mode 100644 index 000000000..0ee1950a4 --- /dev/null +++ b/common/changes/@visactor/react-vtable/test-issue-4836-link-button-stage_2026-09-21-16-30.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@visactor/react-vtable", + "comment": "test: add regression coverage and a reproduction demo for custom-layout Link and Button mounting before stage attachment (GitHub #4836)", + "type": "none" + } + ], + "packageName": "@visactor/react-vtable", + "email": "892739385@qq.com" +} diff --git a/packages/react-vtable/__tests__/custom-layout-components.test.tsx b/packages/react-vtable/__tests__/custom-layout-components.test.tsx new file mode 100644 index 000000000..221ea068c --- /dev/null +++ b/packages/react-vtable/__tests__/custom-layout-components.test.tsx @@ -0,0 +1,31 @@ +/* eslint-env jest */ +import React from 'react'; +import { Group } from '@visactor/vtable/es/vrender'; +import { Button, Link } from '../src/components'; +import { createReconcilerContainer, reconcilor } from '../src/table-components/custom/reconciler'; + +type TestReconciler = typeof reconcilor & { + flushSyncWork?: () => unknown; + flushPassiveEffects?: () => unknown; +}; + +describe('custom layout components', () => { + test.each([ + ['Link', React.createElement(Link, null, 'View')], + ['Button', React.createElement(Button, null, 'View')] + ])('%s can mount before its graphic is attached to a stage', (_, component) => { + const detachedGroup = new Group({}); + const container = createReconcilerContainer(detachedGroup); + const testReconciler = reconcilor as TestReconciler; + + expect(() => { + testReconciler.updateContainer(component, container, null); + testReconciler.flushSyncWork?.(); + testReconciler.flushPassiveEffects?.(); + }).not.toThrow(); + + testReconciler.updateContainer(null, container, null); + testReconciler.flushSyncWork?.(); + testReconciler.flushPassiveEffects?.(); + }); +}); diff --git a/packages/react-vtable/demo/src/App.tsx b/packages/react-vtable/demo/src/App.tsx index d4c9a4369..4e250152b 100644 --- a/packages/react-vtable/demo/src/App.tsx +++ b/packages/react-vtable/demo/src/App.tsx @@ -1,5 +1,6 @@ import listTable from './list-table/list-table'; import issue5203ViteReact19 from './list-table/issue-5203-vite-react19'; +import issue4836LinkButtonStage from './list-table/issue-4836-link-button-stage'; import listOptionRecord from './list-table/list-option-records'; import listComponent from './list-table/list-component'; import listCustomLayout from './list-table/list-custom-layout'; @@ -33,6 +34,7 @@ import { Component, useEffect, useMemo, useState } from 'react'; declare const globalThis: any; const demoList = [ + { key: 'issue4836LinkButtonStage', Comp: issue4836LinkButtonStage }, { key: 'issue5203ViteReact19', Comp: issue5203ViteReact19 }, { key: 'listTable', Comp: listTable }, { key: 'listEditor', Comp: listEditor }, diff --git a/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx b/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx new file mode 100644 index 000000000..16d4b37f5 --- /dev/null +++ b/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx @@ -0,0 +1,102 @@ +/* global window */ +import type { CustomLayoutFunctionArg } from '../../../src'; +import { Button, Group, Link, ListColumn, ListTable } from '../../../src'; + +declare global { + interface Window { + __issue_4836_ready__?: boolean; + } +} + +type ActionCellProps = CustomLayoutFunctionArg & { + kind: 'link' | 'button'; +}; + +const records = Array.from({ length: 100 }, (_, index) => ({ + id: index + 1, + name: `Record ${index + 1}` +})); + +function ActionCell(props: ActionCellProps) { + const { table, row, col, rect, kind } = props; + if (!table || row === undefined || col === undefined) { + return null; + } + + const { width, height } = rect || table.getCellRect(col, row); + const content = + kind === 'link' ? ( + + View + + ) : ( + + ); + + return ( + + + + {content} + + + + ); +} + +function App() { + return ( + { + window.__issue_4836_ready__ = true; + }} + > + + + {Array.from({ length: 6 }, (_, index) => { + const kind = index % 2 === 0 ? 'link' : 'button'; + return ( + + + + ); + })} + + ); +} + +export default App; From 9774b12e2cddc9560e732d0a6643b8755522221c Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 22 Sep 2026 09:50:55 +0800 Subject: [PATCH 02/10] test(bugserver): expose detached React layout coverage Run the shipped Link and Button renderer path in Bugserver. This keeps Issue #4836 covered in the browser bundle. Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- common/config/rush/pnpm-lock.yaml | 12 ++++++++++ tools/bugserver-trigger/bundler.config.js | 4 ++++ tools/bugserver-trigger/package.json | 6 ++++- tools/bugserver-trigger/src/index.ts | 28 +++++++++++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index a968e6123..6801f4e20 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -1849,6 +1849,9 @@ importers: ../../tools/bugserver-trigger: dependencies: + '@visactor/react-vtable': + specifier: workspace:1.26.8 + version: link:../../packages/react-vtable '@visactor/vtable': specifier: workspace:1.26.8 version: link:../../packages/vtable @@ -1864,6 +1867,12 @@ importers: '@visactor/vtable-sheet': specifier: workspace:1.26.8 version: link:../../packages/vtable-sheet + react: + specifier: 18.2.0 + version: 18.2.0 + tslib: + specifier: 2.3.1 + version: 2.3.1 devDependencies: '@internal/bundler': specifier: workspace:* @@ -1883,6 +1892,9 @@ importers: '@types/node-fetch': specifier: 2.6.4 version: 2.6.4 + '@types/react': + specifier: 18.2.79 + version: 18.2.79 cross-env: specifier: ^7.0.3 version: 7.0.3 diff --git a/tools/bugserver-trigger/bundler.config.js b/tools/bugserver-trigger/bundler.config.js index 0cdec59ee..a982630a0 100644 --- a/tools/bugserver-trigger/bundler.config.js +++ b/tools/bugserver-trigger/bundler.config.js @@ -12,6 +12,10 @@ module.exports = { umdOutputFilename: 'index', minify: false, + envs: { + __DEV__: JSON.stringify(false), + 'process.env.NODE_ENV': JSON.stringify('production') + }, output: { footer: '/* follow me on Twitter! @rich_harris */' } diff --git a/tools/bugserver-trigger/package.json b/tools/bugserver-trigger/package.json index 97dabfa9a..47b370562 100644 --- a/tools/bugserver-trigger/package.json +++ b/tools/bugserver-trigger/package.json @@ -8,13 +8,17 @@ "ci": "ts-node --transpileOnly --skipProject ./scripts/trigger-test.ts" }, "dependencies": { + "@visactor/react-vtable": "workspace:1.26.8", "@visactor/vtable": "workspace:1.26.8", "@visactor/vtable-gantt": "workspace:1.26.8", "@visactor/vtable-editors": "workspace:1.26.8", "@visactor/vtable-plugins": "workspace:1.26.8", - "@visactor/vtable-sheet": "workspace:1.26.8" + "@visactor/vtable-sheet": "workspace:1.26.8", + "react": "18.2.0", + "tslib": "2.3.1" }, "devDependencies": { + "@types/react": "18.2.79", "@rushstack/eslint-patch": "~1.1.4", "@internal/bundler": "workspace:*", "@internal/eslint-config": "workspace:*", diff --git a/tools/bugserver-trigger/src/index.ts b/tools/bugserver-trigger/src/index.ts index 02c228914..23fca22c3 100644 --- a/tools/bugserver-trigger/src/index.ts +++ b/tools/bugserver-trigger/src/index.ts @@ -1,5 +1,12 @@ import * as VTable from '@visactor/vtable'; import * as VRender from '@visactor/vtable/es/vrender'; +import * as React from 'react'; +import { Button } from '../../../packages/react-vtable/es/components/button/button'; +import { Link } from '../../../packages/react-vtable/es/components/link/link'; +import { + createReconcilerContainer, + reconcilor +} from '../../../packages/react-vtable/es/table-components/custom/reconciler'; import * as VTableEditors from '@visactor/vtable-editors'; import * as VTableGantt from '@visactor/vtable-gantt'; import { @@ -61,8 +68,29 @@ const VTablePlugins = { window.VTablePlugins = VTablePlugins; // @ts-ignore window.VRender = VRender; +// @ts-ignore +window.ReactVTableTest = { + runDetachedCustomLayoutComponents() { + const testReconciler = reconcilor as typeof reconcilor & { + flushSyncWork?: () => unknown; + flushPassiveEffects?: () => unknown; + }; + + [React.createElement(Link, null, 'View'), React.createElement(Button, null, 'Open')].forEach(component => { + const detachedGroup = new VRender.Group({}); + const container = createReconcilerContainer(detachedGroup); + testReconciler.updateContainer(component, container, null); + testReconciler.flushSyncWork?.(); + testReconciler.flushPassiveEffects?.(); + testReconciler.updateContainer(null, container, null); + testReconciler.flushSyncWork?.(); + testReconciler.flushPassiveEffects?.(); + }); + } +}; export default { + React, VTable, VTableEditors, VTableGantt, From fb2b4f78ef7b07f66a281b4b4e36dc43a035787e Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 22 Sep 2026 10:10:19 +0800 Subject: [PATCH 03/10] test(react-vtable): make issue 4836 readiness reliable Wait for staged Link and Button graphics before reporting demo success. Expose the Bugserver case linkage for review traceability. Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- .../issue-4836-link-button-stage.tsx | 77 ++++++++++++++++++- tools/bugserver-trigger/src/index.ts | 1 + 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx b/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx index 16d4b37f5..16334761e 100644 --- a/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx +++ b/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx @@ -1,10 +1,13 @@ /* global window */ +import type { Tag } from '@visactor/vtable/es/vrender'; +import { useCallback, useLayoutEffect } from 'react'; import type { CustomLayoutFunctionArg } from '../../../src'; import { Button, Group, Link, ListColumn, ListTable } from '../../../src'; declare global { interface Window { __issue_4836_ready__?: boolean; + __issue_4836_error__?: string; } } @@ -12,13 +15,68 @@ type ActionCellProps = CustomLayoutFunctionArg & { kind: 'link' | 'button'; }; +const stagedControls = new Map(); +const observedKinds = new Set(); +let readinessRun = 0; + const records = Array.from({ length: 100 }, (_, index) => ({ id: index + 1, name: `Record ${index + 1}` })); +function observeStageAttachment(kind: ActionCellProps['kind'], control: Tag) { + if (observedKinds.has(kind)) { + return; + } + observedKinds.add(kind); + const run = readinessRun; + let remainingFrames = 120; + + const checkStage = () => { + if (run !== readinessRun || window.__issue_4836_error__) { + return; + } + if (!control.stage) { + remainingFrames -= 1; + if (remainingFrames === 0) { + window.__issue_4836_error__ = `${kind} control was not attached to a stage`; + return; + } + window.requestAnimationFrame(checkStage); + return; + } + + stagedControls.set(kind, control); + if (stagedControls.size !== 2) { + return; + } + + stagedControls.forEach(item => item.stage?.renderNextFrame?.()); + window.requestAnimationFrame(() => { + if ( + run === readinessRun && + !window.__issue_4836_error__ && + Array.from(stagedControls.values()).every(item => item.stage) + ) { + window.__issue_4836_ready__ = true; + } + }); + }; + + window.requestAnimationFrame(checkStage); +} + function ActionCell(props: ActionCellProps) { const { table, row, col, rect, kind } = props; + const handleControlRef = useCallback( + (control: Tag | null) => { + if (control) { + observeStageAttachment(kind, control); + } + }, + [kind] + ); + if (!table || row === undefined || col === undefined) { return null; } @@ -26,11 +84,13 @@ function ActionCell(props: ActionCellProps) { const { width, height } = rect || table.getCellRect(col, row); const content = kind === 'link' ? ( - + View ) : ( - + ); return ( @@ -76,13 +136,22 @@ function ActionCell(props: ActionCellProps) { } function App() { + useLayoutEffect(() => { + readinessRun += 1; + stagedControls.clear(); + observedKinds.clear(); + window.__issue_4836_ready__ = false; + delete window.__issue_4836_error__; + }, []); + return ( { - window.__issue_4836_ready__ = true; + onError={error => { + window.__issue_4836_ready__ = false; + window.__issue_4836_error__ = error instanceof Error ? error.message : String(error); }} > diff --git a/tools/bugserver-trigger/src/index.ts b/tools/bugserver-trigger/src/index.ts index 23fca22c3..ecec5fb32 100644 --- a/tools/bugserver-trigger/src/index.ts +++ b/tools/bugserver-trigger/src/index.ts @@ -68,6 +68,7 @@ const VTablePlugins = { window.VTablePlugins = VTablePlugins; // @ts-ignore window.VRender = VRender; +// Invoked by Bugserver photo case 6ab1ded722ae4f0047df39bd before its required screenshot. // @ts-ignore window.ReactVTableTest = { runDetachedCustomLayoutComponents() { From 305cf7374d15dedd09ac82350dd6f62ce2d8135f Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 22 Sep 2026 10:14:11 +0800 Subject: [PATCH 04/10] test(react-vtable): verify detached controls render Assert Link and Button graphics survive attachment to a real VRender stage. Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- .../custom-layout-components.test.tsx | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/react-vtable/__tests__/custom-layout-components.test.tsx b/packages/react-vtable/__tests__/custom-layout-components.test.tsx index 221ea068c..64091d47d 100644 --- a/packages/react-vtable/__tests__/custom-layout-components.test.tsx +++ b/packages/react-vtable/__tests__/custom-layout-components.test.tsx @@ -1,6 +1,8 @@ /* eslint-env jest */ +/* global document */ import React from 'react'; import { Group } from '@visactor/vtable/es/vrender'; +import { createStageFromVRenderApp } from '@visactor/vtable/es/vrender-app'; import { Button, Link } from '../src/components'; import { createReconcilerContainer, reconcilor } from '../src/table-components/custom/reconciler'; @@ -17,15 +19,38 @@ describe('custom layout components', () => { const detachedGroup = new Group({}); const container = createReconcilerContainer(detachedGroup); const testReconciler = reconcilor as TestReconciler; + const canvas = document.createElement('canvas'); + const { stage, releaseAppRef } = createStageFromVRenderApp( + { + canvas, + width: 200, + height: 80 + }, + { mode: 'browser', scope: 'react-custom-layout-components' } + ); - expect(() => { - testReconciler.updateContainer(component, container, null); + try { + expect(() => { + testReconciler.updateContainer(component, container, null); + testReconciler.flushSyncWork?.(); + testReconciler.flushPassiveEffects?.(); + }).not.toThrow(); + + const graphic = detachedGroup.firstChild; + expect(graphic).toBeTruthy(); + expect(graphic.stage).toBeFalsy(); + + stage.defaultLayer.add(detachedGroup); + stage.render(); + + expect(detachedGroup.firstChild).toBe(graphic); + expect(graphic.stage).toBe(stage); + } finally { + testReconciler.updateContainer(null, container, null); testReconciler.flushSyncWork?.(); testReconciler.flushPassiveEffects?.(); - }).not.toThrow(); - - testReconciler.updateContainer(null, container, null); - testReconciler.flushSyncWork?.(); - testReconciler.flushPassiveEffects?.(); + stage.release(); + releaseAppRef(); + } }); }); From 61ae2b03d4425478cb1fc68b3f06dc16443e8624 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 22 Sep 2026 11:56:32 +0800 Subject: [PATCH 05/10] test(bugserver): identify detached helper bundle Expose the defining script URL so photo cases can reject stale helpers after switching versions. Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- tools/bugserver-trigger/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/bugserver-trigger/src/index.ts b/tools/bugserver-trigger/src/index.ts index ecec5fb32..8f49ec126 100644 --- a/tools/bugserver-trigger/src/index.ts +++ b/tools/bugserver-trigger/src/index.ts @@ -68,9 +68,11 @@ const VTablePlugins = { window.VTablePlugins = VTablePlugins; // @ts-ignore window.VRender = VRender; +const currentBundleUrl = (window.document.currentScript as HTMLScriptElement | null)?.src; // Invoked by Bugserver photo case 6ab1ded722ae4f0047df39bd before its required screenshot. // @ts-ignore window.ReactVTableTest = { + bundleUrl: currentBundleUrl, runDetachedCustomLayoutComponents() { const testReconciler = reconcilor as typeof reconcilor & { flushSyncWork?: () => unknown; From 7d5f5a6e7c2e2099e7c9d0d3b1f167c96c2dee31 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 22 Sep 2026 12:08:26 +0800 Subject: [PATCH 06/10] test(bugserver): verify detached controls on stage Exercise the production helper through a real render lifecycle. Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- .../issue-4836-link-button-stage.tsx | 28 ++++++++++-- tools/bugserver-trigger/src/index.ts | 43 ++++++++++++++++--- 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx b/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx index 16334761e..2c328c2b6 100644 --- a/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx +++ b/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx @@ -17,6 +17,7 @@ type ActionCellProps = CustomLayoutFunctionArg & { const stagedControls = new Map(); const observedKinds = new Set(); +const scheduledAnimationFrames = new Set(); let readinessRun = 0; const records = Array.from({ length: 100 }, (_, index) => ({ @@ -24,6 +25,19 @@ const records = Array.from({ length: 100 }, (_, index) => ({ name: `Record ${index + 1}` })); +function scheduleAnimationFrame(callback: () => void) { + const frameId = window.requestAnimationFrame(() => { + scheduledAnimationFrames.delete(frameId); + callback(); + }); + scheduledAnimationFrames.add(frameId); +} + +function cancelScheduledAnimationFrames() { + scheduledAnimationFrames.forEach(frameId => window.cancelAnimationFrame(frameId)); + scheduledAnimationFrames.clear(); +} + function observeStageAttachment(kind: ActionCellProps['kind'], control: Tag) { if (observedKinds.has(kind)) { return; @@ -42,7 +56,7 @@ function observeStageAttachment(kind: ActionCellProps['kind'], control: Tag) { window.__issue_4836_error__ = `${kind} control was not attached to a stage`; return; } - window.requestAnimationFrame(checkStage); + scheduleAnimationFrame(checkStage); return; } @@ -52,7 +66,7 @@ function observeStageAttachment(kind: ActionCellProps['kind'], control: Tag) { } stagedControls.forEach(item => item.stage?.renderNextFrame?.()); - window.requestAnimationFrame(() => { + scheduleAnimationFrame(() => { if ( run === readinessRun && !window.__issue_4836_error__ && @@ -63,7 +77,7 @@ function observeStageAttachment(kind: ActionCellProps['kind'], control: Tag) { }); }; - window.requestAnimationFrame(checkStage); + scheduleAnimationFrame(checkStage); } function ActionCell(props: ActionCellProps) { @@ -142,6 +156,14 @@ function App() { observedKinds.clear(); window.__issue_4836_ready__ = false; delete window.__issue_4836_error__; + + return () => { + readinessRun += 1; + cancelScheduledAnimationFrames(); + stagedControls.clear(); + observedKinds.clear(); + window.__issue_4836_ready__ = false; + }; }, []); return ( diff --git a/tools/bugserver-trigger/src/index.ts b/tools/bugserver-trigger/src/index.ts index 8f49ec126..fb90584e6 100644 --- a/tools/bugserver-trigger/src/index.ts +++ b/tools/bugserver-trigger/src/index.ts @@ -1,5 +1,6 @@ import * as VTable from '@visactor/vtable'; import * as VRender from '@visactor/vtable/es/vrender'; +import { createStageFromVRenderApp } from '@visactor/vtable/es/vrender-app'; import * as React from 'react'; import { Button } from '../../../packages/react-vtable/es/components/button/button'; import { Link } from '../../../packages/react-vtable/es/components/link/link'; @@ -82,12 +83,42 @@ window.ReactVTableTest = { [React.createElement(Link, null, 'View'), React.createElement(Button, null, 'Open')].forEach(component => { const detachedGroup = new VRender.Group({}); const container = createReconcilerContainer(detachedGroup); - testReconciler.updateContainer(component, container, null); - testReconciler.flushSyncWork?.(); - testReconciler.flushPassiveEffects?.(); - testReconciler.updateContainer(null, container, null); - testReconciler.flushSyncWork?.(); - testReconciler.flushPassiveEffects?.(); + const canvas = window.document.createElement('canvas'); + const { stage, releaseAppRef } = createStageFromVRenderApp( + { + canvas, + width: 200, + height: 80 + }, + { mode: 'browser', scope: 'react-custom-layout-components' } + ); + + try { + testReconciler.updateContainer(component, container, null); + testReconciler.flushSyncWork?.(); + testReconciler.flushPassiveEffects?.(); + + const graphic = detachedGroup.firstChild; + if (!graphic) { + throw new Error('Custom layout component did not create a graphic'); + } + if (graphic.stage) { + throw new Error('Custom layout component was attached to a stage before its group was mounted'); + } + + stage.defaultLayer.add(detachedGroup as unknown as Parameters[0]); + stage.render(); + + if (graphic.stage !== stage) { + throw new Error('Custom layout component was not attached to the rendered stage'); + } + } finally { + testReconciler.updateContainer(null, container, null); + testReconciler.flushSyncWork?.(); + testReconciler.flushPassiveEffects?.(); + stage.release(); + releaseAppRef(); + } }); } }; From 83c1a662813176b9291f07c2a1f0aa07745733e2 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 22 Sep 2026 17:34:37 +0800 Subject: [PATCH 07/10] test(react-vtable): remove unused bugserver coverage Keep the regression PR scoped to the reproducible unit test and demo. Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- common/config/rush/pnpm-lock.yaml | 12 ----- tools/bugserver-trigger/bundler.config.js | 4 -- tools/bugserver-trigger/package.json | 6 +-- tools/bugserver-trigger/src/index.ts | 62 ----------------------- 4 files changed, 1 insertion(+), 83 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 6801f4e20..a968e6123 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -1849,9 +1849,6 @@ importers: ../../tools/bugserver-trigger: dependencies: - '@visactor/react-vtable': - specifier: workspace:1.26.8 - version: link:../../packages/react-vtable '@visactor/vtable': specifier: workspace:1.26.8 version: link:../../packages/vtable @@ -1867,12 +1864,6 @@ importers: '@visactor/vtable-sheet': specifier: workspace:1.26.8 version: link:../../packages/vtable-sheet - react: - specifier: 18.2.0 - version: 18.2.0 - tslib: - specifier: 2.3.1 - version: 2.3.1 devDependencies: '@internal/bundler': specifier: workspace:* @@ -1892,9 +1883,6 @@ importers: '@types/node-fetch': specifier: 2.6.4 version: 2.6.4 - '@types/react': - specifier: 18.2.79 - version: 18.2.79 cross-env: specifier: ^7.0.3 version: 7.0.3 diff --git a/tools/bugserver-trigger/bundler.config.js b/tools/bugserver-trigger/bundler.config.js index a982630a0..0cdec59ee 100644 --- a/tools/bugserver-trigger/bundler.config.js +++ b/tools/bugserver-trigger/bundler.config.js @@ -12,10 +12,6 @@ module.exports = { umdOutputFilename: 'index', minify: false, - envs: { - __DEV__: JSON.stringify(false), - 'process.env.NODE_ENV': JSON.stringify('production') - }, output: { footer: '/* follow me on Twitter! @rich_harris */' } diff --git a/tools/bugserver-trigger/package.json b/tools/bugserver-trigger/package.json index 47b370562..97dabfa9a 100644 --- a/tools/bugserver-trigger/package.json +++ b/tools/bugserver-trigger/package.json @@ -8,17 +8,13 @@ "ci": "ts-node --transpileOnly --skipProject ./scripts/trigger-test.ts" }, "dependencies": { - "@visactor/react-vtable": "workspace:1.26.8", "@visactor/vtable": "workspace:1.26.8", "@visactor/vtable-gantt": "workspace:1.26.8", "@visactor/vtable-editors": "workspace:1.26.8", "@visactor/vtable-plugins": "workspace:1.26.8", - "@visactor/vtable-sheet": "workspace:1.26.8", - "react": "18.2.0", - "tslib": "2.3.1" + "@visactor/vtable-sheet": "workspace:1.26.8" }, "devDependencies": { - "@types/react": "18.2.79", "@rushstack/eslint-patch": "~1.1.4", "@internal/bundler": "workspace:*", "@internal/eslint-config": "workspace:*", diff --git a/tools/bugserver-trigger/src/index.ts b/tools/bugserver-trigger/src/index.ts index fb90584e6..02c228914 100644 --- a/tools/bugserver-trigger/src/index.ts +++ b/tools/bugserver-trigger/src/index.ts @@ -1,13 +1,5 @@ import * as VTable from '@visactor/vtable'; import * as VRender from '@visactor/vtable/es/vrender'; -import { createStageFromVRenderApp } from '@visactor/vtable/es/vrender-app'; -import * as React from 'react'; -import { Button } from '../../../packages/react-vtable/es/components/button/button'; -import { Link } from '../../../packages/react-vtable/es/components/link/link'; -import { - createReconcilerContainer, - reconcilor -} from '../../../packages/react-vtable/es/table-components/custom/reconciler'; import * as VTableEditors from '@visactor/vtable-editors'; import * as VTableGantt from '@visactor/vtable-gantt'; import { @@ -69,62 +61,8 @@ const VTablePlugins = { window.VTablePlugins = VTablePlugins; // @ts-ignore window.VRender = VRender; -const currentBundleUrl = (window.document.currentScript as HTMLScriptElement | null)?.src; -// Invoked by Bugserver photo case 6ab1ded722ae4f0047df39bd before its required screenshot. -// @ts-ignore -window.ReactVTableTest = { - bundleUrl: currentBundleUrl, - runDetachedCustomLayoutComponents() { - const testReconciler = reconcilor as typeof reconcilor & { - flushSyncWork?: () => unknown; - flushPassiveEffects?: () => unknown; - }; - - [React.createElement(Link, null, 'View'), React.createElement(Button, null, 'Open')].forEach(component => { - const detachedGroup = new VRender.Group({}); - const container = createReconcilerContainer(detachedGroup); - const canvas = window.document.createElement('canvas'); - const { stage, releaseAppRef } = createStageFromVRenderApp( - { - canvas, - width: 200, - height: 80 - }, - { mode: 'browser', scope: 'react-custom-layout-components' } - ); - - try { - testReconciler.updateContainer(component, container, null); - testReconciler.flushSyncWork?.(); - testReconciler.flushPassiveEffects?.(); - - const graphic = detachedGroup.firstChild; - if (!graphic) { - throw new Error('Custom layout component did not create a graphic'); - } - if (graphic.stage) { - throw new Error('Custom layout component was attached to a stage before its group was mounted'); - } - - stage.defaultLayer.add(detachedGroup as unknown as Parameters[0]); - stage.render(); - - if (graphic.stage !== stage) { - throw new Error('Custom layout component was not attached to the rendered stage'); - } - } finally { - testReconciler.updateContainer(null, container, null); - testReconciler.flushSyncWork?.(); - testReconciler.flushPassiveEffects?.(); - stage.release(); - releaseAppRef(); - } - }); - } -}; export default { - React, VTable, VTableEditors, VTableGantt, From 66e80edf51ab4269980b1961a79852d34cff179a Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 22 Sep 2026 18:02:52 +0800 Subject: [PATCH 08/10] test(react-vtable): exercise component ref effects Validate the intended ref lifecycle and fail incomplete readiness. Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- .../issue-4836-link-button-stage.tsx | 48 ++++++++++++------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx b/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx index 2c328c2b6..37331d441 100644 --- a/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx +++ b/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx @@ -1,6 +1,6 @@ /* global window */ import type { Tag } from '@visactor/vtable/es/vrender'; -import { useCallback, useLayoutEffect } from 'react'; +import { useEffect, useLayoutEffect, useRef } from 'react'; import type { CustomLayoutFunctionArg } from '../../../src'; import { Button, Group, Link, ListColumn, ListTable } from '../../../src'; @@ -44,18 +44,12 @@ function observeStageAttachment(kind: ActionCellProps['kind'], control: Tag) { } observedKinds.add(kind); const run = readinessRun; - let remainingFrames = 120; const checkStage = () => { if (run !== readinessRun || window.__issue_4836_error__) { return; } if (!control.stage) { - remainingFrames -= 1; - if (remainingFrames === 0) { - window.__issue_4836_error__ = `${kind} control was not attached to a stage`; - return; - } scheduleAnimationFrame(checkStage); return; } @@ -82,14 +76,13 @@ function observeStageAttachment(kind: ActionCellProps['kind'], control: Tag) { function ActionCell(props: ActionCellProps) { const { table, row, col, rect, kind } = props; - const handleControlRef = useCallback( - (control: Tag | null) => { - if (control) { - observeStageAttachment(kind, control); - } - }, - [kind] - ); + const controlRef = useRef(null); + + useEffect(() => { + if (controlRef.current) { + observeStageAttachment(kind, controlRef.current); + } + }, [kind]); if (!table || row === undefined || col === undefined) { return null; @@ -98,11 +91,11 @@ function ActionCell(props: ActionCellProps) { const { width, height } = rect || table.getCellRect(col, row); const content = kind === 'link' ? ( - + View ) : ( - ); @@ -156,6 +149,27 @@ function App() { observedKinds.clear(); window.__issue_4836_ready__ = false; delete window.__issue_4836_error__; + const run = readinessRun; + let remainingFrames = 120; + + const checkReadyDeadline = () => { + if (run !== readinessRun || window.__issue_4836_ready__ || window.__issue_4836_error__) { + return; + } + remainingFrames -= 1; + if (remainingFrames === 0) { + const missingKinds = (['link', 'button'] as const).filter(kind => !stagedControls.has(kind)); + window.__issue_4836_error__ = missingKinds.length + ? `${missingKinds.join(' and ')} control${ + missingKinds.length > 1 ? 's were' : ' was' + } not attached to a stage` + : 'Link and Button controls did not reach the ready state'; + return; + } + scheduleAnimationFrame(checkReadyDeadline); + }; + + scheduleAnimationFrame(checkReadyDeadline); return () => { readinessRun += 1; From 5562dc2523eb93a6faac696040634f3fdba26044 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 22 Sep 2026 19:12:09 +0800 Subject: [PATCH 09/10] test(react-vtable): isolate demo readiness runs Prevent stale errors from leaking across Demo lifecycle runs. Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- .../list-table/issue-4836-link-button-stage.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx b/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx index 37331d441..8754439a6 100644 --- a/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx +++ b/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx @@ -143,13 +143,18 @@ function ActionCell(props: ActionCellProps) { } function App() { + const readinessToken = useRef({ active: false, run: 0 }); + useLayoutEffect(() => { readinessRun += 1; + const token = readinessToken.current; + token.active = true; + token.run = readinessRun; stagedControls.clear(); observedKinds.clear(); window.__issue_4836_ready__ = false; delete window.__issue_4836_error__; - const run = readinessRun; + const run = token.run; let remainingFrames = 120; const checkReadyDeadline = () => { @@ -172,11 +177,16 @@ function App() { scheduleAnimationFrame(checkReadyDeadline); return () => { + token.active = false; + if (readinessRun !== token.run) { + return; + } readinessRun += 1; cancelScheduledAnimationFrames(); stagedControls.clear(); observedKinds.clear(); window.__issue_4836_ready__ = false; + delete window.__issue_4836_error__; }; }, []); @@ -186,6 +196,10 @@ function App() { height="100%" defaultRowHeight={44} onError={error => { + const token = readinessToken.current; + if (!token.active || token.run !== readinessRun) { + return; + } window.__issue_4836_ready__ = false; window.__issue_4836_error__ = error instanceof Error ? error.message : String(error); }} From e7bc02bea2b68734b048f05584ff8542c1257510 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 22 Sep 2026 19:32:37 +0800 Subject: [PATCH 10/10] test(react-vtable): isolate strict mode errors Capture an immutable readiness run in each rendered callback. Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com --- .../issue-4836-link-button-stage.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx b/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx index 8754439a6..f98c055e1 100644 --- a/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx +++ b/packages/react-vtable/demo/src/list-table/issue-4836-link-button-stage.tsx @@ -1,6 +1,6 @@ /* global window */ import type { Tag } from '@visactor/vtable/es/vrender'; -import { useEffect, useLayoutEffect, useRef } from 'react'; +import { useEffect, useLayoutEffect, useRef, useState } from 'react'; import type { CustomLayoutFunctionArg } from '../../../src'; import { Button, Group, Link, ListColumn, ListTable } from '../../../src'; @@ -143,18 +143,16 @@ function ActionCell(props: ActionCellProps) { } function App() { - const readinessToken = useRef({ active: false, run: 0 }); + const [activeRun, setActiveRun] = useState(null); useLayoutEffect(() => { readinessRun += 1; - const token = readinessToken.current; - token.active = true; - token.run = readinessRun; + const run = readinessRun; + setActiveRun(run); stagedControls.clear(); observedKinds.clear(); window.__issue_4836_ready__ = false; delete window.__issue_4836_error__; - const run = token.run; let remainingFrames = 120; const checkReadyDeadline = () => { @@ -177,8 +175,7 @@ function App() { scheduleAnimationFrame(checkReadyDeadline); return () => { - token.active = false; - if (readinessRun !== token.run) { + if (readinessRun !== run) { return; } readinessRun += 1; @@ -190,14 +187,17 @@ function App() { }; }, []); + if (activeRun === null) { + return null; + } + return ( { - const token = readinessToken.current; - if (!token.active || token.run !== readinessRun) { + if (activeRun !== readinessRun) { return; } window.__issue_4836_ready__ = false;