mirror of
https://github.com/supabase/supabase.git
synced 2026-09-09 03:19:36 +08:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Bug fix (accessibility improvement) ## What is the current behavior? Icon-only buttons do not have explicit accessible names for screen readers or tooltips. ## What is the new behavior? All icon-only buttons now have explicit accessible names using visually hidden text (sr-only), ensuring proper screen reader support. ## Additional context Tooltip text is preserved or added for visual users. No visual changes were introduced. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Accessibility Improvements** * Updated table editor action controls with clearer, context-aware `aria-label`s (e.g., “Add new column”, “More options for …”, “New table”). * **UI Refinements** * Added hover tooltips to key table editor actions, including add-column, more-options dropdown triggers, and create-new-table button, improving discoverability and guidance. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1393 lines
50 KiB
TypeScript
1393 lines
50 KiB
TypeScript
import { expect, Page } from '@playwright/test'
|
|
|
|
import { createTable, dropTable, query } from '../utils/db/index.js'
|
|
import { test, withSetupCleanup } from '../utils/test.js'
|
|
import { toUrl } from '../utils/to-url.js'
|
|
import { createApiResponseWaiter, waitForTableToLoad } from '../utils/wait-for-response.js'
|
|
|
|
const QUEUE_OPERATIONS_KEY = 'supabase-ui-queue-operations'
|
|
const tableNamePrefix = 'pw_queue_table'
|
|
|
|
const enableQueueOperations = async (page: Page) => {
|
|
await page.evaluate((key) => {
|
|
localStorage.setItem(key, 'true')
|
|
}, QUEUE_OPERATIONS_KEY)
|
|
}
|
|
|
|
const openQueueDropdownAndClick = async (page: Page, itemName: string) => {
|
|
await page.getByRole('button', { name: 'More options', exact: true }).click()
|
|
await page.getByRole('menuitem', { name: itemName }).click()
|
|
}
|
|
|
|
const clickReview = async (page: Page) => openQueueDropdownAndClick(page, 'Review')
|
|
const clickDiscard = async (page: Page) => openQueueDropdownAndClick(page, 'Discard')
|
|
|
|
const openTableWithQueueOperations = async (page: Page, ref: string, tableName: string) => {
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await enableQueueOperations(page)
|
|
const tableLoadWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=entity-types-public-'
|
|
)
|
|
await page.reload()
|
|
await tableLoadWait
|
|
|
|
const gridLoadWait = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=table-rows-')
|
|
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
|
|
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
await gridLoadWait
|
|
}
|
|
|
|
test.describe('Queue Table Operations', () => {
|
|
test.beforeEach(async ({ page, ref }) => {
|
|
const loadPromise = waitForTableToLoad(page, ref)
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await loadPromise
|
|
await enableQueueOperations(page)
|
|
// Wait on the table-list refetch that follows the reload rather than
|
|
// `networkidle` — studio keeps long-lived polling/SSE connections open
|
|
// (PostHog, Sentry, realtime) so `networkidle` never resolves and the
|
|
// beforeEach hits its 120 s test timeout. The reload's goal is "wait
|
|
// until the editor is loaded again with the new toggle applied" and
|
|
// the entity-types query is a precise signal for that.
|
|
const reloadWait = waitForTableToLoad(page, ref)
|
|
await page.reload()
|
|
await reloadWait
|
|
})
|
|
|
|
test('cell edits are queued and can be saved', async ({ page, ref }) => {
|
|
const tableName = `${tableNamePrefix}_cell_edit`
|
|
const columnName = 'name'
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createTable(tableName, columnName, [{ name: 'original value' }])
|
|
},
|
|
async () => {
|
|
await dropTable(tableName)
|
|
}
|
|
)
|
|
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await enableQueueOperations(page)
|
|
const tableLoadWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=entity-types-public-'
|
|
)
|
|
await page.reload()
|
|
await tableLoadWait
|
|
|
|
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
|
|
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
|
|
await expect(page.getByRole('gridcell', { name: 'original value' })).toBeVisible()
|
|
|
|
const cell = page.getByRole('gridcell', { name: 'original value' })
|
|
await cell.dblclick()
|
|
|
|
const editor = page.getByRole('textbox', { name: /Editor content/ })
|
|
await expect(editor).toBeVisible()
|
|
await editor.fill('edited value')
|
|
await page.keyboard.press('Enter')
|
|
|
|
await expect(page.getByText('1 pending change')).toBeVisible()
|
|
|
|
await clickReview(page)
|
|
|
|
const sidePanel = page.getByRole('dialog')
|
|
await expect(sidePanel.getByText('Pending changes')).toBeVisible()
|
|
await expect(sidePanel.getByText('1 cell edit')).toBeVisible()
|
|
await expect(sidePanel.getByTitle('original value')).toBeVisible()
|
|
await expect(sidePanel.getByTitle('edited value')).toBeVisible()
|
|
|
|
await sidePanel.getByRole('button', { name: /^Save/ }).click()
|
|
await expect(page.getByText('Changes saved successfully')).toBeVisible()
|
|
|
|
await expect(page.getByRole('gridcell', { name: 'edited value' })).toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'original value' })).not.toBeVisible()
|
|
})
|
|
|
|
test('cell edits can be cancelled', async ({ page, ref }) => {
|
|
const tableName = `${tableNamePrefix}_cell_cancel`
|
|
const columnName = 'name'
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createTable(tableName, columnName, [{ name: 'keep this value' }])
|
|
},
|
|
async () => {
|
|
await dropTable(tableName)
|
|
}
|
|
)
|
|
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await enableQueueOperations(page)
|
|
const tableLoadWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=entity-types-public-'
|
|
)
|
|
await page.reload()
|
|
await tableLoadWait
|
|
|
|
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
|
|
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
|
|
await expect(page.getByRole('gridcell', { name: 'keep this value' })).toBeVisible()
|
|
|
|
const cell = page.getByRole('gridcell', { name: 'keep this value' })
|
|
await cell.dblclick()
|
|
const editor = page.getByRole('textbox', { name: /Editor content/ })
|
|
await editor.fill('should be cancelled')
|
|
await page.keyboard.press('Enter')
|
|
|
|
await expect(page.getByText('1 pending change')).toBeVisible()
|
|
|
|
await clickDiscard(page)
|
|
|
|
const confirmDialog = page.getByRole('alertdialog')
|
|
await expect(confirmDialog.getByRole('heading', { name: 'Unsaved changes' })).toBeVisible()
|
|
await confirmDialog.getByRole('button', { name: 'Discard changes' }).click()
|
|
|
|
await expect(page.getByRole('gridcell', { name: 'keep this value' })).toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'should be cancelled' })).not.toBeVisible()
|
|
|
|
await expect(page.getByText('pending change')).not.toBeVisible()
|
|
})
|
|
|
|
test('revert can be cancelled via discard confirmation dialog', async ({ page, ref }) => {
|
|
const tableName = `${tableNamePrefix}_discard_cancel`
|
|
const columnName = 'name'
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createTable(tableName, columnName, [{ name: 'keep this value' }])
|
|
},
|
|
async () => {
|
|
await dropTable(tableName)
|
|
}
|
|
)
|
|
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await enableQueueOperations(page)
|
|
const tableLoadWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=entity-types-public-'
|
|
)
|
|
await page.reload()
|
|
await tableLoadWait
|
|
|
|
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
|
|
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
|
|
const cell = page.getByRole('gridcell', { name: 'keep this value' })
|
|
await cell.dblclick()
|
|
const editor = page.getByRole('textbox', { name: /Editor content/ })
|
|
await editor.fill('edited value')
|
|
await page.keyboard.press('Enter')
|
|
|
|
await expect(page.getByText('1 pending change')).toBeVisible()
|
|
|
|
await clickDiscard(page)
|
|
|
|
const confirmDialog = page.getByRole('alertdialog')
|
|
await expect(confirmDialog.getByRole('heading', { name: 'Unsaved changes' })).toBeVisible()
|
|
await confirmDialog.getByRole('button', { name: 'Keep editing' }).click()
|
|
|
|
await expect(page.getByText('1 pending change')).toBeVisible()
|
|
})
|
|
|
|
test('row inserts are queued and can be saved', async ({ page, ref }) => {
|
|
const tableName = `${tableNamePrefix}_row_insert`
|
|
const columnName = 'name'
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createTable(tableName, columnName)
|
|
},
|
|
async () => {
|
|
await dropTable(tableName)
|
|
}
|
|
)
|
|
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await enableQueueOperations(page)
|
|
const tableLoadWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=entity-types-public-'
|
|
)
|
|
await page.reload()
|
|
await tableLoadWait
|
|
|
|
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
|
|
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
|
|
await page.getByTestId('table-editor-insert-new-row').click()
|
|
await page.getByRole('menuitem', { name: 'Insert row' }).click()
|
|
await page.getByTestId(`${columnName}-input`).fill('new row value')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
await expect(page.getByText('1 pending change')).toBeVisible()
|
|
|
|
await expect(page.getByRole('gridcell', { name: 'new row value' })).toBeVisible()
|
|
|
|
await clickReview(page)
|
|
|
|
const sidePanel = page.getByRole('dialog')
|
|
await expect(sidePanel.getByText('Pending changes')).toBeVisible()
|
|
await expect(sidePanel.getByText('1 row addition')).toBeVisible()
|
|
await expect(sidePanel.getByText('New row', { exact: true })).toBeVisible()
|
|
|
|
await sidePanel.getByRole('button', { name: /^Save/ }).click()
|
|
await expect(page.getByText('Changes saved successfully')).toBeVisible()
|
|
|
|
await expect(page.getByRole('gridcell', { name: 'new row value' })).toBeVisible()
|
|
})
|
|
|
|
test('multiple operations can be batched and saved together', async ({ page, ref }) => {
|
|
const tableName = `${tableNamePrefix}_batch_ops`
|
|
const columnName = 'name'
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createTable(tableName, columnName)
|
|
},
|
|
async () => {
|
|
await dropTable(tableName)
|
|
}
|
|
)
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await enableQueueOperations(page)
|
|
const tableLoadWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=entity-types-public-'
|
|
)
|
|
await page.reload()
|
|
await tableLoadWait
|
|
|
|
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
|
|
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
|
|
await page.getByTestId('table-editor-insert-new-row').click()
|
|
await page.getByRole('menuitem', { name: 'Insert row' }).click()
|
|
await page.getByTestId(`${columnName}-input`).fill('row one')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
await page.getByTestId('table-editor-insert-new-row').click()
|
|
await page.getByRole('menuitem', { name: 'Insert row' }).click()
|
|
await page.getByTestId(`${columnName}-input`).fill('row two')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
await expect(page.getByText('2 pending changes')).toBeVisible()
|
|
|
|
await clickReview(page)
|
|
|
|
const sidePanel = page.getByRole('dialog')
|
|
await expect(sidePanel.getByText('2 operations')).toBeVisible()
|
|
await expect(sidePanel.getByText('2 row additions')).toBeVisible()
|
|
|
|
await sidePanel.getByRole('button', { name: /^Save/ }).click()
|
|
await expect(page.getByText('Changes saved successfully')).toBeVisible()
|
|
|
|
await expect(page.getByRole('gridcell', { name: 'row one' })).toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'row two' })).toBeVisible()
|
|
})
|
|
|
|
test('individual operations can be removed from the queue', async ({ page, ref }) => {
|
|
const tableName = `${tableNamePrefix}_remove_op`
|
|
const columnName = 'name'
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createTable(tableName, columnName)
|
|
},
|
|
async () => {
|
|
await dropTable(tableName)
|
|
}
|
|
)
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await enableQueueOperations(page)
|
|
const tableLoadWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=entity-types-public-'
|
|
)
|
|
await page.reload()
|
|
await tableLoadWait
|
|
|
|
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
|
|
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
|
|
await page.getByTestId('table-editor-insert-new-row').click()
|
|
await page.getByRole('menuitem', { name: 'Insert row' }).click()
|
|
await page.getByTestId(`${columnName}-input`).fill('keep this row')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
await page.getByTestId('table-editor-insert-new-row').click()
|
|
await page.getByRole('menuitem', { name: 'Insert row' }).click()
|
|
await page.getByTestId(`${columnName}-input`).fill('remove this row')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
await expect(page.getByText('2 pending changes')).toBeVisible()
|
|
|
|
await clickReview(page)
|
|
|
|
const sidePanel = page.getByRole('dialog')
|
|
const removeButtons = sidePanel.getByRole('button', { name: 'Discard change' })
|
|
await removeButtons.last().click()
|
|
|
|
await expect(sidePanel.getByText('1 operation')).toBeVisible()
|
|
|
|
await sidePanel.getByRole('button', { name: /^Save/ }).click()
|
|
await expect(page.getByText('Changes saved successfully')).toBeVisible()
|
|
|
|
await expect(page.getByRole('gridcell', { name: 'keep this row' })).toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'remove this row' })).not.toBeVisible()
|
|
})
|
|
|
|
test('keyboard shortcuts work for queue operations', async ({ page, ref }) => {
|
|
const tableName = `${tableNamePrefix}_shortcuts`
|
|
const columnName = 'name'
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createTable(tableName, columnName)
|
|
},
|
|
async () => {
|
|
await dropTable(tableName)
|
|
}
|
|
)
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await enableQueueOperations(page)
|
|
const tableLoadWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=entity-types-public-'
|
|
)
|
|
await page.reload()
|
|
await tableLoadWait
|
|
|
|
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
|
|
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
|
|
await page.getByTestId('table-editor-insert-new-row').click()
|
|
await page.getByRole('menuitem', { name: 'Insert row' }).click()
|
|
await page.getByTestId(`${columnName}-input`).fill('shortcut test')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
await expect(page.getByText('1 pending change')).toBeVisible()
|
|
|
|
await page.keyboard.press('ControlOrMeta+.')
|
|
await expect(page.getByRole('dialog').getByText('Pending changes')).toBeVisible()
|
|
|
|
await page.keyboard.press('ControlOrMeta+.')
|
|
await expect(page.getByRole('dialog')).not.toBeVisible()
|
|
await expect(page.getByText('pending change')).toBeVisible()
|
|
|
|
await page.keyboard.press('ControlOrMeta+s')
|
|
await expect(page.getByText('Changes saved successfully')).toBeVisible()
|
|
|
|
await expect(page.getByRole('gridcell', { name: 'shortcut test' })).toBeVisible()
|
|
})
|
|
|
|
test('undo reverts the latest operation from the queue', async ({ page, ref }) => {
|
|
const tableName = `${tableNamePrefix}_undo`
|
|
const columnName = 'name'
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createTable(tableName, columnName, [{ name: 'existing row' }])
|
|
},
|
|
async () => {
|
|
await dropTable(tableName)
|
|
}
|
|
)
|
|
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await enableQueueOperations(page)
|
|
const tableLoadWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=entity-types-public-'
|
|
)
|
|
await page.reload()
|
|
await tableLoadWait
|
|
|
|
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
|
|
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
|
|
await expect(page.getByRole('gridcell', { name: 'existing row' })).toBeVisible()
|
|
|
|
await page.getByTestId('table-editor-insert-new-row').click()
|
|
await page.getByRole('menuitem', { name: 'Insert row' }).click()
|
|
await page.getByTestId(`${columnName}-input`).fill('undo this row')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
await expect(page.getByText('1 pending change')).toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'undo this row' })).toBeVisible()
|
|
|
|
await page.keyboard.press('ControlOrMeta+z')
|
|
|
|
await expect(page.getByText('pending change')).not.toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'undo this row' })).not.toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'existing row' })).toBeVisible()
|
|
})
|
|
|
|
test('undo reverts operations one at a time', async ({ page, ref }) => {
|
|
const tableName = `${tableNamePrefix}_undo_multi`
|
|
const columnName = 'name'
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createTable(tableName, columnName)
|
|
},
|
|
async () => {
|
|
await dropTable(tableName)
|
|
}
|
|
)
|
|
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await enableQueueOperations(page)
|
|
const tableLoadWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=entity-types-public-'
|
|
)
|
|
await page.reload()
|
|
await tableLoadWait
|
|
|
|
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
|
|
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
|
|
await page.getByTestId('table-editor-insert-new-row').click()
|
|
await page.getByRole('menuitem', { name: 'Insert row' }).click()
|
|
await page.getByTestId(`${columnName}-input`).fill('first row')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
await page.getByTestId('table-editor-insert-new-row').click()
|
|
await page.getByRole('menuitem', { name: 'Insert row' }).click()
|
|
await page.getByTestId(`${columnName}-input`).fill('second row')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
await expect(page.getByText('2 pending changes')).toBeVisible()
|
|
|
|
await page.keyboard.press('ControlOrMeta+z')
|
|
|
|
await expect(page.getByText('1 pending change')).toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'first row' })).toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'second row' })).not.toBeVisible()
|
|
|
|
await page.keyboard.press('ControlOrMeta+z')
|
|
|
|
await expect(page.getByText('pending change')).not.toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'first row' })).not.toBeVisible()
|
|
})
|
|
|
|
test('undo works for cell edits', async ({ page, ref }) => {
|
|
const tableName = `${tableNamePrefix}_undo_edit`
|
|
const columnName = 'name'
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createTable(tableName, columnName, [{ name: 'original value' }])
|
|
},
|
|
async () => {
|
|
await dropTable(tableName)
|
|
}
|
|
)
|
|
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await enableQueueOperations(page)
|
|
const tableLoadWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=entity-types-public-'
|
|
)
|
|
await page.reload()
|
|
await tableLoadWait
|
|
|
|
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
|
|
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
|
|
await expect(page.getByRole('gridcell', { name: 'original value' })).toBeVisible()
|
|
|
|
const cell = page.getByRole('gridcell', { name: 'original value' })
|
|
await cell.dblclick()
|
|
|
|
const editor = page.getByRole('textbox', { name: /Editor content/ })
|
|
await expect(editor).toBeVisible()
|
|
await editor.fill('edited value')
|
|
await page.keyboard.press('Enter')
|
|
|
|
await expect(page.getByText('1 pending change')).toBeVisible()
|
|
|
|
await page.keyboard.press('ControlOrMeta+z')
|
|
|
|
await expect(page.getByText('pending change')).not.toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'original value' })).toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'edited value' })).not.toBeVisible()
|
|
})
|
|
|
|
test('undo works for row deletes', async ({ page, ref }) => {
|
|
const tableName = `${tableNamePrefix}_undo_del`
|
|
const columnName = 'name'
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createTable(tableName, columnName, [{ name: 'row to keep' }])
|
|
},
|
|
async () => {
|
|
await dropTable(tableName)
|
|
}
|
|
)
|
|
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await enableQueueOperations(page)
|
|
const tableLoadWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=entity-types-public-'
|
|
)
|
|
await page.reload()
|
|
await tableLoadWait
|
|
|
|
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
|
|
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
|
|
await expect(page.getByRole('gridcell', { name: 'row to keep' })).toBeVisible()
|
|
|
|
const cell = page.getByRole('gridcell', { name: 'row to keep' })
|
|
await cell.click({ button: 'right' })
|
|
await page.getByRole('menuitem', { name: 'Delete row' }).click()
|
|
|
|
await expect(page.getByText('1 pending change')).toBeVisible()
|
|
|
|
await page.keyboard.press('ControlOrMeta+z')
|
|
|
|
await expect(page.getByText('pending change')).not.toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'row to keep' })).toBeVisible()
|
|
})
|
|
|
|
test('row deletes via context menu are queued', async ({ page, ref }) => {
|
|
const tableName = `${tableNamePrefix}_row_delete`
|
|
const columnName = 'name'
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createTable(tableName, columnName, [{ name: 'row to delete' }])
|
|
},
|
|
async () => {
|
|
await dropTable(tableName)
|
|
}
|
|
)
|
|
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await enableQueueOperations(page)
|
|
const tableLoadWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=entity-types-public-'
|
|
)
|
|
await page.reload()
|
|
await tableLoadWait
|
|
|
|
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
|
|
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
|
|
await expect(page.getByRole('gridcell', { name: 'row to delete' })).toBeVisible()
|
|
|
|
const cell = page.getByRole('gridcell', { name: 'row to delete' })
|
|
await cell.click({ button: 'right' })
|
|
|
|
await page.getByRole('menuitem', { name: 'Delete row' }).click()
|
|
|
|
await expect(page.getByText('1 pending change')).toBeVisible()
|
|
|
|
await clickReview(page)
|
|
|
|
const sidePanel = page.getByRole('dialog')
|
|
await expect(sidePanel.getByText('Pending changes')).toBeVisible()
|
|
await expect(sidePanel.getByText('1 row deletion')).toBeVisible()
|
|
await expect(sidePanel.getByText('Delete row', { exact: true })).toBeVisible()
|
|
|
|
await sidePanel.getByRole('button', { name: /^Save/ }).click()
|
|
await expect(page.getByText('Changes saved successfully')).toBeVisible()
|
|
|
|
await expect(page.getByRole('gridcell', { name: 'row to delete' })).not.toBeVisible()
|
|
await expect(page.getByText('0 records')).toBeVisible()
|
|
})
|
|
|
|
test('row deletes can be cancelled', async ({ page, ref }) => {
|
|
const tableName = `${tableNamePrefix}_delete_cancel`
|
|
const columnName = 'name'
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createTable(tableName, columnName, [{ name: 'should not be deleted' }])
|
|
},
|
|
async () => {
|
|
await dropTable(tableName)
|
|
}
|
|
)
|
|
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await enableQueueOperations(page)
|
|
const tableLoadWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=entity-types-public-'
|
|
)
|
|
await page.reload()
|
|
await tableLoadWait
|
|
|
|
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
|
|
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
|
|
const cell = page.getByRole('gridcell', { name: 'should not be deleted' })
|
|
await cell.click({ button: 'right' })
|
|
await page.getByRole('menuitem', { name: 'Delete row' }).click()
|
|
|
|
await expect(page.getByText('1 pending change')).toBeVisible()
|
|
|
|
await clickDiscard(page)
|
|
|
|
const confirmDialog = page.getByRole('alertdialog')
|
|
await expect(confirmDialog.getByRole('heading', { name: 'Unsaved changes' })).toBeVisible()
|
|
await confirmDialog.getByRole('button', { name: 'Discard changes' }).click()
|
|
|
|
await expect(page.getByRole('gridcell', { name: 'should not be deleted' })).toBeVisible()
|
|
await expect(page.getByText('pending change')).not.toBeVisible()
|
|
})
|
|
|
|
test('mixed operations (add, edit, delete) can be batched', async ({ page, ref }) => {
|
|
const tableName = `${tableNamePrefix}_mixed_ops`
|
|
const columnName = 'name'
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createTable(tableName, columnName, [
|
|
{ name: 'row to edit' },
|
|
{ name: 'row to delete' },
|
|
])
|
|
},
|
|
async () => {
|
|
await dropTable(tableName)
|
|
}
|
|
)
|
|
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await enableQueueOperations(page)
|
|
const tableLoadWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=entity-types-public-'
|
|
)
|
|
await page.reload()
|
|
await tableLoadWait
|
|
|
|
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
|
|
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
|
|
const cellToEdit = page.getByRole('gridcell', { name: 'row to edit' })
|
|
await cellToEdit.dblclick()
|
|
const editor = page.getByRole('textbox', { name: /Editor content/ })
|
|
await editor.fill('edited row')
|
|
await page.keyboard.press('Enter')
|
|
|
|
const cellToDelete = page.getByRole('gridcell', { name: 'row to delete' })
|
|
await cellToDelete.click({ button: 'right' })
|
|
await page.getByRole('menuitem', { name: 'Delete row' }).click()
|
|
|
|
await page.getByTestId('table-editor-insert-new-row').click()
|
|
await page.getByRole('menuitem', { name: 'Insert row' }).click()
|
|
await page.getByTestId(`${columnName}-input`).fill('new row')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
await expect(page.getByText('3 pending changes')).toBeVisible()
|
|
|
|
await clickReview(page)
|
|
const sidePanel = page.getByRole('dialog')
|
|
await expect(sidePanel.getByText('3 operations')).toBeVisible()
|
|
await expect(sidePanel.getByText('1 row deletion')).toBeVisible()
|
|
await expect(sidePanel.getByText('1 row addition')).toBeVisible()
|
|
await expect(sidePanel.getByText('1 cell edit')).toBeVisible()
|
|
|
|
await sidePanel.getByRole('button', { name: /^Save/ }).click()
|
|
await expect(page.getByText('Changes saved successfully')).toBeVisible()
|
|
|
|
await expect(page.getByRole('gridcell', { name: 'edited row' })).toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'new row' })).toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'row to delete' })).not.toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'row to edit' })).not.toBeVisible()
|
|
})
|
|
|
|
test('newly added row is preserved after deleting another row', async ({ page, ref }) => {
|
|
const tableName = `${tableNamePrefix}_add_then_del`
|
|
const columnName = 'name'
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createTable(tableName, columnName, [{ name: 'existing row' }])
|
|
},
|
|
async () => {
|
|
await dropTable(tableName)
|
|
}
|
|
)
|
|
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await enableQueueOperations(page)
|
|
const tableLoadWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=entity-types-public-'
|
|
)
|
|
await page.reload()
|
|
await tableLoadWait
|
|
|
|
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
|
|
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
|
|
// Add a new row
|
|
await page.getByTestId('table-editor-insert-new-row').click()
|
|
await page.getByRole('menuitem', { name: 'Insert row' }).click()
|
|
await page.getByTestId(`${columnName}-input`).fill('new row')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
await expect(page.getByText('1 pending change')).toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'new row' })).toBeVisible()
|
|
|
|
// Delete the existing row
|
|
const cellToDelete = page.getByRole('gridcell', { name: 'existing row' })
|
|
await cellToDelete.click({ button: 'right' })
|
|
await page.getByRole('menuitem', { name: 'Delete row' }).click()
|
|
|
|
await expect(page.getByText('2 pending changes')).toBeVisible()
|
|
|
|
// The newly added row should still be visible and not replaced
|
|
await expect(page.getByRole('gridcell', { name: 'new row' })).toBeVisible()
|
|
|
|
// Save and verify
|
|
await clickReview(page)
|
|
const sidePanel = page.getByRole('dialog')
|
|
await expect(sidePanel.getByText('1 row addition')).toBeVisible()
|
|
await expect(sidePanel.getByText('1 row deletion')).toBeVisible()
|
|
|
|
await sidePanel.getByRole('button', { name: /^Save/ }).click()
|
|
await expect(page.getByText('Changes saved successfully')).toBeVisible()
|
|
|
|
await expect(page.getByRole('gridcell', { name: 'new row' })).toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'existing row' })).not.toBeVisible()
|
|
})
|
|
|
|
test('pending changes persist when switching between tables', async ({ page, ref }) => {
|
|
const tableName1 = `${tableNamePrefix}_persist1`
|
|
const tableName2 = `${tableNamePrefix}_persist2`
|
|
const columnName = 'name'
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createTable(tableName1, columnName)
|
|
await createTable(tableName2, columnName)
|
|
},
|
|
async () => {
|
|
await dropTable(tableName1)
|
|
await dropTable(tableName2)
|
|
}
|
|
)
|
|
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await enableQueueOperations(page)
|
|
const tableLoadWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=entity-types-public-'
|
|
)
|
|
await page.reload()
|
|
await tableLoadWait
|
|
|
|
await page.getByRole('button', { name: `View ${tableName1}`, exact: true }).click()
|
|
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
|
|
await page.getByTestId('table-editor-insert-new-row').click()
|
|
await page.getByRole('menuitem', { name: 'Insert row' }).click()
|
|
await page.getByTestId(`${columnName}-input`).fill('pending in table 1')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
await expect(page.getByText('1 pending change')).toBeVisible()
|
|
|
|
await page.getByRole('button', { name: `View ${tableName2}`, exact: true }).click()
|
|
|
|
await page.getByTestId('table-editor-insert-new-row').click()
|
|
await page.getByRole('menuitem', { name: 'Insert row' }).click()
|
|
await page.getByTestId(`${columnName}-input`).fill('pending in table 2')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
await expect(page.getByText('2 pending changes')).toBeVisible()
|
|
|
|
await clickReview(page)
|
|
const sidePanel = page.getByRole('dialog')
|
|
await expect(sidePanel.getByText('2 operations')).toBeVisible()
|
|
|
|
await sidePanel.getByRole('button', { name: /^Save/ }).click()
|
|
await expect(page.getByText('Changes saved successfully')).toBeVisible()
|
|
|
|
await page.getByRole('button', { name: `View ${tableName1}`, exact: true }).click()
|
|
await expect(page.getByRole('gridcell', { name: 'pending in table 1' })).toBeVisible()
|
|
|
|
await page.getByRole('button', { name: `View ${tableName2}`, exact: true }).click()
|
|
await expect(page.getByRole('gridcell', { name: 'pending in table 2' })).toBeVisible()
|
|
})
|
|
|
|
test('pending row changes do not leak across tables', async ({ page, ref }) => {
|
|
const tableName1 = `${tableNamePrefix}_leak1`
|
|
const tableName2 = `${tableNamePrefix}_leak2`
|
|
const columnName = 'name'
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createTable(tableName1, columnName)
|
|
await createTable(tableName2, columnName)
|
|
},
|
|
async () => {
|
|
await dropTable(tableName1)
|
|
await dropTable(tableName2)
|
|
}
|
|
)
|
|
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await enableQueueOperations(page)
|
|
const tableLoadWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=entity-types-public-'
|
|
)
|
|
await page.reload()
|
|
await tableLoadWait
|
|
|
|
await page.getByRole('button', { name: `View ${tableName1}`, exact: true }).click()
|
|
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
|
|
await page.getByTestId('table-editor-insert-new-row').click()
|
|
await page.getByRole('menuitem', { name: 'Insert row' }).click()
|
|
await page.getByTestId(`${columnName}-input`).fill('only in table 1')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
await expect(page.getByText('1 pending change')).toBeVisible()
|
|
|
|
// Navigate to table 2 — pending row from table 1 should not appear
|
|
await page.getByRole('button', { name: `View ${tableName2}`, exact: true }).click()
|
|
await expect(page.getByRole('gridcell', { name: 'only in table 1' })).not.toBeVisible()
|
|
|
|
// Add a row in table 2
|
|
await page.getByTestId('table-editor-insert-new-row').click()
|
|
await page.getByRole('menuitem', { name: 'Insert row' }).click()
|
|
await page.getByTestId(`${columnName}-input`).fill('only in table 2')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
// Navigate back to table 1 — pending row from table 2 should not appear
|
|
await page.getByRole('button', { name: `View ${tableName1}`, exact: true }).click()
|
|
await expect(page.getByRole('gridcell', { name: 'only in table 1' })).toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'only in table 2' })).not.toBeVisible()
|
|
})
|
|
|
|
test('reverted cell edit clears the pending change', async ({ page, ref }) => {
|
|
const tableName = `${tableNamePrefix}_revert_edit`
|
|
const columnName = 'name'
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createTable(tableName, columnName, [{ name: 'original value' }])
|
|
},
|
|
async () => {
|
|
await dropTable(tableName)
|
|
}
|
|
)
|
|
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await enableQueueOperations(page)
|
|
const tableLoadWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=entity-types-public-'
|
|
)
|
|
await page.reload()
|
|
await tableLoadWait
|
|
|
|
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
|
|
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
|
|
await expect(page.getByRole('gridcell', { name: 'original value' })).toBeVisible()
|
|
|
|
// Edit the cell to a different value
|
|
const cell = page.getByRole('gridcell', { name: 'original value' })
|
|
await cell.dblclick()
|
|
|
|
const editor = page.getByRole('textbox', { name: /Editor content/ })
|
|
await expect(editor).toBeVisible()
|
|
await editor.fill('changed value')
|
|
await page.keyboard.press('Enter')
|
|
|
|
await expect(page.getByText('1 pending change')).toBeVisible()
|
|
|
|
// Edit the cell back to the original value
|
|
const changedCell = page.getByRole('gridcell', { name: 'changed value' })
|
|
await changedCell.dblclick()
|
|
|
|
const editor2 = page.getByRole('textbox', { name: /Editor content/ })
|
|
await expect(editor2).toBeVisible()
|
|
await editor2.fill('original value')
|
|
await page.keyboard.press('Enter')
|
|
|
|
// The pending change should be cleared since the value was reverted
|
|
await expect(page.getByText('pending change')).not.toBeVisible()
|
|
})
|
|
|
|
test('editing multiple columns via side panel queues all changes', async ({ page, ref }) => {
|
|
const tableName = `${tableNamePrefix}_multi_col`
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await query(
|
|
`CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
id bigint generated by default as identity primary key,
|
|
created_at timestamp with time zone null default now(),
|
|
first_name text,
|
|
last_name text
|
|
)`
|
|
)
|
|
await query(`INSERT INTO ${tableName} (first_name, last_name) VALUES ($1, $2)`, [
|
|
'Alice',
|
|
'Smith',
|
|
])
|
|
},
|
|
async () => {
|
|
await dropTable(tableName)
|
|
}
|
|
)
|
|
|
|
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
|
await enableQueueOperations(page)
|
|
const tableLoadWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=entity-types-public-'
|
|
)
|
|
await page.reload()
|
|
await tableLoadWait
|
|
|
|
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
|
|
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
|
|
|
|
await expect(page.getByRole('gridcell', { name: 'Alice' })).toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'Smith' })).toBeVisible()
|
|
|
|
// Right-click to open context menu and edit the row
|
|
const cell = page.getByRole('gridcell', { name: 'Alice' })
|
|
await cell.click({ button: 'right' })
|
|
await page.getByRole('menuitem', { name: 'Edit row' }).click()
|
|
|
|
// Update both columns in the side panel
|
|
const firstNameInput = page.getByTestId('first_name-input')
|
|
await expect(firstNameInput).toBeVisible()
|
|
await firstNameInput.clear()
|
|
await firstNameInput.fill('Bob')
|
|
|
|
const lastNameInput = page.getByTestId('last_name-input')
|
|
await lastNameInput.clear()
|
|
await lastNameInput.fill('Jones')
|
|
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
// Should queue 2 cell edits (one per changed column)
|
|
await expect(page.getByText('2 pending changes')).toBeVisible()
|
|
|
|
// Both values should be optimistically updated in the grid
|
|
await expect(page.getByRole('gridcell', { name: 'Bob' })).toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'Jones' })).toBeVisible()
|
|
|
|
// Review the queued operations
|
|
await clickReview(page)
|
|
|
|
const sidePanel = page.getByRole('dialog')
|
|
await expect(sidePanel.getByText('2 cell edits')).toBeVisible()
|
|
|
|
// Save all changes
|
|
await sidePanel.getByRole('button', { name: /^Save/ }).click()
|
|
await expect(page.getByText('Changes saved successfully')).toBeVisible()
|
|
|
|
// Both columns should reflect the saved values
|
|
await expect(page.getByRole('gridcell', { name: 'Bob' })).toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'Jones' })).toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'Alice' })).not.toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'Smith' })).not.toBeVisible()
|
|
})
|
|
})
|
|
|
|
test.describe('Queue Table Operations - queue identity fixes', () => {
|
|
test('primary-key and column edits to the same row are saved together', async ({ page, ref }) => {
|
|
const tableName = `${tableNamePrefix}_pk_col_edit`
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await dropTable(tableName)
|
|
await query(
|
|
`CREATE TABLE ${tableName} (
|
|
id bigint primary key,
|
|
name text
|
|
)`
|
|
)
|
|
await query(`INSERT INTO ${tableName} (id, name) VALUES ($1, $2)`, [
|
|
13001,
|
|
'original pk row',
|
|
])
|
|
},
|
|
async () => {
|
|
await dropTable(tableName)
|
|
}
|
|
)
|
|
|
|
await openTableWithQueueOperations(page, ref, tableName)
|
|
|
|
const originalNameCell = page.getByRole('gridcell', { name: 'original pk row' })
|
|
await expect(
|
|
originalNameCell,
|
|
'Original row should be visible before editing through the row editor'
|
|
).toBeVisible()
|
|
|
|
await originalNameCell.click({ button: 'right' })
|
|
await page.getByRole('menuitem', { name: 'Edit row' }).click()
|
|
|
|
const rowEditor = page.getByTestId('side-panel-row-editor')
|
|
await expect(rowEditor, 'Row editor should open for the selected row').toBeVisible()
|
|
|
|
await rowEditor.getByTestId('id-input').fill('13002')
|
|
await rowEditor.getByTestId('name-input').fill('updated pk row')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
await expect(
|
|
page.getByText('2 pending changes'),
|
|
'Editing the primary key and name should queue two cell edits'
|
|
).toBeVisible()
|
|
await expect(
|
|
page.getByRole('gridcell', { name: '13002' }),
|
|
'Optimistic grid state should show the edited primary key'
|
|
).toBeVisible()
|
|
await expect(
|
|
page.getByRole('gridcell', { name: 'updated pk row' }),
|
|
'Optimistic grid state should show the edited name'
|
|
).toBeVisible()
|
|
|
|
await clickReview(page)
|
|
|
|
const sidePanel = page.getByRole('dialog')
|
|
await expect(sidePanel.getByText('Pending changes')).toBeVisible()
|
|
await expect(
|
|
sidePanel.getByText('2 cell edits'),
|
|
'Both queued edits should still be reviewed as cell edits'
|
|
).toBeVisible()
|
|
|
|
const saveWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=operation-queue-save',
|
|
{
|
|
method: 'POST',
|
|
}
|
|
)
|
|
await sidePanel.getByRole('button', { name: /^Save/ }).click()
|
|
await saveWait
|
|
await expect(
|
|
page.getByText('Changes saved successfully'),
|
|
'Queue save should complete successfully'
|
|
).toBeVisible({ timeout: 15000 })
|
|
|
|
const rows = await query<{ id: string; name: string }>(
|
|
`SELECT id::text AS id, name FROM ${tableName} ORDER BY id`
|
|
)
|
|
expect(rows).toEqual([{ id: '13002', name: 'updated pk row' }])
|
|
|
|
await expect(page.getByRole('gridcell', { name: 'updated pk row' })).toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: 'original pk row' })).not.toBeVisible()
|
|
})
|
|
|
|
test('primary-key edits can be reverted before saving', async ({ page, ref }) => {
|
|
const tableName = `${tableNamePrefix}_pk_revert`
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await dropTable(tableName)
|
|
await query(
|
|
`CREATE TABLE ${tableName} (
|
|
id bigint primary key,
|
|
name text
|
|
)`
|
|
)
|
|
await query(`INSERT INTO ${tableName} (id, name) VALUES ($1, $2)`, [14001, 'pk revert row'])
|
|
},
|
|
async () => {
|
|
await dropTable(tableName)
|
|
}
|
|
)
|
|
|
|
await openTableWithQueueOperations(page, ref, tableName)
|
|
|
|
const originalIdCell = page.getByRole('gridcell', { name: '14001', exact: true })
|
|
await expect(
|
|
originalIdCell,
|
|
'Original primary key should be visible before editing'
|
|
).toBeVisible()
|
|
|
|
await originalIdCell.click({ button: 'right' })
|
|
await page.getByRole('menuitem', { name: 'Edit row' }).click()
|
|
|
|
const rowEditor = page.getByTestId('side-panel-row-editor')
|
|
await expect(rowEditor, 'Row editor should open for the selected row').toBeVisible()
|
|
await rowEditor.getByTestId('id-input').fill('14002')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
await expect(
|
|
page.getByText('1 pending change'),
|
|
'Changing a primary-key cell should queue one pending edit'
|
|
).toBeVisible()
|
|
await expect(
|
|
page.getByRole('gridcell', { name: '14002', exact: true }),
|
|
'Optimistic grid state should show the changed primary key'
|
|
).toBeVisible()
|
|
|
|
await page.getByRole('gridcell', { name: '14002', exact: true }).click({ button: 'right' })
|
|
await page.getByRole('menuitem', { name: 'Edit row' }).click()
|
|
|
|
const revertedRowEditor = page.getByTestId('side-panel-row-editor')
|
|
await expect(
|
|
revertedRowEditor,
|
|
'Row editor should reopen after the primary key changed'
|
|
).toBeVisible()
|
|
await revertedRowEditor.getByTestId('id-input').fill('14001')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
await expect(
|
|
page.getByText('pending change'),
|
|
'Reverting the primary key to its original value should clear the queued edit'
|
|
).not.toBeVisible()
|
|
await expect(page.getByRole('gridcell', { name: '14001', exact: true })).toBeVisible()
|
|
})
|
|
|
|
test('primary-key edits keep rows distinct when another row takes the old key', async ({
|
|
page,
|
|
ref,
|
|
}) => {
|
|
const tableName = `${tableNamePrefix}_pk_take_old`
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await dropTable(tableName)
|
|
await query(
|
|
`CREATE TABLE ${tableName} (
|
|
id bigint primary key,
|
|
name text
|
|
)`
|
|
)
|
|
await query(`INSERT INTO ${tableName} (id, name) VALUES ($1, $2), ($3, $4)`, [
|
|
15001,
|
|
'Alice keeps identity',
|
|
15002,
|
|
'Bob takes old key',
|
|
])
|
|
},
|
|
async () => {
|
|
await dropTable(tableName)
|
|
}
|
|
)
|
|
|
|
await openTableWithQueueOperations(page, ref, tableName)
|
|
|
|
const aliceCell = page.getByRole('gridcell', { name: 'Alice keeps identity' })
|
|
await expect(aliceCell, 'Alice row should be visible before editing').toBeVisible()
|
|
await aliceCell.click({ button: 'right' })
|
|
await page.getByRole('menuitem', { name: 'Edit row' }).click()
|
|
|
|
const aliceEditor = page.getByTestId('side-panel-row-editor')
|
|
await expect(aliceEditor, 'Row editor should open for Alice').toBeVisible()
|
|
await aliceEditor.getByTestId('id-input').fill('15003')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
await expect(
|
|
page.getByText('1 pending change'),
|
|
'Alice primary-key edit should queue one pending change'
|
|
).toBeVisible()
|
|
await expect(
|
|
page.getByRole('gridcell', { name: '15003', exact: true }),
|
|
'Alice row should show the updated primary key in the grid'
|
|
).toBeVisible()
|
|
|
|
const bobCell = page.getByRole('gridcell', { name: 'Bob takes old key' })
|
|
await expect(
|
|
bobCell,
|
|
'Bob row should remain visible after Alice primary-key edit'
|
|
).toBeVisible()
|
|
await bobCell.click({ button: 'right' })
|
|
await page.getByRole('menuitem', { name: 'Edit row' }).click()
|
|
|
|
const bobEditor = page.getByTestId('side-panel-row-editor')
|
|
await expect(bobEditor, 'Row editor should open for Bob').toBeVisible()
|
|
await bobEditor.getByTestId('id-input').fill('15001')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
await expect(
|
|
page.getByText('2 pending changes'),
|
|
'Both primary-key edits should stay queued against their original rows'
|
|
).toBeVisible()
|
|
|
|
await clickReview(page)
|
|
|
|
const sidePanel = page.getByRole('dialog')
|
|
await expect(
|
|
sidePanel.getByText('Pending changes'),
|
|
'Review panel should show pending changes before saving primary-key edits'
|
|
).toBeVisible()
|
|
await expect(
|
|
sidePanel.getByText('2 cell edits'),
|
|
'Review panel should show both primary-key edits as cell edits'
|
|
).toBeVisible()
|
|
|
|
const saveWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=operation-queue-save',
|
|
{
|
|
method: 'POST',
|
|
}
|
|
)
|
|
await sidePanel.getByRole('button', { name: /^Save/ }).click()
|
|
await saveWait
|
|
await expect(
|
|
page.getByText('Changes saved successfully'),
|
|
'Saving swapped primary-key edits should show a success toast'
|
|
).toBeVisible({ timeout: 15000 })
|
|
|
|
const rows = await query<{ id: string; name: string }>(
|
|
`SELECT id::text AS id, name FROM ${tableName} ORDER BY id`
|
|
)
|
|
expect(rows).toEqual([
|
|
{ id: '15001', name: 'Bob takes old key' },
|
|
{ id: '15003', name: 'Alice keeps identity' },
|
|
])
|
|
})
|
|
|
|
test('deleting a newly inserted row removes it from pending changes', async ({ page, ref }) => {
|
|
const tableName = `${tableNamePrefix}_pending_del`
|
|
const columnName = 'name'
|
|
|
|
await using _ = await withSetupCleanup(
|
|
async () => {
|
|
await createTable(tableName, columnName)
|
|
},
|
|
async () => {
|
|
await dropTable(tableName)
|
|
}
|
|
)
|
|
|
|
await openTableWithQueueOperations(page, ref, tableName)
|
|
|
|
await page.getByTestId('table-editor-insert-new-row').click()
|
|
await page.getByRole('menuitem', { name: 'Insert row' }).click()
|
|
await page.getByTestId(`${columnName}-input`).fill('keep pending row')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
await page.getByTestId('table-editor-insert-new-row').click()
|
|
await page.getByRole('menuitem', { name: 'Insert row' }).click()
|
|
await page.getByTestId(`${columnName}-input`).fill('delete pending row')
|
|
await page.getByTestId('action-bar-save-row').click()
|
|
|
|
await expect(
|
|
page.getByText('2 pending changes'),
|
|
'Adding two pending rows should queue two pending changes'
|
|
).toBeVisible()
|
|
await expect(
|
|
page.getByRole('gridcell', { name: 'keep pending row' }),
|
|
'First pending row should be visible before deleting the second row'
|
|
).toBeVisible()
|
|
await expect(
|
|
page.getByRole('gridcell', { name: 'delete pending row' }),
|
|
'Second pending row should be visible before deletion'
|
|
).toBeVisible()
|
|
|
|
await page.getByRole('gridcell', { name: 'delete pending row' }).click({ button: 'right' })
|
|
await page.getByRole('menuitem', { name: 'Delete row' }).click()
|
|
|
|
await expect(
|
|
page.getByText('1 pending change'),
|
|
'Deleting one pending insert should remove only that row from the queue'
|
|
).toBeVisible()
|
|
await expect(
|
|
page.getByRole('gridcell', { name: 'keep pending row' }),
|
|
'Remaining pending row should stay visible after deleting the other pending row'
|
|
).toBeVisible()
|
|
await expect(
|
|
page.getByRole('gridcell', { name: 'delete pending row' }),
|
|
'Deleted pending row should be removed from the grid'
|
|
).not.toBeVisible()
|
|
|
|
await clickReview(page)
|
|
|
|
const sidePanel = page.getByRole('dialog')
|
|
await expect(
|
|
sidePanel.getByText('Pending changes'),
|
|
'Review panel should show pending changes after deleting one pending row'
|
|
).toBeVisible()
|
|
await expect(
|
|
sidePanel.getByText('1 row addition'),
|
|
'Review panel should only show the remaining pending row addition'
|
|
).toBeVisible()
|
|
await expect(
|
|
sidePanel.getByText('delete pending row'),
|
|
'Review panel should not include the deleted pending row'
|
|
).not.toBeVisible()
|
|
|
|
const saveWait = createApiResponseWaiter(
|
|
page,
|
|
'pg-meta',
|
|
ref,
|
|
'query?key=operation-queue-save',
|
|
{
|
|
method: 'POST',
|
|
}
|
|
)
|
|
await sidePanel.getByRole('button', { name: /^Save/ }).click()
|
|
await saveWait
|
|
await expect(
|
|
page.getByText('Changes saved successfully'),
|
|
'Saving the remaining pending row should show a success toast'
|
|
).toBeVisible({ timeout: 15000 })
|
|
|
|
const rows = await query<{ name: string }>(`SELECT name FROM ${tableName} ORDER BY name`)
|
|
expect(rows).toEqual([{ name: 'keep pending row' }])
|
|
})
|
|
})
|