From 3f81b2a48f1ce6320b1271802cc38548af9c364d Mon Sep 17 00:00:00 2001
From: fangsmile <892739385@qq.com>
Date: Fri, 18 Sep 2026 15:57:22 +0800
Subject: [PATCH 1/3] test(vtable): cover Firefox paste event fallback
Protect synchronous paste data when the async Clipboard API is unavailable.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
---
.../vtable/__tests__/clipboard-paste.test.ts | 56 ++++++++++++
.../debug/issue-4699-firefox-paste.ts | 85 +++++++++++++++++++
packages/vtable/examples/menu.ts | 4 +
3 files changed, 145 insertions(+)
create mode 100644 packages/vtable/__tests__/clipboard-paste.test.ts
create mode 100644 packages/vtable/examples/debug/issue-4699-firefox-paste.ts
diff --git a/packages/vtable/__tests__/clipboard-paste.test.ts b/packages/vtable/__tests__/clipboard-paste.test.ts
new file mode 100644
index 000000000..d599fba70
--- /dev/null
+++ b/packages/vtable/__tests__/clipboard-paste.test.ts
@@ -0,0 +1,56 @@
+// @ts-nocheck
+import { ListTable, TABLE_EVENT_TYPE } from '../src';
+import { createDiv, removeDom } from './dom';
+
+global.__VERSION__ = 'none';
+
+describe('clipboard paste', () => {
+ test('uses paste event data before the asynchronous Clipboard API', async () => {
+ const container = createDiv();
+ container.style.width = '600px';
+ container.style.height = '400px';
+
+ const clipboardDescriptor = Object.getOwnPropertyDescriptor(navigator, 'clipboard');
+ const read = jest.fn(() => Promise.reject(new Error('clipboard-read is unavailable')));
+ Object.defineProperty(navigator, 'clipboard', {
+ configurable: true,
+ value: { read }
+ });
+
+ const table = new ListTable(container, {
+ records: [{ name: 'before' }],
+ columns: [{ field: 'name', title: 'Name', editor: '' }],
+ keyboardOptions: {
+ pasteValueToCell: true
+ }
+ });
+
+ try {
+ table.selectCell(0, table.columnHeaderLevelCount);
+ const pasted = new Promise(resolve => {
+ table.on(TABLE_EVENT_TYPE.PASTED_DATA, resolve);
+ });
+ const pasteEvent = new Event('paste', { bubbles: true });
+ Object.defineProperty(pasteEvent, 'clipboardData', {
+ value: {
+ types: ['text/plain'],
+ getData: (type: string) => (type === 'text/plain' ? 'pasted in Firefox' : '')
+ }
+ });
+
+ table.getElement().dispatchEvent(pasteEvent);
+ await pasted;
+
+ expect(read).not.toHaveBeenCalled();
+ expect(table.getCellOriginValue(0, table.columnHeaderLevelCount)).toBe('pasted in Firefox');
+ } finally {
+ table.release();
+ removeDom(container);
+ if (clipboardDescriptor) {
+ Object.defineProperty(navigator, 'clipboard', clipboardDescriptor);
+ } else {
+ Reflect.deleteProperty(navigator, 'clipboard');
+ }
+ }
+ });
+});
diff --git a/packages/vtable/examples/debug/issue-4699-firefox-paste.ts b/packages/vtable/examples/debug/issue-4699-firefox-paste.ts
new file mode 100644
index 000000000..875a7d12d
--- /dev/null
+++ b/packages/vtable/examples/debug/issue-4699-firefox-paste.ts
@@ -0,0 +1,85 @@
+import * as VTable from '../../src';
+import { InputEditor } from '@visactor/vtable-editors';
+
+const CONTAINER_ID = 'vTable';
+const PASTE_VALUE = 'Firefox paste works';
+const inputEditor = new InputEditor({});
+VTable.register.editor('issue4699-input', inputEditor);
+
+interface Issue4699Window extends Window {
+ tableInstance?: VTable.ListTable;
+ issue4699Run?: () => void;
+ BUGSERVER_SCREENSHOT?: () => void;
+}
+
+const records = [
+ { item: 'Alpha', result: 'Waiting' },
+ { item: 'Beta', result: 'Waiting' },
+ { item: 'Gamma', result: 'Waiting' }
+];
+
+const createPasteEvent = (text: string) => {
+ const event = new Event('paste', { bubbles: true });
+ Object.defineProperty(event, 'clipboardData', {
+ value: {
+ types: ['text/plain'],
+ getData: (type: string) => (type === 'text/plain' ? text : '')
+ }
+ });
+ return event;
+};
+
+export function createTable() {
+ const container = document.getElementById(CONTAINER_ID);
+ if (!container) {
+ throw new Error(`Missing #${CONTAINER_ID} container`);
+ }
+ const status = document.createElement('div');
+ status.id = 'issue4699Status';
+ status.style.cssText = [
+ 'height: 36px',
+ 'line-height: 36px',
+ 'margin-bottom: 8px',
+ 'padding: 0 12px',
+ 'font: 600 14px/36px sans-serif',
+ 'background: #f5f5f5',
+ 'color: #595959'
+ ].join(';');
+ status.textContent = 'READY | Select a Result cell and paste in Firefox';
+ container.parentElement?.insertBefore(status, container);
+
+ const tableInstance = new VTable.ListTable(container, {
+ records,
+ columns: [
+ { field: 'item', title: 'Item', width: 180 },
+ { field: 'result', title: 'Result', width: 260, editor: 'issue4699-input' }
+ ],
+ keyboardOptions: {
+ pasteValueToCell: true
+ },
+ widthMode: 'standard',
+ defaultRowHeight: 44
+ });
+
+ const targetRow = tableInstance.columnHeaderLevelCount;
+ const runCheck = () => {
+ tableInstance.selectCell(1, targetRow);
+ tableInstance.getElement().focus();
+ tableInstance.getElement().dispatchEvent(createPasteEvent(PASTE_VALUE));
+ };
+
+ tableInstance.on(VTable.TABLE_EVENT_TYPE.PASTED_DATA, () => {
+ requestAnimationFrame(() => {
+ const value = tableInstance.getCellOriginValue(1, targetRow);
+ const pass = value === PASTE_VALUE;
+ status.textContent = `${pass ? 'PASS' : 'FAIL'} | pasted value: ${String(value)}`;
+ status.style.background = pass ? '#f6ffed' : '#fff2f0';
+ status.style.color = pass ? '#237804' : '#a8071a';
+ (window as Issue4699Window).BUGSERVER_SCREENSHOT?.();
+ });
+ });
+
+ (window as Issue4699Window).tableInstance = tableInstance;
+ (window as Issue4699Window).issue4699Run = runCheck;
+ requestAnimationFrame(runCheck);
+}
diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts
index b30a28fde..be58b3652 100644
--- a/packages/vtable/examples/menu.ts
+++ b/packages/vtable/examples/menu.ts
@@ -74,6 +74,10 @@ export const menus = [
path: 'debug',
name: 'issue-3903-delete-all-bottom-frozen'
},
+ {
+ path: 'debug',
+ name: 'issue-4699-firefox-paste'
+ },
{
path: 'debug',
name: 'issue-3752-large-merge-auto-height'
From ad3462dc39936fbfeb2c77cd1bfd9b848354710a Mon Sep 17 00:00:00 2001
From: fangsmile <892739385@qq.com>
Date: Fri, 18 Sep 2026 17:00:09 +0800
Subject: [PATCH 2/3] test(vtable): run Firefox paste regression in Playwright
Replace the synthetic Bugserver coverage with a Firefox browser test enforced by CI.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
---
.github/workflows/unit-test.yml | 6 ++
.gitignore | 2 +
common/config/rush/pnpm-lock.yaml | 43 +++++++++-
.../e2e/clipboard-paste.firefox.spec.ts | 72 ++++++++++++++++
packages/vtable/e2e/fixtures/app.ts | 19 +++++
packages/vtable/e2e/fixtures/index.html | 15 ++++
packages/vtable/e2e/vite.config.ts | 15 ++++
.../debug/issue-4699-firefox-paste.ts | 85 -------------------
packages/vtable/examples/menu.ts | 4 -
packages/vtable/package.json | 5 +-
packages/vtable/playwright.config.ts | 24 ++++++
packages/vtable/tsconfig.eslint.json | 2 +-
12 files changed, 199 insertions(+), 93 deletions(-)
create mode 100644 packages/vtable/e2e/clipboard-paste.firefox.spec.ts
create mode 100644 packages/vtable/e2e/fixtures/app.ts
create mode 100644 packages/vtable/e2e/fixtures/index.html
create mode 100644 packages/vtable/e2e/vite.config.ts
delete mode 100644 packages/vtable/examples/debug/issue-4699-firefox-paste.ts
create mode 100644 packages/vtable/playwright.config.ts
diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml
index e5705096e..874107748 100644
--- a/.github/workflows/unit-test.yml
+++ b/.github/workflows/unit-test.yml
@@ -36,3 +36,9 @@ jobs:
run: node common/scripts/install-run-rush.js build -t @visactor/vtable-search
- name: Test
run: node common/scripts/install-run-rush.js test -t @visactor/vtable-search
+ - name: Install Playwright Firefox
+ working-directory: ./packages/vtable
+ run: npx playwright install firefox
+ - name: Test Firefox paste
+ working-directory: ./packages/vtable
+ run: node ../../common/scripts/install-run-rushx.js test:firefox
diff --git a/.gitignore b/.gitignore
index 83c3ca471..dc413e3d5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,6 +43,8 @@ stats.html
report.html
coverage/
+playwright-report/
+test-results/
*.local.ts
diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml
index 7ec0174aa..a968e6123 100644
--- a/common/config/rush/pnpm-lock.yaml
+++ b/common/config/rush/pnpm-lock.yaml
@@ -492,6 +492,9 @@ importers:
'@internal/ts-config':
specifier: workspace:*
version: link:../../share/ts-config
+ '@playwright/test':
+ specifier: 1.55.1
+ version: 1.55.1
'@resvg/resvg-js':
specifier: ^2.5.0
version: 2.6.2
@@ -2132,7 +2135,7 @@ importers:
version: 4.9.5
vitest:
specifier: 0.30.1
- version: 0.30.1(jsdom@16.7.0)(less@4.1.3)(sass@1.43.5)(terser@5.17.1)
+ version: 0.30.1(jsdom@16.7.0)(less@4.1.3)(playwright@1.55.1)(sass@1.43.5)(terser@5.17.1)
packages:
@@ -2967,6 +2970,11 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
+ '@playwright/test@1.55.1':
+ resolution: {integrity: sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==}
+ engines: {node: '>=18'}
+ hasBin: true
+
'@remix-run/router@1.4.0':
resolution: {integrity: sha512-BJ9SxXux8zAg991UmT8slpwpsd31K1dHHbD3Ba4VzD+liLQ4WAMSxQp2d2ZPRPfN0jN2NPRowcSSoM7lCaF08Q==}
engines: {node: '>=14'}
@@ -5472,6 +5480,11 @@ packages:
os: [darwin]
deprecated: Upgrade to fsevents v2 to mitigate potential security issues
+ fsevents@2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -7393,6 +7406,16 @@ packages:
pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
+ playwright-core@1.55.1:
+ resolution: {integrity: sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ playwright@1.55.1:
+ resolution: {integrity: sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==}
+ engines: {node: '>=18'}
+ hasBin: true
+
plugin-error@0.1.2:
resolution: {integrity: sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==}
engines: {node: '>=0.10.0'}
@@ -10587,6 +10610,10 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1
+ '@playwright/test@1.55.1':
+ dependencies:
+ playwright: 1.55.1
+
'@remix-run/router@1.4.0': {}
'@resvg/resvg-js-android-arm-eabi@2.4.1':
@@ -13496,6 +13523,9 @@ snapshots:
nan: 2.26.2
optional: true
+ fsevents@2.3.2:
+ optional: true
+
fsevents@2.3.3:
optional: true
@@ -15929,6 +15959,14 @@ snapshots:
mlly: 1.8.2
pathe: 2.0.3
+ playwright-core@1.55.1: {}
+
+ playwright@1.55.1:
+ dependencies:
+ playwright-core: 1.55.1
+ optionalDependencies:
+ fsevents: 2.3.2
+
plugin-error@0.1.2:
dependencies:
ansi-cyan: 0.1.1
@@ -17801,7 +17839,7 @@ snapshots:
sass: 1.43.5
terser: 5.17.1
- vitest@0.30.1(jsdom@16.7.0)(less@4.1.3)(sass@1.43.5)(terser@5.17.1):
+ vitest@0.30.1(jsdom@16.7.0)(less@4.1.3)(playwright@1.55.1)(sass@1.43.5)(terser@5.17.1):
dependencies:
'@types/chai': 4.3.20
'@types/chai-subset': 1.3.6(@types/chai@4.3.20)
@@ -17831,6 +17869,7 @@ snapshots:
why-is-node-running: 2.3.0
optionalDependencies:
jsdom: 16.7.0
+ playwright: 1.55.1
transitivePeerDependencies:
- less
- sass
diff --git a/packages/vtable/e2e/clipboard-paste.firefox.spec.ts b/packages/vtable/e2e/clipboard-paste.firefox.spec.ts
new file mode 100644
index 000000000..9a5be52c8
--- /dev/null
+++ b/packages/vtable/e2e/clipboard-paste.firefox.spec.ts
@@ -0,0 +1,72 @@
+import { expect, test } from '@playwright/test';
+
+interface Issue4699Table {
+ columnHeaderLevelCount: number;
+ getCellOriginValue: (col: number, row: number) => unknown;
+ getElement: () => HTMLElement;
+ selectCell: (col: number, row: number) => void;
+}
+
+interface Issue4699Window extends Window {
+ __issue4699ClipboardState: {
+ readCalls: number;
+ };
+ __issue4699Table: Issue4699Table;
+}
+
+test('pastes event clipboard data into a selected cell without an editor caret', async ({ page, browserName }) => {
+ expect(browserName).toBe('firefox');
+
+ await page.addInitScript(() => {
+ const state = { readCalls: 0 };
+ Object.defineProperty(window, '__issue4699ClipboardState', {
+ configurable: true,
+ value: state
+ });
+ Object.defineProperty(navigator, 'clipboard', {
+ configurable: true,
+ value: {
+ read: () => {
+ state.readCalls++;
+ return Promise.reject(new Error('Firefox clipboard-read is unavailable'));
+ }
+ }
+ });
+ });
+
+ await page.goto('/');
+ await page.waitForFunction(() => Boolean((window as unknown as Issue4699Window).__issue4699Table));
+
+ const activeElement = await page.evaluate(() => {
+ const testWindow = window as unknown as Issue4699Window;
+ const table = testWindow.__issue4699Table;
+ const row = table.columnHeaderLevelCount;
+ const clipboardData = new DataTransfer();
+ clipboardData.setData('text/plain', 'pasted in Firefox');
+ const pasteEvent = new ClipboardEvent('paste', {
+ bubbles: true,
+ cancelable: true
+ });
+ Object.defineProperty(pasteEvent, 'clipboardData', {
+ value: clipboardData
+ });
+
+ table.selectCell(0, row);
+ table.getElement().focus();
+ table.getElement().dispatchEvent(pasteEvent);
+ return document.activeElement?.tagName;
+ });
+
+ expect(activeElement).not.toMatch(/^(INPUT|TEXTAREA)$/);
+ await expect
+ .poll(() =>
+ page.evaluate(() => {
+ const table = (window as unknown as Issue4699Window).__issue4699Table;
+ return table.getCellOriginValue(0, table.columnHeaderLevelCount);
+ })
+ )
+ .toBe('pasted in Firefox');
+ await expect
+ .poll(() => page.evaluate(() => (window as unknown as Issue4699Window).__issue4699ClipboardState.readCalls))
+ .toBe(0);
+});
diff --git a/packages/vtable/e2e/fixtures/app.ts b/packages/vtable/e2e/fixtures/app.ts
new file mode 100644
index 000000000..c8460e0ad
--- /dev/null
+++ b/packages/vtable/e2e/fixtures/app.ts
@@ -0,0 +1,19 @@
+import { ListTable } from '../../src';
+
+const container = document.getElementById('vtable');
+if (!container) {
+ throw new Error('Missing #vtable container');
+}
+
+const table = new ListTable(container, {
+ records: [{ value: 'before' }],
+ columns: [{ field: 'value', title: 'Value', editor: '' }],
+ keyboardOptions: {
+ pasteValueToCell: true
+ }
+});
+
+Object.defineProperty(window, '__issue4699Table', {
+ configurable: true,
+ value: table
+});
diff --git a/packages/vtable/e2e/fixtures/index.html b/packages/vtable/e2e/fixtures/index.html
new file mode 100644
index 000000000..8341bf9d1
--- /dev/null
+++ b/packages/vtable/e2e/fixtures/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/packages/vtable/e2e/vite.config.ts b/packages/vtable/e2e/vite.config.ts
new file mode 100644
index 000000000..5aa218d0d
--- /dev/null
+++ b/packages/vtable/e2e/vite.config.ts
@@ -0,0 +1,15 @@
+import path from 'path';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ define: {
+ __DEV__: true,
+ __VERSION__: JSON.stringify('e2e')
+ },
+ resolve: {
+ alias: {
+ '@src': path.resolve(__dirname, '../src'),
+ '@vutils-extension': path.resolve(__dirname, '../src/vutil-extension-temp')
+ }
+ }
+});
diff --git a/packages/vtable/examples/debug/issue-4699-firefox-paste.ts b/packages/vtable/examples/debug/issue-4699-firefox-paste.ts
deleted file mode 100644
index 875a7d12d..000000000
--- a/packages/vtable/examples/debug/issue-4699-firefox-paste.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import * as VTable from '../../src';
-import { InputEditor } from '@visactor/vtable-editors';
-
-const CONTAINER_ID = 'vTable';
-const PASTE_VALUE = 'Firefox paste works';
-const inputEditor = new InputEditor({});
-VTable.register.editor('issue4699-input', inputEditor);
-
-interface Issue4699Window extends Window {
- tableInstance?: VTable.ListTable;
- issue4699Run?: () => void;
- BUGSERVER_SCREENSHOT?: () => void;
-}
-
-const records = [
- { item: 'Alpha', result: 'Waiting' },
- { item: 'Beta', result: 'Waiting' },
- { item: 'Gamma', result: 'Waiting' }
-];
-
-const createPasteEvent = (text: string) => {
- const event = new Event('paste', { bubbles: true });
- Object.defineProperty(event, 'clipboardData', {
- value: {
- types: ['text/plain'],
- getData: (type: string) => (type === 'text/plain' ? text : '')
- }
- });
- return event;
-};
-
-export function createTable() {
- const container = document.getElementById(CONTAINER_ID);
- if (!container) {
- throw new Error(`Missing #${CONTAINER_ID} container`);
- }
- const status = document.createElement('div');
- status.id = 'issue4699Status';
- status.style.cssText = [
- 'height: 36px',
- 'line-height: 36px',
- 'margin-bottom: 8px',
- 'padding: 0 12px',
- 'font: 600 14px/36px sans-serif',
- 'background: #f5f5f5',
- 'color: #595959'
- ].join(';');
- status.textContent = 'READY | Select a Result cell and paste in Firefox';
- container.parentElement?.insertBefore(status, container);
-
- const tableInstance = new VTable.ListTable(container, {
- records,
- columns: [
- { field: 'item', title: 'Item', width: 180 },
- { field: 'result', title: 'Result', width: 260, editor: 'issue4699-input' }
- ],
- keyboardOptions: {
- pasteValueToCell: true
- },
- widthMode: 'standard',
- defaultRowHeight: 44
- });
-
- const targetRow = tableInstance.columnHeaderLevelCount;
- const runCheck = () => {
- tableInstance.selectCell(1, targetRow);
- tableInstance.getElement().focus();
- tableInstance.getElement().dispatchEvent(createPasteEvent(PASTE_VALUE));
- };
-
- tableInstance.on(VTable.TABLE_EVENT_TYPE.PASTED_DATA, () => {
- requestAnimationFrame(() => {
- const value = tableInstance.getCellOriginValue(1, targetRow);
- const pass = value === PASTE_VALUE;
- status.textContent = `${pass ? 'PASS' : 'FAIL'} | pasted value: ${String(value)}`;
- status.style.background = pass ? '#f6ffed' : '#fff2f0';
- status.style.color = pass ? '#237804' : '#a8071a';
- (window as Issue4699Window).BUGSERVER_SCREENSHOT?.();
- });
- });
-
- (window as Issue4699Window).tableInstance = tableInstance;
- (window as Issue4699Window).issue4699Run = runCheck;
- requestAnimationFrame(runCheck);
-}
diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts
index be58b3652..b30a28fde 100644
--- a/packages/vtable/examples/menu.ts
+++ b/packages/vtable/examples/menu.ts
@@ -74,10 +74,6 @@ export const menus = [
path: 'debug',
name: 'issue-3903-delete-all-bottom-frozen'
},
- {
- path: 'debug',
- name: 'issue-4699-firefox-paste'
- },
{
path: 'debug',
name: 'issue-3752-large-merge-auto-height'
diff --git a/packages/vtable/package.json b/packages/vtable/package.json
index aea9cb8e3..1d94e3775 100644
--- a/packages/vtable/package.json
+++ b/packages/vtable/package.json
@@ -40,11 +40,13 @@
"demo": "vite serve examples",
"site-demo": "vite serve site-demo",
"compile": "tsc --noEmit",
+ "e2e:serve": "vite serve e2e/fixtures --config e2e/vite.config.ts --host 127.0.0.1 --port 3010",
"eslint": "eslint --debug --fix src/",
"build": "npm run fix-memory-limit && bundle --clean",
"dev": "bundle --clean -f es -w",
"start": "vite serve examples",
"test": "jest --silent",
+ "test:firefox": "playwright test --config playwright.config.ts",
"test-cov": "jest --coverage",
"ci": "ts-node --transpileOnly --skipProject ./scripts/trigger-test.ts",
"build:es": "bundle --clean -f es --ignorePostTasks",
@@ -120,7 +122,8 @@
"d3-hexbin": "^0.2.2",
"d3-hierarchy": "^3.1.1",
"@resvg/resvg-js": "^2.5.0",
- "pikaday": "1.8.2"
+ "pikaday": "1.8.2",
+ "@playwright/test": "1.55.1"
},
"unpkg": "latest",
"unpkgFiles": [
diff --git a/packages/vtable/playwright.config.ts b/packages/vtable/playwright.config.ts
new file mode 100644
index 000000000..a9c629fb9
--- /dev/null
+++ b/packages/vtable/playwright.config.ts
@@ -0,0 +1,24 @@
+import { defineConfig, devices } from '@playwright/test';
+
+export default defineConfig({
+ testDir: './e2e',
+ outputDir: './test-results',
+ reporter: 'list',
+ use: {
+ baseURL: 'http://127.0.0.1:3010',
+ trace: 'retain-on-failure'
+ },
+ projects: [
+ {
+ name: 'firefox',
+ use: {
+ ...devices['Desktop Firefox']
+ }
+ }
+ ],
+ webServer: {
+ command: 'rushx e2e:serve',
+ url: 'http://127.0.0.1:3010',
+ reuseExistingServer: !process.env.CI
+ }
+});
diff --git a/packages/vtable/tsconfig.eslint.json b/packages/vtable/tsconfig.eslint.json
index cbfa15726..401bd61b0 100644
--- a/packages/vtable/tsconfig.eslint.json
+++ b/packages/vtable/tsconfig.eslint.json
@@ -14,6 +14,6 @@
]
}
},
- "include": ["src", "__tests__", "examples", "site-demo"],
+ "include": ["src", "__tests__", "e2e", "examples", "site-demo", "playwright.config.ts"],
"exclude": ["bugserver-config"]
}
From 07723a232a12f34279b782a744193223e33bc1f9 Mon Sep 17 00:00:00 2001
From: fangsmile <892739385@qq.com>
Date: Fri, 18 Sep 2026 17:54:21 +0800
Subject: [PATCH 3/3] test(vtable): harden Firefox paste regression fixture
Keep the browser restriction explicit and make the fixture script path relative.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
---
packages/vtable/e2e/clipboard-paste.firefox.spec.ts | 2 +-
packages/vtable/e2e/fixtures/index.html | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/vtable/e2e/clipboard-paste.firefox.spec.ts b/packages/vtable/e2e/clipboard-paste.firefox.spec.ts
index 9a5be52c8..cac53f266 100644
--- a/packages/vtable/e2e/clipboard-paste.firefox.spec.ts
+++ b/packages/vtable/e2e/clipboard-paste.firefox.spec.ts
@@ -15,7 +15,7 @@ interface Issue4699Window extends Window {
}
test('pastes event clipboard data into a selected cell without an editor caret', async ({ page, browserName }) => {
- expect(browserName).toBe('firefox');
+ test.skip(browserName !== 'firefox', 'Firefox-only clipboard regression');
await page.addInitScript(() => {
const state = { readCalls: 0 };
diff --git a/packages/vtable/e2e/fixtures/index.html b/packages/vtable/e2e/fixtures/index.html
index 8341bf9d1..fa8f8f2a0 100644
--- a/packages/vtable/e2e/fixtures/index.html
+++ b/packages/vtable/e2e/fixtures/index.html
@@ -1,7 +1,7 @@
-
+