mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 00:02:38 +00:00
* 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
330 lines
14 KiB
TypeScript
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')
|
|
})
|