Files
supabase/e2e/studio/features/queue-table-operations.spec.ts
Alaister Young 5950b6ca04 test(e2e/studio): stabilize flaky and TZ/OS-sensitive specs (#46039)
Backports a batch of e2e test stabilization fixes — each commit is
scoped to a single failure class and only touches `e2e/studio/` files.

**Changed:**
- **`_global.setup` — playwright-locks cleanup was dead code**: the
lock-cleanup block was at the bottom of `Global Setup`, but every branch
above it returns early — so it never ran. Tests that use
`withFileOnceSetup` (cron-jobs) would see a stale `setup.done.json`
marker from the previous run and silently skip their setup, leaving e.g.
`pg_cron` uninstalled and all 11 cron-jobs specs failing. Moved the
cleanup to before any early return.
- **filter-bar — Home key**: macOS Chromium doesn't honor a standalone
`Home` keypress inside text inputs (macOS routes "go to line start" via
`Cmd+ArrowLeft` / `Fn+ArrowLeft`). Tests that expected the cursor to
jump to position 0 silently kept the previous selection. Replaced with
`el.setSelectionRange(0, 0)` so the assertion runs against a known
cursor position on every OS.
- **filter-bar — date filters**: tests inserted rows with `CURRENT_DATE`
/ `NOW()` (postgres session TZ = UTC) and asserted with JS-local dates
from `getDateValue()`. Near midnight the two diverged and the filter
returned 0 rows. Switched the inserts to explicit `getDateValue()`
strings so insert and assert use the same calendar day.
- **queue-table-operations — `networkidle`**: Studio holds long-poll /
SSE connections (PostHog, realtime), so `page.reload({ waitUntil:
'networkidle' })` never resolves and timed out. Replaced with a targeted
`waitForTableToLoad` API waiter.
- **sql-editor — RLS smoke test**: a hard-coded table name
(`pw_rls_smoke_test`) collided across 3 parallel workers running against
the same db. Suffixed with `test.info().parallelIndex`.
- **table-editor — FK spec timeout**: `waitForApiResponseWithTimeout`
for `query?key=table-update` returns `null` on timeout (silent), then
the panel-close assertion fails. Bumped 15s → 30s to absorb
parallel-load latency.
- **table-editor / storage-helpers — URL encoding & redirect race**:
post-action URL assertions were over-specific (`%20` vs `+` encoding)
and the bucket-delete redirect could race other history updates. Relaxed
the regex to accept both encodings; asserting the row removal directly
is a more stable signal than the redirect URL.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Tests**
* Improved end-to-end determinism with explicit dates/timestamps and
stable cursor positioning
* Prevented parallel-test collisions by using unique identifiers for
resources
* Made page reloads and API waits more robust for long-lived connections
and increased timeouts
* Strengthened assertions to rely on stable UI signals instead of
transient navigation/network state
* Ensured test setup reliably cleans up temporary locks before any setup
steps run

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46039?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
2026-05-18 17:31:40 +08:00

1031 lines
37 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' }).click()
await page.getByRole('menuitem', { name: itemName }).click()
}
const clickReview = async (page: Page) => openQueueDropdownAndClick(page, 'Review')
const clickDiscard = async (page: Page) => openQueueDropdownAndClick(page, 'Discard')
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()
})
})