Files
orca/tests/e2e/csv-table-editing.spec.ts
T
Neil c53b6f235a Edit CSV tables directly and remember column widths (#25442)
* Add direct CSV table editing and persistent column widths

* Preserve pending CSV edits across concurrent saves and panes

* Keep CSV input stable during autosave and table commands

* Cancel delayed menu paste after CSV pane focus changes

* Prove delayed CSV menu paste starts before focus changes
2026-10-05 01:00:20 -07:00

330 lines
14 KiB
TypeScript

import { readFileSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { expect, test } from './helpers/orca-app'
test.use({
orcaAppExtraArgs: process.env.ORCA_CSV_CDP_PORT
? [`--remote-debugging-port=${process.env.ORCA_CSV_CDP_PORT}`]
: []
})
test('CSV table edits, inspection, clipboard and widths survive save and view switches', async ({
orcaPage,
seededRepoPath,
electronApp
}, testInfo) => {
const name = 'csv-table-editing.csv'
const filePath = path.join(seededRepoPath, name)
const original =
'\ufeffName,Score,Description\r\nBeta,2,"Original text"\r\nAlpha,10,Second record\r\n'
writeFileSync(filePath, original)
await orcaPage.evaluate(
async ({ filePath, name }) => {
const state = window.__store?.getState()
if (!state?.activeWorktreeId) {
throw new Error('Missing CSV workspace')
}
await state.updateSettings({ editorAutoSave: false })
state.openFile(
{
filePath,
relativePath: name,
worktreeId: state.activeWorktreeId,
language: 'plaintext',
mode: 'edit'
},
{ preview: false }
)
},
{ filePath, name }
)
const grid = orcaPage.getByTestId('csv-grid')
await expect(grid).toHaveAttribute('role', 'grid')
await orcaPage.screenshot({ path: testInfo.outputPath('csv-editable-before-interaction.png') })
const beta = grid.getByRole('gridcell', { name: 'Beta', exact: true })
await beta.dblclick()
const input = orcaPage.getByRole('textbox', { name: /Edit row/ })
await input.fill('Cancelled')
await input.press('Escape')
await expect(beta).toBeVisible()
await expect
.poll(() =>
orcaPage.evaluate(() =>
Object.values(window.__store?.getState().editorDrafts ?? {}).some((draft) =>
draft.includes('Cancelled')
)
)
)
.toBe(false)
await beta.dblclick()
await input.fill('Beta updated')
// Switch directly while the cell owns focus; the pending value must reach Source.
await orcaPage.getByRole('radio', { name: 'Source', exact: true }).click()
await expect(orcaPage.locator('.monaco-editor')).toBeVisible()
await expect
.poll(() =>
orcaPage.evaluate(() =>
Object.values(window.__store?.getState().editorDrafts ?? {}).some((draft) =>
draft.includes('Beta updated')
)
)
)
.toBe(true)
await orcaPage.getByRole('radio', { name: 'Table', exact: true }).click()
await expect(grid.getByRole('gridcell', { name: 'Beta updated', exact: true })).toBeVisible()
const separator = orcaPage.getByRole('separator', { name: 'Resize column 3', exact: true })
const initialWidth = Number(await separator.getAttribute('aria-valuenow'))
await separator.press('Shift+ArrowRight')
const resized = String(initialWidth + 40)
await expect(separator).toHaveAttribute('aria-valuenow', resized)
await orcaPage.getByRole('radio', { name: 'Source', exact: true }).click()
await orcaPage.getByRole('radio', { name: 'Table', exact: true }).click()
await expect(separator).toHaveAttribute('aria-valuenow', resized!)
await separator.press('Home')
await grid.getByRole('gridcell', { name: 'Beta updated', exact: true }).click()
await orcaPage.getByRole('button', { name: 'Sort', exact: true }).click()
await orcaPage
.getByRole('menuitem', { name: 'Sort selected column ascending', exact: true })
.click()
await expect(grid.getByRole('row').nth(1)).toContainText('Alpha')
await orcaPage.getByRole('textbox', { name: 'Filter rows', exact: true }).fill('Alpha')
await grid.getByRole('gridcell', { name: '10', exact: true }).dblclick()
await input.fill('11')
await input.press('Enter')
await orcaPage.getByRole('textbox', { name: 'Filter rows', exact: true }).fill('')
await grid.getByRole('gridcell', { name: 'Alpha', exact: true }).click()
await grid.press('Shift+ArrowRight')
await expect(grid.locator('[aria-selected="true"]')).toHaveCount(2)
// DOM clipboard events exercise range handling without replacing the user's OS clipboard.
const clipboard = await grid.evaluate((element) => {
const copied = new DataTransfer()
element.dispatchEvent(
new ClipboardEvent('copy', { bubbles: true, cancelable: true, clipboardData: copied })
)
const pasted = new DataTransfer()
pasted.setData('text/plain', 'Alpha renamed\t12')
element.dispatchEvent(
new ClipboardEvent('paste', { bubbles: true, cancelable: true, clipboardData: pasted })
)
return copied.getData('text/plain')
})
expect(clipboard).toBe('Alpha\t11')
await expect(grid.getByRole('gridcell', { name: '12', exact: true })).toBeVisible()
await orcaPage.getByRole('button', { name: 'Save', exact: true }).click()
const expected =
'\ufeffName,Score,Description\r\nBeta updated,2,"Original text"\r\nAlpha renamed,12,Second record\r\n'
await expect.poll(() => readFileSync(filePath, 'utf8')).toBe(expected)
await orcaPage.screenshot({ path: testInfo.outputPath('csv-table-editing-after.png') })
if (
process.env.ORCA_E2E_FORCE_HEADFUL !== '1' &&
testInfo.project.metadata.orcaHeadful !== true
) {
expect(
await electronApp.evaluate(({ BrowserWindow }) =>
BrowserWindow.getAllWindows().some((window) => window.isVisible())
)
).toBe(false)
}
})
test('CSV row and column operations preserve column widths and produce the expected file', async ({
orcaPage,
seededRepoPath
}, testInfo) => {
const name = 'csv-structure.csv'
const filePath = path.join(seededRepoPath, name)
writeFileSync(filePath, 'First,Second,Third\nA,1,x\nB,2,y')
await orcaPage.evaluate(
async ({ filePath, name }) => {
const state = window.__store?.getState()
if (!state?.activeWorktreeId) {
throw new Error('Missing CSV workspace')
}
await state.updateSettings({ editorAutoSave: false })
state.openFile(
{
filePath,
relativePath: name,
worktreeId: state.activeWorktreeId,
language: 'plaintext',
mode: 'edit'
},
{ preview: false }
)
},
{ filePath, name }
)
const grid = orcaPage.getByTestId('csv-grid')
await expect(grid).toHaveAttribute('role', 'grid')
await grid.getByRole('gridcell', { name: '1', exact: true }).click()
const secondWidth = orcaPage.getByRole('separator', { name: 'Resize column 2', exact: true })
await secondWidth.press('ArrowRight')
const width = await secondWidth.getAttribute('aria-valuenow')
await orcaPage.getByRole('button', { name: 'Columns', exact: true }).click()
await orcaPage.getByRole('menuitem', { name: 'Move column left', exact: true }).click()
await expect(grid.getByRole('columnheader').nth(1)).toHaveText('Second')
await expect(
orcaPage.getByRole('separator', { name: 'Resize column 1', exact: true })
).toHaveAttribute('aria-valuenow', width!)
await orcaPage.getByRole('button', { name: 'Columns', exact: true }).click()
await orcaPage.getByRole('menuitem', { name: 'Insert column after', exact: true }).click()
await expect(grid).toHaveAttribute('aria-colcount', '5')
await grid.getByRole('columnheader', { name: 'Column 4', exact: true }).click()
await orcaPage.getByRole('button', { name: 'Columns', exact: true }).click()
await orcaPage.getByRole('menuitem', { name: 'Delete selected columns', exact: true }).click()
await expect(grid).toHaveAttribute('aria-colcount', '4')
await grid.getByRole('gridcell', { name: 'A', exact: true }).click()
await orcaPage.getByRole('button', { name: 'Rows', exact: true }).click()
await orcaPage.getByRole('menuitem', { name: 'Insert row below', exact: true }).click()
await expect(grid).toHaveAttribute('aria-rowcount', '4')
await grid.locator('[data-csv-row="2"][data-csv-column="0"]').click()
await orcaPage.getByRole('button', { name: 'Rows', exact: true }).click()
await orcaPage.getByRole('menuitem', { name: 'Delete selected rows', exact: true }).click()
await expect(grid).toHaveAttribute('aria-rowcount', '3')
await orcaPage.getByRole('button', { name: 'Save', exact: true }).click()
await expect.poll(() => readFileSync(filePath, 'utf8')).toBe('Second,First,Third\n1,A,x\n2,B,y')
await orcaPage.screenshot({ path: testInfo.outputPath('csv-structure-after.png') })
})
test('keyboard selection reaches and edits a cell beyond both virtualized viewports', async ({
orcaPage,
seededRepoPath
}, testInfo) => {
const filePath = path.join(seededRepoPath, 'editable-wide.csv')
const header = Array.from({ length: 1024 }, (_, column) => `column-${column}`).join(',')
const row = Array.from({ length: 1024 }, (_, column) => `v${column}`).join(',')
writeFileSync(filePath, `${header}\n${`${row}\n`.repeat(100)}`)
await orcaPage.evaluate(
async ({ filePath }) => {
const state = window.__store?.getState()
if (!state?.activeWorktreeId) {
throw new Error('Missing CSV workspace')
}
await state.updateSettings({ editorAutoSave: false })
state.openFile(
{
filePath,
relativePath: 'editable-wide.csv',
worktreeId: state.activeWorktreeId,
language: 'plaintext',
mode: 'edit'
},
{ preview: false }
)
},
{ filePath }
)
const grid = orcaPage.getByTestId('csv-grid')
await expect(grid).toHaveAttribute('aria-colcount', '1025')
// Dispatch on the clipped cell without Playwright scrolling it into view first.
const clipped = await grid.evaluate((element) => {
const viewport = element.closest('[data-testid="csv-scroll"]')
if (!viewport) {
throw new Error('Missing CSV scroll surface')
}
const edge = viewport.getBoundingClientRect().right
const cell = [...element.querySelectorAll('[data-csv-row="1"]')].find((cell) => {
const bounds = cell.getBoundingClientRect()
return bounds.left < edge - 4 && bounds.right > edge + 4
})
if (!cell) {
throw new Error('Missing clipped cell')
}
const column = Number(cell.getAttribute('data-csv-column'))
cell.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))
return { column, scrollLeft: viewport.scrollLeft }
})
const clippedInput = orcaPage.getByRole('textbox', {
name: `Edit row 2, column ${clipped.column + 1}`,
exact: true
})
await expect(clippedInput).toBeFocused()
expect(await orcaPage.getByTestId('csv-scroll').evaluate((element) => element.scrollLeft)).toBe(
clipped.scrollLeft
)
await orcaPage.keyboard.type('z')
await expect(clippedInput).toHaveValue(`zv${clipped.column}`)
await expect(clippedInput).toBeFocused()
await orcaPage.screenshot({ path: testInfo.outputPath('csv-clipped-cell-editor.png') })
await orcaPage.getByTestId('csv-scroll').evaluate((element) => {
element.scrollTop = 2000
})
await expect(clippedInput).toHaveCount(0)
expect(
await orcaPage.getByTestId('csv-scroll').evaluate((element) => element.scrollTop)
).toBeGreaterThan(0)
await orcaPage.getByTestId('csv-scroll').evaluate((element) => {
element.scrollTop = 0
})
await grid.getByRole('gridcell', { name: 'v0', exact: true }).first().click()
const modifier = await orcaPage.evaluate(() =>
navigator.userAgent.includes('Mac') ? 'Meta' : 'Control'
)
await grid.press(`${modifier}+End`)
const last = grid.locator('[data-csv-row="100"][data-csv-column="1023"]')
await expect(last).toBeVisible()
await expect(last).toHaveAttribute('data-active', 'true')
await grid.press('F2')
const input = orcaPage.getByRole('textbox', { name: 'Edit row 101, column 1024', exact: true })
await input.fill('edited-final-cell')
await input.press('Enter')
await expect(last).toHaveText('edited-final-cell')
expect(await grid.getByRole('columnheader').count()).toBeLessThan(40)
expect(await grid.getByRole('row').count()).toBeLessThan(80)
await orcaPage.getByRole('button', { name: 'Save', exact: true }).click()
await expect
.poll(() => readFileSync(filePath, 'utf8').split('\n').at(-2)?.split(',').at(-1))
.toBe('edited-final-cell')
expect(readFileSync(filePath, 'utf8').split('\n')[1]?.split(',')[clipped.column]).toBe(
`zv${clipped.column}`
)
})
test('autosave preserves an active cell while saving the previously committed draft', async ({
orcaPage,
seededRepoPath
}, testInfo) => {
const filePath = path.join(seededRepoPath, 'csv-autosave.csv')
writeFileSync(filePath, 'Name,Value\nA,1\nB,2')
await orcaPage.evaluate(
async ({ filePath }) => {
const state = window.__store?.getState()
if (!state?.activeWorktreeId) {
throw new Error('Missing CSV workspace')
}
await state.updateSettings({ editorAutoSave: true, editorAutoSaveDelayMs: 1000 })
state.openFile(
{
filePath,
relativePath: 'csv-autosave.csv',
worktreeId: state.activeWorktreeId,
language: 'plaintext',
mode: 'edit'
},
{ preview: false }
)
},
{ filePath }
)
const grid = orcaPage.getByTestId('csv-grid')
await grid.getByRole('gridcell', { name: 'A', exact: true }).dblclick()
const input = orcaPage.getByRole('textbox', { name: /Edit row/ })
await input.fill('committed')
await input.press('Enter')
await grid.getByRole('gridcell', { name: 'B', exact: true }).dblclick()
await input.fill('still typing')
await expect.poll(() => readFileSync(filePath, 'utf8')).toBe('Name,Value\ncommitted,1\nB,2')
await expect(input).toBeFocused()
await expect(input).toHaveValue('still typing')
await expect
.poll(() =>
orcaPage.evaluate(() => window.__store?.getState().openFiles.some((file) => file.isDirty))
)
.toBe(true)
await orcaPage.screenshot({ path: testInfo.outputPath('csv-autosave-keeps-input.png') })
await input.press('Enter')
await expect
.poll(() => readFileSync(filePath, 'utf8'))
.toBe('Name,Value\ncommitted,1\nstill typing,2')
})