Files
supabase/e2e/studio/features/table-editor.spec.ts
T
Danny White 1470c51021 fix(studio): align rls confirmation dialogs (#45961)
## What kind of change does this PR introduce?

Studio UI cleanup and docs update. Resolves FE-3217.

## What is the current behavior?

The table RLS confirmation dialogs differ across the Policies page and
Table Editor surfaces. The table editor disable flow also uses a verbose
confirmation modal with admonitions and repeated warning copy.

| Policies | Table Editor |
| --- | --- |
| <img width="852" height="448" alt="25125"
src="https://github.com/user-attachments/assets/baa9fafa-752c-4701-bf54-40da4e32030b"
/> | <img width="1108" height="1016" alt="57354"
src="https://github.com/user-attachments/assets/b1b88355-be39-49b9-aed2-a60f20c25104"
/> |
| <img width="866" height="434" alt="CleanShot 2026-05-15 at 18 01
15@2x"
src="https://github.com/user-attachments/assets/d67f69ee-0a49-4dbd-939e-ca968361fc33"
/> | <img width="840" height="426" alt="CleanShot 2026-05-15 at 18 01
52@2x"
src="https://github.com/user-attachments/assets/a0b5f390-abeb-453f-8636-ad097f22308b"
/> |

## What is the new behavior?

Table RLS enable and disable confirmations now use one shared concise
Alert Dialog treatment across the Policies page, Table Editor header,
and Table Editor side panel. The server-backed toggles use the async
Alert Dialog action behaviour from the stacked base branch, and the
design-system docs now clarify when to start with Alert Dialog versus
Confirmation Modal.

This PR is stacked on #45960.

| Before | After |
| --- | --- |
| <img width="852" height="448" alt="25125"
src="https://github.com/user-attachments/assets/baa9fafa-752c-4701-bf54-40da4e32030b"
/> | <img width="864" height="518" alt="CleanShot 2026-05-15 at 18 07
21@2x"
src="https://github.com/user-attachments/assets/bda53246-164b-4d5b-81a1-25d3bc661eb3"
/> |
| <img width="1108" height="1016" alt="57354"
src="https://github.com/user-attachments/assets/b1b88355-be39-49b9-aed2-a60f20c25104"
/> | <img width="864" height="518" alt="CleanShot 2026-05-15 at 18 07
21@2x"
src="https://github.com/user-attachments/assets/bda53246-164b-4d5b-81a1-25d3bc661eb3"
/> |
| <img width="866" height="434" alt="CleanShot 2026-05-15 at 18 01
15@2x"
src="https://github.com/user-attachments/assets/d67f69ee-0a49-4dbd-939e-ca968361fc33"
/> | <img width="860" height="488" alt="CleanShot 2026-05-15 at 18 08
41@2x"
src="https://github.com/user-attachments/assets/4c67f797-e2cd-4d56-a49d-e6c6fc0edff3"
/> |
| <img width="840" height="426" alt="CleanShot 2026-05-15 at 18 01
52@2x"
src="https://github.com/user-attachments/assets/a0b5f390-abeb-453f-8636-ad097f22308b"
/> | <img width="860" height="488" alt="CleanShot 2026-05-15 at 18 08
41@2x"
src="https://github.com/user-attachments/assets/4c67f797-e2cd-4d56-a49d-e6c6fc0edff3"
/> |

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

* **Documentation**
* Refined guidance and wording for dialog usage, clarifying when to use
Alert Dialog, Confirmation Modal, and modal modality.

* **New Features**
* Reworked RLS confirmation flow with a streamlined toggle dialog,
clearer messaging, and a “Learn more” link to docs.

* **Tests**
* Updated end-to-end tests to cover the updated RLS dialog flows and
explanatory content.

<!-- 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/45961?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 -->
2026-05-25 22:47:40 +00:00

2032 lines
79 KiB
TypeScript

import fs from 'fs'
import path from 'path'
import { expect, Page } from '@playwright/test'
import { env } from '../env.config.js'
import { expectClipboardValue } from '../utils/clipboard.js'
import { dropTable, query } from '../utils/db/index.js'
import { createTable, createTableWithRLS } from '../utils/db/queries.js'
import { resetLocalStorage } from '../utils/reset-local-storage.js'
import { test, withSetupCleanup } from '../utils/test.js'
import { toUrl } from '../utils/to-url.js'
import { waitForApiResponseWithTimeout } from '../utils/wait-for-response-with-timeout.js'
import {
createApiResponseWaiter,
waitForApiResponse,
waitForGridDataToLoad,
waitForTableToLoad,
} from '../utils/wait-for-response.js'
const deleteTable = async (page: Page, ref: string, tableName: string) => {
const viewLocator = page.getByLabel(`View ${tableName}`)
if ((await viewLocator.count()) === 0) return
await viewLocator.nth(0).click()
await viewLocator.locator('button[aria-haspopup="menu"]').click({ force: true })
await page.getByText('Delete table').click()
await page.getByRole('checkbox', { name: 'Drop table with cascade?' }).click()
const apiPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-delete-', {
method: 'POST',
})
const revalidatePromise = waitForApiResponse(page, 'pg-meta', ref, `query?key=entity-types-`)
await page.getByRole('button', { name: 'Delete' }).click()
await Promise.all([apiPromise, revalidatePromise])
await expect(page.getByTestId('confirm-delete-table-modal')).not.toBeVisible()
}
const deleteEnumIfExist = async (page: Page, ref: string, enumName: string) => {
const loadTypesPromise = waitForApiResponse(page, 'pg-meta', ref, `types`)
await page.goto(toUrl(`/project/${ref}/database/types?schema=public`))
await loadTypesPromise
expect(page.getByText('public').first()).toBeVisible()
// if enum (test) exists, delete it.
const exists = (await page.getByRole('cell', { name: enumName, exact: true }).count()) > 0
if (!exists) return
await page
.getByRole('row', { name: `public ${enumName}` })
.getByRole('button')
.click()
await page.getByRole('menuitem', { name: 'Delete type' }).click()
await page.getByRole('heading', { name: 'Confirm to delete enumerated' }).click()
const deleteEnumPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByRole('button', { name: 'Confirm delete' }).click()
await deleteEnumPromise
}
// Due to rate API rate limits run this test in serial mode on platform.
const testRunner = env.IS_PLATFORM ? test.describe.serial : test.describe
testRunner('table editor', () => {
test('sidebar actions works as expected', async ({ page, ref }) => {
const tableNameActions = 'pw_table_actions'
const tableNameActionsDuplicate = 'pw_table_actions_duplicate'
// create table + verify that this exists.
await using _ = await withSetupCleanup(
() => createTableWithRLS(tableNameActions, 'pw_column'),
async () => {
await dropTable(tableNameActions)
await dropTable(tableNameActionsDuplicate)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
// copies table name to clipboard when copy table name is clicked
await page.getByRole('button', { name: `View ${tableNameActions}`, exact: true }).click()
await page
.getByRole('button', { name: `View ${tableNameActions}`, exact: true })
.getByRole('button')
.nth(2)
.click()
await page.getByRole('menuitem', { name: 'Copy name' }).click()
// Make sure the dropdown has closed otherwise it would make the other assertions unstable
await expect(page.getByRole('menuitem', { name: 'Copy name' })).not.toBeVisible()
await expectClipboardValue({
page,
value: 'pw_table_actions',
exact: true,
})
// copies table schema to clipboard when copy schema option is clicked
await page
.getByRole('button', { name: `View ${tableNameActions}`, exact: true })
.getByRole('button')
.nth(2)
.click()
await page.getByRole('menuitem', { name: 'Copy table schema' }).click()
await expect(page.getByRole('menuitem', { name: 'Copy table schema' })).not.toBeVisible()
await expectClipboardValue({
page,
value: `create table public.pw_table_actions (
id bigint generated by default as identity not null,
created_at timestamp with time zone null default now(),
pw_column text null,
constraint pw_table_actions_pkey primary key (id)
) TABLESPACE pg_default;`,
exact: true,
})
// duplicates table
await page
.getByRole('button', { name: `View ${tableNameActions}`, exact: true })
.getByRole('button')
.nth(2)
.click()
await page.getByRole('menuitem', { name: 'Duplicate table' }).click()
await expect(page.getByRole('menuitem', { name: 'Duplicate table' })).not.toBeVisible()
const duplicatePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByRole('button', { name: 'Save' }).click()
await duplicatePromise // create duplicate table
await waitForTableToLoad(page, ref) // load tables
await expect(
page.getByLabel(`View ${tableNameActionsDuplicate}`, { exact: true })
).toBeVisible()
})
test('switching schemas work as expected', async ({ page, ref }) => {
const authTableSso = 'identities'
const authTableMfa = 'mfa_factors'
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
// change schema from public to auth
await page.getByTestId('schema-selector').click()
await page.getByPlaceholder('Find schema...').fill('auth')
// Set up the waiter BEFORE clicking to avoid race condition
const tableLoadPromise = waitForTableToLoad(page, ref, 'auth')
await page.getByRole('option', { name: 'auth' }).click()
await tableLoadPromise // wait for auth tables to load
await expect(page.getByLabel(`View ${authTableSso}`)).toBeVisible()
await expect(page.getByLabel(`View ${authTableMfa}`)).toBeVisible()
// Search is client-side filtering - no API call needed
await page.getByRole('textbox', { name: 'Search tables...' }).fill('mfa')
// Wait for the UI to update after search (allow debounce to complete)
await page.waitForTimeout(300)
await expect(page.getByLabel(`View ${authTableSso}`)).not.toBeVisible()
await expect(page.getByLabel(`View ${authTableMfa}`)).toBeVisible()
})
test('should show rls accordingly', async ({ page, ref }) => {
const tableNameRlsEnabled = 'pw_table_rls_enabled'
const tableNameRlsDisabled = 'pw_table_rls_disabled'
// create table with RLS enabled and verify
await using _ = await withSetupCleanup(
async () => {
await createTableWithRLS(tableNameRlsEnabled, 'pw_column')
await createTable(tableNameRlsDisabled, 'pw_column')
},
async () => {
await dropTable(tableNameRlsEnabled)
await dropTable(tableNameRlsDisabled)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await page.getByRole('button', { name: `View ${tableNameRlsEnabled}` }).click()
await expect(page.getByRole('link', { name: 'Add RLS policy' })).toBeVisible()
await page.getByRole('button', { name: `View ${tableNameRlsDisabled}` }).click()
await page.getByRole('button', { name: 'RLS disabled' }).click()
await page.getByRole('button', { name: 'Enable RLS for this table' }).click()
await expect(page.getByRole('heading', { name: 'Enable Row Level Security' })).toBeVisible()
await expect(
page.getByText(
'RLS restricts table access until policies allow a request. Existing queries may stop returning rows until policies are added.'
)
).toBeVisible()
await page.getByRole('button', { name: 'Cancel' }).click()
})
test('add enums and show enums on table', async ({ page, ref }) => {
const tableNameEnum = 'pw_table_enum'
const columnNameEnum = 'pw_column_enum'
const enum_name = 'pw_enum'
let shouldCleanup = true
await using _ = await withSetupCleanup(
async () => {
await query(`drop table if exists ${tableNameEnum};`)
await query(`drop type if exists ${enum_name};`)
},
async () => {
if (shouldCleanup) {
await query(`drop table if exists ${tableNameEnum};`)
await query(`drop type if exists ${enum_name};`)
}
}
)
await page.goto(toUrl(`/project/${ref}/database/types?schema=public`))
// create a new enum
await page.getByRole('button', { name: 'Create type' }).click()
await page.getByRole('button', { name: 'Add value' }).click()
await page.locator('input[name="values.0.value"]').fill('value1')
await page.locator('input[name="values.1.value"]').fill('value2')
await page.getByRole('textbox', { name: 'Name' }).fill(enum_name)
await page.getByRole('button', { name: 'Create type' }).click()
// verify enum is created
await expect(page.getByRole('cell', { name: enum_name, exact: true })).toBeVisible()
await expect(page.getByRole('cell', { name: 'value1, value2', exact: true })).toBeVisible()
// create a new table with new column for enums
const tableEditorEnumLoadWait = createApiResponseWaiter(
page,
'pg-meta',
ref,
'query?key=entity-types-public-'
)
await page.goto(toUrl(`/project/${ref}/editor`))
await tableEditorEnumLoadWait // load tables
await page.getByRole('button', { name: 'New table', exact: true }).click()
await page.getByTestId('table-name-input').fill(tableNameEnum)
await page.getByTestId('created_at-extra-options').click()
await page.getByText('Is Nullable').click()
await page.getByTestId('created_at-extra-options').click()
await page.getByRole('button', { name: 'Add column' }).click()
await page.getByLabel('Column name').nth(2).fill(columnNameEnum)
await page.getByRole('combobox').filter({ hasText: 'Choose a column type...' }).click()
await page.getByPlaceholder('Search types...').fill(enum_name)
// wait for response, then click
await page.getByRole('option', { name: enum_name }).click()
await page.getByRole('button', { name: 'Save' }).click()
await expect(
page.getByText(`Table ${tableNameEnum} is good to go!`),
'Success toast should be visible after table creation'
).toBeVisible({
timeout: 50000,
})
await expect(page.getByTestId('table-editor-side-panel')).not.toBeVisible()
// Wait for the grid to be visible and data to be loaded
await expect(page.getByRole('grid'), 'Grid should be visible after inserting data').toBeVisible(
{ timeout: 50_000 }
)
await expect(page.getByRole('columnheader', { name: enum_name })).toBeVisible()
// insert row with enum value
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByText('Insert row').click()
await page.getByRole('combobox').click()
await page.getByRole('option', { name: 'value1' }).click()
await page.getByTestId('action-bar-save-row').click()
await expect(page.getByTestId('side-panel-row-editor')).not.toBeVisible()
await expect(page.getByRole('gridcell', { name: 'value1' })).toBeVisible()
// insert row with another enum value
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByText('Insert row').click()
await page.getByRole('combobox').click()
await page.getByRole('option', { name: 'value2' }).click()
await page.getByTestId('action-bar-save-row').click()
await expect(page.getByRole('gridcell', { name: 'value2' })).toBeVisible({ timeout: 10_000 })
// delete enum and enum table
await deleteTable(page, ref, tableNameEnum)
await page.goto(toUrl(`/project/${ref}/database/types?schema=public`))
await deleteEnumIfExist(page, ref, enum_name)
// clear local storage, as it might result in some flakiness
await resetLocalStorage(page, ref)
shouldCleanup = false
})
test('Grid editor exporting works as expected', async ({ page, ref }) => {
const tableNameGridEditor = ' pw_table_grid_editor'
const tableNameUpdated = 'pw_table_updated'
const columnNameUpdated = 'pw_column_updated'
// create a new table
await using _ = await withSetupCleanup(
async () => {
await createTableWithRLS(tableNameGridEditor, 'pw_column')
},
async () => {
await dropTable(tableNameUpdated)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await page.getByRole('button', { name: `View ${tableNameGridEditor}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
// create 3 rows
for (const value of ['789', '456', '123']) {
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row' }).click()
await page.getByTestId('pw_column-input').fill(value)
const insertPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByTestId('action-bar-save-row').click()
await insertPromise // insert rows
}
// verify row content
expect(await page.getByRole('gridcell').nth(3).textContent()).toBe('789')
expect(await page.getByRole('gridcell').nth(8).textContent()).toBe('456')
expect(await page.getByRole('gridcell').nth(13).textContent()).toBe('123')
// edit table (rename table, rename column name)
await page
.getByRole('button', { name: `View ${tableNameGridEditor}`, exact: true })
.getByRole('button')
.nth(2)
.click()
await page.getByRole('menuitem', { name: 'Edit table' }).click()
await page.getByTestId('table-name-input').fill(tableNameUpdated)
await page.getByLabel('Column name').nth(2).fill(columnNameUpdated)
const updateTablePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=column-update', {
method: 'POST',
})
await page.getByRole('button', { name: 'Save' }).click()
await updateTablePromise // update table
await waitForTableToLoad(page, ref) // load tables
await expect(page.getByLabel(`View ${tableNameUpdated}`, { exact: true })).toBeVisible()
await expect(page.getByLabel(`View ${tableNameGridEditor}`, { exact: true })).not.toBeVisible()
await expect(page.getByRole('columnheader', { name: columnNameUpdated })).toBeVisible()
await expect(
page.getByRole('columnheader', { name: 'pw_column', exact: true })
).not.toBeVisible()
// test export data via csv
await page
.getByRole('button', { name: `View ${tableNameUpdated}`, exact: true })
.getByRole('button')
.nth(2)
.click()
// Open nested export submenu via keyboard (more stable than hover in headless)
const exportDataItemCsv = page.getByRole('menuitem', { name: 'Export data' })
await expect(exportDataItemCsv).toBeVisible()
await exportDataItemCsv.hover()
await expect(exportDataItemCsv).toHaveAttribute('data-state', /open/)
await expect(page.getByRole('menuitem', { name: 'Export table as CSV' })).toBeVisible()
const [downloadCsv] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('menuitem', { name: 'Export table as CSV' }).click(),
])
expect(downloadCsv.suggestedFilename()).toContain('.csv')
const downloadCsvPath = await downloadCsv.path()
const csvContent = fs.readFileSync(downloadCsvPath, 'utf-8').replace(/\r?\n/g, '\n')
const rows = csvContent.trim().split('\n')
const columnData = rows.map((row) => {
const columns = row.split(',')
return columns[2].trim()
})
const expectedColumnData = `${columnNameUpdated}, 123, 456, 789`
columnData.forEach((expectedValue) => {
expect(expectedColumnData).toContain(expectedValue)
})
fs.unlinkSync(downloadCsvPath)
// Close submenu and parent menu to avoid UI leftovers
await page.keyboard.press('Escape')
await page.keyboard.press('Escape')
await page.waitForTimeout(500)
// expect to NOT find the Export data menu item
await expect(page.getByRole('menuitem', { name: 'Export data' })).not.toBeVisible()
// test export data via SQL + verify
await page
.getByRole('button', { name: `View ${tableNameUpdated}`, exact: true })
.getByRole('button')
.nth(2)
.click()
// Open nested export submenu via keyboard (more stable than hover in headless)
const exportDataItemSql = page.getByRole('menuitem', { name: 'Export data' })
await expect(exportDataItemSql).toBeVisible()
await exportDataItemSql.hover({
force: true,
})
await expect(exportDataItemSql).toHaveAttribute('data-state', /open/)
await expect(page.getByRole('menuitem', { name: 'Export table as SQL' })).toBeVisible()
const [downloadSql] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('menuitem', { name: 'Export table as SQL' }).click(),
])
expect(downloadSql.suggestedFilename()).toContain('.sql')
const downloadSqlPath = await downloadSql.path()
const sqlContent = fs.readFileSync(downloadSqlPath, 'utf-8')
expect(sqlContent).toContain(
`INSERT INTO "public"."${tableNameUpdated}" ("id", "created_at", "${columnNameUpdated}") VALUES`
)
expect(sqlContent).toContain('789')
expect(sqlContent).toContain('456')
expect(sqlContent).toContain('123')
fs.unlinkSync(downloadSqlPath)
// Close submenu and parent menu to avoid UI leftovers
await page.keyboard.press('Escape')
await page.keyboard.press('Escape')
await page.waitForTimeout(500)
// test export data via CLI
await page
.getByRole('button', { name: `View ${tableNameUpdated}`, exact: true })
.getByRole('button')
.nth(2)
.click()
const exportDataItemCli = page.getByRole('menuitem', { name: 'Export data' })
await expect(exportDataItemCli).toBeVisible()
await exportDataItemCli.hover({
force: true,
})
await expect(page.getByRole('menuitem', { name: 'Export table via CLI' })).toBeVisible()
await page.getByRole('menuitem', { name: 'Export table via CLI' }).click()
await expect(page.getByRole('heading', { name: 'Export table data via CLI' })).toBeVisible()
await page.getByRole('button', { name: 'Close' }).first().click()
})
test('view table definition works as expected', async ({ page, ref }) => {
const tableName = 'pw_table_definition'
const colName = 'pw_column'
await using _ = await withSetupCleanup(
async () => {
await createTableWithRLS(tableName, 'pw_column')
},
async () => {
await dropTable(tableName)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
const apiPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-definition-')
await page.getByText('definition', { exact: true }).click()
await apiPromise
await expect(page.locator('.view-lines')).toContainText(
`create table public.${tableName} ( id bigint generated by default as identity not null, created_at timestamp with time zone null default now(), ${colName} text null, constraint ${tableName}_pkey primary key (id)) TABLESPACE pg_default;`
)
})
test('view definition preserves security_invoker for security invoker views', async ({
page,
ref,
}) => {
const tableName = `pw_view_def_source_${test.info().parallelIndex}`
const viewName = `pw_view_def_invoker_${test.info().parallelIndex}`
await using _ = await withSetupCleanup(
async () => {
await query(`drop view if exists public.${viewName};`)
await dropTable(tableName)
await query(`
create table public.${tableName} (
id bigint generated by default as identity primary key,
secret text
);
alter table public.${tableName} enable row level security;
create policy "${tableName}_anon_denied"
on public.${tableName}
for select
to anon
using (false);
create view public.${viewName} with (security_invoker = true) as
select id, secret
from public.${tableName};
`)
},
async () => {
await query(`drop view if exists public.${viewName};`)
await dropTable(tableName)
}
)
const tableLoadWait = createApiResponseWaiter(
page,
'pg-meta',
ref,
'query?key=entity-types-public-'
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await tableLoadWait
await page.getByRole('button', { name: `View ${viewName}`, exact: true }).click()
const definitionWait = waitForApiResponse(page, 'pg-meta', ref, 'query?key=view-definition-')
await page.getByText('definition', { exact: true }).click()
await definitionWait
await expect(page.locator('.view-lines')).toContainText(`create view public.${viewName}`)
await expect(page.locator('.view-lines')).toContainText(`security_invoker = true`)
const openInSqlEditorLink = page.getByRole('link', { name: 'Open in SQL Editor' })
// Accept either percent-encoded (`%20`) or form-encoded (`+`) spaces —
// some routers serialize search params via URLSearchParams which emits
// `+` for spaces. Both decode to the same SQL.
await expect(openInSqlEditorLink).toHaveAttribute(
'href',
/security_invoker(%20|\+)%3D(%20|\+)true/
)
await openInSqlEditorLink.click()
await page.waitForURL(/\/sql\/new/)
await expect(page.locator('.view-lines')).toContainText(`create view public.${viewName}`)
await expect(page.locator('.view-lines')).toContainText(`security_invoker = true`)
})
test('sorting rows works as expected', async ({ page, ref }) => {
const tableName = 'pw_table_sorting'
const colName = 'pw_column'
await using _ = await withSetupCleanup(
async () => {
await createTableWithRLS(tableName, 'pw_column')
},
async () => {
await dropTable(tableName)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
for (const value of ['789', '456', '123']) {
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row' }).click()
await page.getByTestId(`${colName}-input`).fill(value)
const insertPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByTestId('action-bar-save-row').click()
await insertPromise
}
// Apply sorting
await page.getByRole('button', { name: 'Sort', exact: true }).click()
await page.getByRole('button', { name: 'Pick a column to sort by' }).click()
await page.getByRole('menuitem', { name: colName }).click()
const waitForSortingApply = createApiResponseWaiter(
page,
'pg-meta',
ref,
'query?key=table-rows-'
)
await page.getByRole('button', { name: 'Apply sorting' }).click()
await waitForSortingApply
await page.getByRole('button', { name: 'Sorted by 1 rule' }).click()
// Verify sorted row content asc lexicographically for strings
await page.waitForTimeout(500)
expect(await page.getByRole('gridcell').nth(3).textContent()).toBe('123')
expect(await page.getByRole('gridcell').nth(8).textContent()).toBe('456')
expect(await page.getByRole('gridcell').nth(13).textContent()).toBe('789')
})
test('column actions works as expected', async ({ page, ref }) => {
const tableName = 'pw_table_column_menu'
const colName = 'pw_column'
// Create a small table and three rows
await using _ = await withSetupCleanup(
async () => {
await createTableWithRLS(tableName, 'pw_column')
},
async () => {
await dropTable(tableName)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
// Copy the column name
await page
.getByRole('columnheader', { name: colName })
.getByRole('button', { name: `Column ${colName} actions` })
.click()
await page.getByRole('menuitem', { name: 'Copy name' }).click()
await expectClipboardValue({
page,
value: colName,
exact: true,
})
})
test('importing, pagination and large data actions works as expected', async ({ page, ref }) => {
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
const tableNameDataActions = 'pw_table_data'
// create table
await using _ = await withSetupCleanup(
async () => {
await createTableWithRLS(tableNameDataActions, 'pw_column')
},
async () => {
await dropTable(tableNameDataActions)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await page.getByRole('button', { name: `View ${tableNameDataActions}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
// importing 50 data via csv file
const csvFilePath = path.join(import.meta.dirname, 'files', 'table-editor-import-file.csv')
await page.getByRole('button', { name: 'Import data from CSV' }).click()
await page.getByRole('tab', { name: 'Upload CSV' }).click()
await page.setInputFiles('input[type="file"]', csvFilePath)
await expect(page.getByText('A total of 50 rows will be')).toBeVisible()
const waitForCsvInsert = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByRole('button', { name: 'Import data' }).click()
await waitForCsvInsert // insert data
await waitForGridDataToLoad(page, ref) // retrieve updated data
await expect(page.getByText('50 records')).toBeVisible()
// importing 51 data via paste text
const filePath = path.join(import.meta.dirname, 'files', 'table-editor-import-paste.txt')
const fileContent = fs.readFileSync(filePath, 'utf-8')
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Import data from CSV' }).click()
await page.getByRole('tab', { name: 'Paste text' }).click()
await page.getByRole('textbox').fill(fileContent)
await expect(page.getByText('A total of 51 rows will be')).toBeVisible()
const waitForPasteInsert = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByRole('button', { name: 'Import data' }).click()
await waitForPasteInsert // insert data
await waitForGridDataToLoad(page, ref) // retrieve updated data
await expect(page.getByText('101 records')).toBeVisible()
// test pagination (page 1 -> page 2)
await expect(page.getByRole('gridcell', { name: 'value 7', exact: true })).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'value 101', exact: true })).not.toBeVisible()
const waitForPageChange = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=table-rows-')
await page.getByLabel('Table grid footer').getByRole('button').nth(1).click()
await waitForPageChange // retrieve next page data
await expect(page.getByRole('gridcell', { name: 'value 7', exact: true })).not.toBeVisible()
await expect(page.getByRole('gridcell', { name: 'value 101', exact: true })).toBeVisible()
// change pagination size (100 -> 500)
await page.getByRole('button', { name: '100 rows' }).click()
const waitForPaginationChange = createApiResponseWaiter(
page,
'pg-meta',
ref,
'query?key=table-rows-'
)
await page.getByRole('menuitem', { name: '500 rows' }).click()
await waitForPaginationChange // retrieve updated pagination size data
await expect(page.getByRole('gridcell', { name: 'value 7', exact: true })).toBeVisible()
await page.getByRole('grid').evaluate((element) => {
element.scrollTop = element.scrollHeight
}) // scroll to bottom
await expect(page.getByRole('gridcell', { name: 'value 101', exact: true })).toBeVisible()
// remove selected rows when multiple rows action is selected
await page.getByRole('grid').evaluate((element) => {
element.scrollTop = 0
}) // scroll to top
await page.getByRole('row', { name: 'value 1 to delete' }).getByRole('checkbox').click()
await page.getByRole('row', { name: 'value 2 to delete' }).getByRole('checkbox').click()
await page.getByRole('row', { name: 'value 3 to delete' }).getByRole('checkbox').click()
await page.getByRole('button', { name: 'Delete 3 rows' }).click()
await expect(page.getByText('delete the selected 3 rows')).toBeVisible()
const waitForDeleteRows = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByRole('button', { name: 'Delete' }).click()
await waitForDeleteRows // delete selected rows
await waitForGridDataToLoad(page, ref) // retrieve row data
// export selected rows when multiple rows action is selected
await page.getByRole('row', { name: 'value 4 to export' }).getByRole('checkbox').click()
await page.getByRole('row', { name: 'value 5 to export' }).getByRole('checkbox').click()
await page.getByRole('row', { name: 'value 6 to export' }).getByRole('checkbox').click()
await page.getByRole('button', { name: 'Export' }).click()
const [downloadSql] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('menuitem', { name: 'Export as SQL' }).click(),
])
expect(downloadSql.suggestedFilename()).toContain('.sql')
const downloadSqlPath = await downloadSql.path()
const sqlContent = fs.readFileSync(downloadSqlPath, 'utf-8')
expect(sqlContent).toBe(
`INSERT INTO "public"."${tableNameDataActions}" ("id", "created_at", "pw_column") VALUES (4, '2025-01-01 12:00:00+00', 'value 4 to export'), (5, '2025-01-01 12:00:00+00', 'value 5 to export'), (6, '2025-01-01 12:00:00+00', 'value 6 to export');`
)
await page.waitForTimeout(1000) // wait for event processing to complete
fs.unlinkSync(downloadSqlPath)
// Close menu to prevent overlap with next export
await page.keyboard.press('Escape')
await page.keyboard.press('Escape')
await page.waitForTimeout(500)
await page.getByRole('button', { name: 'Export' }).click()
const [downloadJson] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('menuitem', { name: 'Export as JSON' }).click(),
])
expect(downloadJson.suggestedFilename()).toContain('.json')
const downloadJsonPath = await downloadJson.path()
const jsonContent = fs.readFileSync(downloadJsonPath, 'utf-8')
expect(jsonContent).toBe(
`[{"id":4,"created_at":"2025-01-01 12:00:00+00","pw_column":"value 4 to export"},{"id":5,"created_at":"2025-01-01 12:00:00+00","pw_column":"value 5 to export"},{"id":6,"created_at":"2025-01-01 12:00:00+00","pw_column":"value 6 to export"}]`
)
await page.waitForTimeout(1000) // wait for event processing to complete
fs.unlinkSync(downloadJsonPath)
// Close menu to prevent overlap with next export
await page.keyboard.press('Escape')
await page.keyboard.press('Escape')
await page.waitForTimeout(500)
await page.getByRole('button', { name: 'Export' }).click()
const [downloadCsv] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('menuitem', { name: 'Export as CSV' }).click(),
])
expect(downloadCsv.suggestedFilename()).toContain('.csv')
const downloadCsvPath = await downloadCsv.path()
const csvContent = fs.readFileSync(downloadCsvPath, 'utf-8').replace(/\r?\n/g, '\n')
const rows = csvContent.trim().split('\n')
const defaultColumnValues = rows.map((row) => {
const columns = row.split(',')
return columns[2].trim()
})
const expectedDefaultColumnValues = [
'pw_column',
'value 4 to export',
'value 5 to export',
'value 6 to export',
]
defaultColumnValues.forEach((expectedValue) => {
expect(expectedDefaultColumnValues).toContain(expectedValue)
})
await page.waitForTimeout(1000) // wait for event processing to complete
fs.unlinkSync(downloadCsvPath)
// Close menu to avoid leaving it open
await page.keyboard.press('Escape')
await page.keyboard.press('Escape')
await page.waitForTimeout(500)
// select all actions works (delete action)
await page.getByRole('checkbox', { name: 'Select All' }).click()
await page.getByRole('button', { name: 'Delete 98 rows' }).click()
const waitForDeleteAllRows = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByRole('button', { name: 'Delete' }).click()
await expect(page.getByText('delete the selected 98 rows')).toBeVisible()
await waitForDeleteAllRows // delete all rows
await waitForGridDataToLoad(page, ref) // retrieve rows data
await expect(page.getByRole('gridcell', { name: 'value 7' })).not.toBeVisible()
await expect(page.getByRole('gridcell', { name: 'value 101' })).not.toBeVisible()
})
test('copying cell values from first and second row works', async ({ page, ref }) => {
const tableName = 'pw_table_copy_rows'
const colName = 'pw_column'
// Create table and add two rows
await using _ = await withSetupCleanup(
async () => {
await createTableWithRLS(tableName, 'pw_column')
},
async () => {
await dropTable(tableName)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
// Insert first row with value 'first_row_value'
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row' }).click()
await page.getByTestId(`${colName}-input`).fill('first_row_value')
const insertFirstPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByTestId('action-bar-save-row').click()
await insertFirstPromise
// Insert second row with value 'second_row_value'
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row' }).click()
await page.getByTestId(`${colName}-input`).fill('second_row_value')
const insertSecondPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByTestId('action-bar-save-row').click()
await insertSecondPromise
// Wait for grid to be visible
await expect(page.getByRole('grid')).toBeVisible()
// Right-click on the first row's cell to open context menu
const firstRowCell = page.getByRole('gridcell', { name: 'first_row_value' })
await expect(firstRowCell).toBeVisible()
await firstRowCell.click({ button: 'right' })
// Click "Copy cell" from context menu
await page.getByRole('menuitem', { name: 'Copy cell' }).click()
// Verify first row value was copied
await expectClipboardValue({
page,
value: 'first_row_value',
exact: true,
})
// Right-click on the second row's cell to open context menu
const secondRowCell = page.getByRole('gridcell', { name: 'second_row_value' })
await expect(secondRowCell).toBeVisible()
await secondRowCell.click({ button: 'right' })
// Click "Copy cell" from context menu
await page.getByRole('menuitem', { name: 'Copy cell' }).click()
// Verify second row value was copied
await expectClipboardValue({
page,
value: 'second_row_value',
exact: true,
})
})
test('boolean fields can be edited correctly', async ({ page, ref }) => {
const tableName = 'pw_table_boolean_edits'
const boolColName = 'is_active'
await using _ = await withSetupCleanup(
async () => {
await createTableWithRLS(tableName, 'pw_column')
},
async () => {
await dropTable(tableName)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page
.getByRole('button', { name: `View ${tableName}`, exact: true })
.getByRole('button')
.nth(2)
.click()
await page.getByRole('menuitem', { name: 'Edit table' }).click()
// Add boolean column
await page.getByRole('button', { name: 'Add column' }).click()
await page.getByLabel('Column name').nth(3).fill(boolColName)
await page.getByText('Choose a column type...').click()
await page.getByPlaceholder('Search types...').fill('bool')
await page.getByRole('option', { name: 'bool' }).first().click()
await page.getByRole('button', { name: 'Save' }).click()
await expect(
page.getByText(`Successfully updated ${tableName}!`),
'Success toast should be visible after table update'
).toBeVisible({ timeout: 50000 })
// Navigate to the table
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
// Insert a row with TRUE value via side panel
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row' }).click()
await page.getByRole('combobox').click()
await page.getByRole('option', { name: 'TRUE' }).click()
const insertTruePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByTestId('action-bar-save-row').click()
await insertTruePromise
await expect(
page.getByRole('gridcell', { name: 'TRUE' }),
'TRUE value should be displayed'
).toBeVisible()
// Insert a row with FALSE value via side panel
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row' }).click()
await page.getByRole('combobox').click()
await page.getByRole('option', { name: 'FALSE' }).click()
const insertFalsePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByTestId('action-bar-save-row').click()
await insertFalsePromise
// Verify FALSE value is preserved
await expect(
page.getByRole('gridcell', { name: 'FALSE' }),
'FALSE value should be displayed and preserved'
).toBeVisible()
// Edit the FALSE value to TRUE using inline editor
const falseCell = page.getByRole('gridcell', { name: 'FALSE' }).first()
await falseCell.dblclick()
// Wait for boolean editor dropdown to appear
const booleanEditor = page.locator('#boolean-editor')
await expect(booleanEditor, 'Boolean editor should be visible').toBeVisible()
// Change from false to true
const updateTrueResponse = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await booleanEditor.click()
await page.getByRole('option', { name: 'true' }).click()
await page.getByRole('columnheader', { name: 'id' }).click()
await updateTrueResponse
// Verify the value changed to TRUE (now there should be 2 TRUE values in the table)
await expect(
page.getByRole('gridcell', { name: 'TRUE' }),
'Value should change to TRUE after inline edit'
).toHaveCount(2)
// Edit TRUE value back to FALSE using inline editor
// Use the second TRUE cell (the one we just edited from FALSE to TRUE)
const trueCell = page.getByRole('gridcell', { name: 'TRUE' }).nth(1)
await trueCell.dblclick()
await expect(booleanEditor, 'Boolean editor should be visible for second edit').toBeVisible()
const updateFalseResponse = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await booleanEditor.click()
await page.getByRole('option', { name: 'false' }).click()
await page.getByRole('columnheader', { name: 'id' }).click()
await updateFalseResponse
// Verify FALSE value is preserved and not converted to NULL (this is the critical regression test)
const falseCells = page.getByRole('gridcell', { name: 'FALSE' })
await expect(
falseCells.first(),
'FALSE value should be preserved and not become NULL after inline edit'
).toBeVisible()
})
test('nullable boolean fields support NULL values', async ({ page, ref }) => {
const tableName = 'pw_table_boolean_nullable'
const boolColName = 'is_enabled'
await using _ = await withSetupCleanup(
async () => {
await createTableWithRLS(tableName, 'pw_column')
},
async () => {
await dropTable(tableName)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page
.getByRole('button', { name: `View ${tableName}`, exact: true })
.getByRole('button')
.nth(2)
.click()
await page.getByRole('menuitem', { name: 'Edit table' }).click()
// Add nullable boolean column
await page.getByRole('button', { name: 'Add column' }).click()
await page.getByLabel('Column name').nth(3).fill(boolColName)
await page.getByText('Choose a column type...').click()
await page.getByPlaceholder('Search types...').fill('bool')
await page.getByRole('option', { name: 'bool' }).first().click()
await page.getByRole('button', { name: 'Save' }).click()
await expect(
page.getByText(`Successfully updated ${tableName}!`),
'Success toast should be visible after table update'
).toBeVisible({ timeout: 50000 })
await expect(
page.getByRole('button', { name: `View ${tableName}`, exact: true }),
'Table should be visible after update'
).toBeVisible()
// Navigate to the table
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
// Insert a row with TRUE value
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row' }).click()
await page.getByRole('combobox').click()
await page.getByRole('option', { name: 'TRUE' }).click()
const insertTruePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByTestId('action-bar-save-row').click()
await insertTruePromise
await expect(
page.getByRole('gridcell', { name: 'TRUE' }),
'TRUE value should be displayed'
).toBeVisible()
// Insert a row with FALSE value
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row' }).click()
await page.getByRole('combobox').click()
await page.getByRole('option', { name: 'FALSE' }).click()
const insertFalsePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByTestId('action-bar-save-row').click()
await insertFalsePromise
await expect(
page.getByRole('gridcell', { name: 'FALSE' }),
'FALSE value should be displayed'
).toBeVisible()
// Edit FALSE to NULL using inline editor
const falseCellToNull = page.getByRole('gridcell', { name: 'FALSE' })
await falseCellToNull.dblclick()
const booleanEditor = page.locator('#boolean-editor')
await expect(booleanEditor, 'Boolean editor should be visible').toBeVisible()
const updateNullResponse = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await booleanEditor.click()
await page.getByRole('option', { name: 'null' }).click()
await page.getByRole('columnheader', { name: 'id' }).click()
await updateNullResponse
// Verify value changed to NULL on the second row
const nullCells = page.getByRole('gridcell', { name: 'NULL' }).nth(2)
await expect(nullCells, 'FALSE should change to NULL after inline edit').toBeVisible()
// Edit NULL to FALSE using inline editor
const nullCellToFalse = page.getByRole('gridcell', { name: 'NULL' }).nth(2)
await nullCellToFalse.dblclick()
const updateFalseResponse = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await booleanEditor.click()
await page.getByRole('option', { name: 'false' }).click()
await page.getByRole('columnheader', { name: 'id' }).click()
await updateFalseResponse
await expect(
page.getByRole('gridcell', { name: 'FALSE' }),
'NULL should change to FALSE after inline edit'
).toBeVisible()
})
test('can create and remove foreign key with column selection', async ({ page, ref }) => {
const sourceTableName = 'pw_table_fk_source'
const targetTableName = 'pw_table_fk_target'
await using _ = await withSetupCleanup(
async () => {
await createTableWithRLS(targetTableName, 'pw_column')
await createTableWithRLS(sourceTableName, 'pw_column')
},
async () => {
await dropTable(sourceTableName)
await dropTable(targetTableName)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await page.getByRole('button', { name: `View ${sourceTableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
// Open edit table dialog
await page
.getByRole('button', { name: `View ${sourceTableName}`, exact: true })
.locator('button[aria-haspopup="menu"]')
.click()
await page.getByRole('menuitem', { name: 'Edit table' }).click()
// Select target table
const tableQueryPromise = waitForApiResponseWithTimeout(page, (response) =>
response.url().includes(`table-public-${targetTableName}`)
)
// Open foreign key selector
await page.getByRole('button', { name: 'Add foreign key relation' }).click()
// Select schema (should default to public)
await expect(page.getByRole('combobox', { name: 'Select a schema' })).toContainText('public')
await page.getByRole('combobox', { name: 'Select a table to reference to' }).click()
await page.getByRole('option', { name: `public ${targetTableName}` }).click()
// Wait for table columns to load
await tableQueryPromise
// Verify column selection UI appears
await expect(
page.getByText(`Select columns from public.${targetTableName} to reference to`)
).toBeVisible()
// Select source column (id from source table)
await page.getByRole('combobox', { name: 'Column from public.pw_table_fk_source' }).click()
await page.getByRole('option', { name: 'id int8' }).click()
// Select target column (id from target table)
await page.getByRole('combobox', { name: 'Column from public.pw_table_fk_target' }).click()
await page.getByRole('option', { name: 'id int8' }).click()
// Verify cascade action options are visible
await expect(page.getByText('Action if referenced row is updated')).toBeVisible()
await expect(page.getByText('Action if referenced row is removed')).toBeVisible()
// Verify save button is now enabled
const saveButton = page.getByRole('button', { name: 'Save' }).last()
await expect(saveButton).toBeEnabled()
// Save the foreign key
const fkCreatePromise = waitForApiResponseWithTimeout(page, (response) =>
response.url().includes('query?key=')
)
await saveButton.click()
await fkCreatePromise
// Verify foreign key selector closed
await expect(
page.getByRole('banner', { name: `Add foreign key relationship to ${sourceTableName}` })
).not.toBeVisible()
// Save table changes
const saveTablePromise = waitForApiResponseWithTimeout(
page,
(response) => response.url().includes('query?key=table-update'),
30000
)
await page.getByRole('button', { name: 'Save' }).first().click()
await saveTablePromise
// Wait for table editor side panel to close
await expect(page.getByTestId('table-editor-side-panel')).not.toBeVisible()
// Verify foreign key was created by opening edit table dialog again
await page
.getByRole('button', { name: `View ${sourceTableName}`, exact: true })
.locator('button[aria-haspopup="menu"]')
.click()
await page.getByRole('menuitem', { name: 'Edit table' }).click()
// Scroll down to see foreign key relations section
await page.getByRole('heading', { name: 'Foreign keys' }).scrollIntoViewIfNeeded()
// Verify foreign key relation exists
await expect(page.getByRole('link', { name: 'public.pw_table_fk_target' })).toBeVisible()
// Remove the foreign key relation
await page.getByRole('button', { name: 'Remove' }).click()
// Save the table changes after removing foreign key
const removeFkPromise = waitForApiResponseWithTimeout(
page,
(response) => response.url().includes('query?key=table-update'),
30000
)
await page.getByRole('button', { name: 'Save' }).first().click()
await removeFkPromise
// Wait for table editor side panel to close
await expect(page.getByTestId('table-editor-side-panel')).not.toBeVisible()
// Verify foreign key was removed by opening edit table dialog again
await page
.getByRole('button', { name: `View ${sourceTableName}`, exact: true })
.locator('button[aria-haspopup="menu"]')
.click()
await page.getByRole('menuitem', { name: 'Edit table' }).click()
// Scroll down to see foreign key relations section
await page.getByRole('heading', { name: 'Foreign keys' }).scrollIntoViewIfNeeded()
// Verify foreign key relation no longer exists
await expect(page.getByText(`public.${targetTableName}`, { exact: false })).not.toBeVisible()
// Close the edit table dialog
await page.getByRole('button', { name: 'Cancel' }).click()
await expect(page.getByTestId('table-editor-side-panel')).not.toBeVisible()
})
test('shortcut saves fk before column update', async ({ page, ref }) => {
const runId = `${test.info().parallelIndex}_${test.info().repeatEachIndex}`
const sourceTableName = `pw_column_fk_shortcut_source_${runId}`
const targetTableName = `pw_column_fk_shortcut_target_${runId}`
const columnEditor = page.getByRole('dialog', {
name: `Update column ref_id from ${sourceTableName}`,
exact: true,
})
const foreignKeySchemaSelect = page.getByRole('combobox', { name: 'Select a schema' })
const foreignKeyLink = columnEditor.getByRole('link', {
name: `public.${targetTableName}`,
exact: true,
})
const openColumnEditor = async () => {
await page
.getByRole('columnheader', { name: 'ref_id' })
.getByRole('button', { name: 'Column ref_id actions' })
.click()
await page.getByRole('menuitem', { name: 'Edit column' }).click()
await expect(columnEditor).toBeVisible()
}
await using _ = await withSetupCleanup(
() =>
query(`
drop table if exists public.${sourceTableName} cascade;
drop table if exists public.${targetTableName} cascade;
create table public.${targetTableName} (
id text primary key
);
create table public.${sourceTableName} (
id bigint generated by default as identity primary key,
ref_id text
);
`),
async () => {
await dropTable(sourceTableName)
await dropTable(targetTableName)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await page.getByRole('button', { name: `View ${sourceTableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
await openColumnEditor()
await columnEditor.getByRole('button', { name: 'Add foreign key' }).click()
await expect(foreignKeySchemaSelect).toContainText('public')
const tableQueryPromise = waitForApiResponseWithTimeout(page, (response) =>
response.url().includes(`table-public-${targetTableName}`)
)
await page.getByRole('combobox', { name: 'Select a table to reference to' }).click()
await page.getByRole('option', { name: `public ${targetTableName}` }).click()
await tableQueryPromise
await page.getByRole('combobox', { name: `Column from public.${targetTableName}` }).click()
await page.getByRole('option', { name: 'id text' }).click()
await page.keyboard.press('ControlOrMeta+Enter')
await expect(foreignKeySchemaSelect).not.toBeVisible()
await expect(foreignKeyLink).toBeVisible()
const updateColumnPromise = waitForApiResponse(
page,
'pg-meta',
ref,
'query?key=column-update',
{
method: 'POST',
}
)
await columnEditor.getByRole('button', { name: 'Save' }).click()
await updateColumnPromise
await expect(columnEditor).not.toBeVisible()
await openColumnEditor()
await expect(foreignKeyLink).toBeVisible()
})
test('CSV drag and drop imports data on empty table', async ({ page, ref }) => {
const tableName = 'pw_table_csv_drag_drop'
await using _ = await withSetupCleanup(
async () => {
await createTableWithRLS(tableName, 'pw_column')
},
async () => {
await dropTable(tableName)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
await expect(
page.getByText('or drag and drop a CSV file here'),
'Empty table should show drag and drop hint'
).toBeVisible()
const csvFilePath = path.join(import.meta.dirname, 'files', 'table-editor-drag-drop.csv')
const csvBuffer = fs.readFileSync(csvFilePath)
// Synthesize a DataTransfer with the CSV file to simulate a browser file drag-and-drop
const dataTransfer = await page.evaluateHandle((csvBase64: string) => {
const dt = new DataTransfer()
const bytes = Uint8Array.from(atob(csvBase64), (c) => c.charCodeAt(0))
const file = new File([bytes], 'table-editor-drag-drop.csv', { type: 'text/csv' })
dt.items.add(file)
return dt
}, csvBuffer.toString('base64'))
const gridContainer = page.getByTestId('table-editor-grid-container')
await gridContainer.dispatchEvent('dragover', { dataTransfer })
// After the refactor, the empty state text and import button remain visible during drag
// (no more "Drop your CSV file here" overlay), just a dashed border is shown
await expect(
page.getByText('This table is empty'),
'Empty table message should remain visible during drag'
).toBeVisible()
await gridContainer.dispatchEvent('drop', { dataTransfer })
await expect(
page.getByText('A total of 3 rows will be'),
'Import dialog should show correct row count from CSV'
).toBeVisible({ timeout: 10_000 })
const waitForCsvInsert = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByRole('button', { name: 'Import data' }).click()
await waitForCsvInsert
await waitForGridDataToLoad(page, ref)
await expect(
page.getByText('3 records'),
'Table should show 3 records after drag and drop import'
).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'drag drop value 1' })).toBeVisible()
})
test('CSV import with extra column succeeds after deselecting incompatible header', async ({
page,
ref,
}) => {
const tableName = 'pw_table_csv_extra_col'
await using _ = await withSetupCleanup(
async () => {
await createTableWithRLS(tableName, 'pw_column')
},
async () => {
await dropTable(tableName)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
// Upload a CSV that has an extra column not in the table
const csvFilePath = path.join(
import.meta.dirname,
'files',
'table-editor-import-extra-column.csv'
)
await page.getByRole('button', { name: 'Import data from CSV' }).click()
await page.getByRole('tab', { name: 'Upload CSV' }).click()
await page.setInputFiles('input[type="file"]', csvFilePath)
// The "Data incompatible" badge should be visible because of the extra column
await expect(page.getByText('Data incompatible')).toBeVisible()
// Expand the configuration section and deselect the extra column
await page.getByRole('button', { name: 'Toggle import configuration' }).click()
await expect(page.getByText('Select which columns to import')).toBeVisible()
await page.getByRole('button', { name: 'Toggle column extra_column' }).click()
// After deselecting, the incompatible badge should disappear
await expect(page.getByText('Data incompatible')).not.toBeVisible()
// Import should succeed now
const waitForCsvInsert = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByRole('button', { name: 'Import data' }).click()
await waitForCsvInsert
await waitForGridDataToLoad(page, ref)
await expect(page.getByText('3 records')).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'value 1' })).toBeVisible()
})
test('CSV import syncs custom owned sequences before the next insert', async ({ page, ref }) => {
const tableName = 'pw_table_csv_sequence_sync'
const sequenceName = 'pw_table_csv_import_owned_seq'
await using _ = await withSetupCleanup(
async () => {
await query(`drop table if exists public.${tableName} cascade;`)
await query(`drop sequence if exists public.${sequenceName};`)
await query(`create sequence public.${sequenceName};`)
await query(`create table public.${tableName} (
id bigint primary key default nextval('public.${sequenceName}'),
name text
);`)
await query(`alter sequence public.${sequenceName} owned by public.${tableName}.id;`)
},
async () => {
await query(`drop table if exists public.${tableName} cascade;`)
await query(`drop sequence if exists public.${sequenceName};`)
}
)
const tableEditorLoadWait = createApiResponseWaiter(
page,
'pg-meta',
ref,
'query?key=entity-types-public-'
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await tableEditorLoadWait
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
const csvFilePath = path.join(import.meta.dirname, 'files', 'table-editor-import-sequence.csv')
await page.getByRole('button', { name: 'Import data from CSV' }).click()
await page.getByRole('tab', { name: 'Upload CSV' }).click()
await page.setInputFiles('input[type="file"]', csvFilePath)
await expect(page.getByText('A total of 3 rows will be')).toBeVisible()
const waitForCsvInsert = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByRole('button', { name: 'Import data' }).click()
await waitForCsvInsert
await waitForGridDataToLoad(page, ref)
await expect(page.getByText('3 records')).toBeVisible()
await expect
.poll(async () => {
const [{ state }] = await query<{ state: string }>(`
select format(
'%s|%s|%s',
(select coalesce(max(id), 0) from public.${tableName}),
last_value,
is_called
) as state
from public.${sequenceName};
`)
return state
})
.toBe('229|229|t')
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row' }).click()
await page.getByTestId('name-input').fill('Dave')
const insertPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByTestId('action-bar-save-row').click()
await insertPromise
await expect
.poll(async () => {
const [{ id }] = await query<{ id: string }>(
`select id::text as id from public.${tableName} where name = 'Dave'`
)
return id
})
.toBe('230')
})
test('pasted CSV text syncs custom owned sequences before the next insert', async ({
page,
ref,
}) => {
const tableName = 'pw_table_paste_sequence_sync'
const sequenceName = 'pw_table_paste_import_owned_seq'
await using _ = await withSetupCleanup(
async () => {
await query(`drop table if exists public.${tableName} cascade;`)
await query(`drop sequence if exists public.${sequenceName};`)
await query(`create sequence public.${sequenceName};`)
await query(`create table public.${tableName} (
id bigint primary key default nextval('public.${sequenceName}'),
name text
);`)
await query(`alter sequence public.${sequenceName} owned by public.${tableName}.id;`)
},
async () => {
await query(`drop table if exists public.${tableName} cascade;`)
await query(`drop sequence if exists public.${sequenceName};`)
}
)
const waitForTable = waitForTableToLoad(page, ref)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await waitForTable
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
const csvFilePath = path.join(import.meta.dirname, 'files', 'table-editor-import-sequence.csv')
const csvText = fs.readFileSync(csvFilePath, 'utf-8')
await page.getByRole('button', { name: 'Import data from CSV' }).click()
await page.getByRole('tab', { name: 'Paste text' }).click()
await page.getByRole('textbox').fill(csvText)
await expect(page.getByText('A total of 3 rows will be')).toBeVisible()
const waitForCsvInsert = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByRole('button', { name: 'Import data' }).click()
await waitForCsvInsert
await waitForGridDataToLoad(page, ref)
await expect(page.getByText('3 records')).toBeVisible()
await expect
.poll(async () => {
const [{ state }] = await query<{ state: string }>(`
select format(
'%s|%s|%s',
(select coalesce(max(id), 0) from public.${tableName}),
last_value,
is_called
) as state
from public.${sequenceName};
`)
return state
})
.toBe('229|229|t')
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row' }).click()
await page.getByTestId('name-input').fill('Dave')
const insertPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByTestId('action-bar-save-row').click()
await insertPromise
await expect
.poll(async () => {
const [{ id }] = await query<{ id: string }>(
`select id::text as id from public.${tableName} where name = 'Dave'`
)
return id
})
.toBe('230')
})
test('row insert via side panel saves immediately', async ({ page, ref }) => {
const tableName = 'pw_table_row_insert'
const columnName = 'name'
await using _ = await withSetupCleanup(
async () => {
await createTable(tableName, columnName)
},
async () => {
await dropTable(tableName)
}
)
const tableEditorLoadWait = createApiResponseWaiter(
page,
'pg-meta',
ref,
'query?key=entity-types-public-'
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await tableEditorLoadWait
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
// Open side panel to insert 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('immediate insert')
// Wait for the POST mutation to complete when saving
const insertPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByTestId('action-bar-save-row').click()
await insertPromise
// Should show success toast
await expect(
page.getByText('Successfully created row'),
'Success toast should appear after immediate row creation'
).toBeVisible({ timeout: 10000 })
// Row should be visible in the grid
await expect(
page.getByRole('gridcell', { name: 'immediate insert' }),
'Newly inserted row should be visible in the grid'
).toBeVisible()
// Should NOT show pending changes (queue is off)
await expect(
page.getByText('pending change'),
'No pending changes should appear when queue is disabled'
).not.toBeVisible()
})
test('row edit via side panel saves immediately', async ({ page, ref }) => {
const tableName = 'pw_table_row_edit'
const columnName = 'name'
await using _ = await withSetupCleanup(
async () => {
await createTable(tableName, columnName, [{ name: 'original value' }])
},
async () => {
await dropTable(tableName)
}
)
const tableEditorLoadWait = createApiResponseWaiter(
page,
'pg-meta',
ref,
'query?key=entity-types-public-'
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await tableEditorLoadWait
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()
// Right-click to open context menu and edit the row
const cell = page.getByRole('gridcell', { name: 'original value' })
await cell.click({ button: 'right' })
await page.getByRole('menuitem', { name: 'Edit row' }).click()
// Update the value in the side panel
const input = page.getByTestId(`${columnName}-input`)
await expect(input).toBeVisible()
await input.clear()
await input.fill('updated value')
// Wait for the POST mutation to complete when saving
const updatePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByTestId('action-bar-save-row').click()
await updatePromise
// Updated value should be visible in the grid after immediate save
await expect(
page.getByRole('gridcell', { name: 'updated value' }),
'Updated value should be visible in the grid'
).toBeVisible()
// Original value should be gone
await expect(
page.getByRole('gridcell', { name: 'original value' }),
'Original value should no longer be visible'
).not.toBeVisible()
// Should NOT show pending changes (queue is off)
await expect(
page.getByText('pending change'),
'No pending changes should appear when queue is disabled'
).not.toBeVisible()
})
test('editing multiple columns via side panel saves all changes', async ({ page, ref }) => {
const tableName = 'pw_table_multi_col_edit'
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)
}
)
const tableEditorLoadWait = createApiResponseWaiter(
page,
'pg-meta',
ref,
'query?key=entity-types-public-'
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await tableEditorLoadWait
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')
// Wait for the POST mutation to complete when saving
const updatePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByTestId('action-bar-save-row').click()
await updatePromise
// Both columns should reflect the updated values
await expect(
page.getByRole('gridcell', { name: 'Bob' }),
'First name should be updated to Bob'
).toBeVisible()
await expect(
page.getByRole('gridcell', { name: 'Jones' }),
'Last name should be updated to Jones'
).toBeVisible()
// Original values should be gone
await expect(
page.getByRole('gridcell', { name: 'Alice' }),
'Original first name should no longer be visible'
).not.toBeVisible()
await expect(
page.getByRole('gridcell', { name: 'Smith' }),
'Original last name should no longer be visible'
).not.toBeVisible()
})
test('row delete via context menu shows confirmation dialog', async ({ page, ref }) => {
const tableName = 'pw_table_row_delete'
const columnName = 'name'
await using _ = await withSetupCleanup(
async () => {
await createTable(tableName, columnName, [{ name: 'row to delete' }])
},
async () => {
await dropTable(tableName)
}
)
const tableEditorLoadWait = createApiResponseWaiter(
page,
'pg-meta',
ref,
'query?key=entity-types-public-'
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await tableEditorLoadWait
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()
// Right-click to open context menu and delete the row
const cell = page.getByRole('gridcell', { name: 'row to delete' })
await cell.click({ button: 'right' })
await page.getByRole('menuitem', { name: 'Delete row' }).click()
// In non-queue mode, a confirmation dialog should appear
const confirmDialog = page.getByRole('dialog', { name: 'Confirm to delete the selected row' })
await expect(
confirmDialog,
'Confirmation dialog should appear for non-queue row deletion'
).toBeVisible({ timeout: 10000 })
// Confirm the deletion
const deletePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await confirmDialog.getByRole('button', { name: 'Delete' }).click()
await deletePromise
// Row should be gone
await expect(
page.getByRole('gridcell', { name: 'row to delete' }),
'Deleted row should no longer be visible'
).not.toBeVisible()
// Should show 0 records
await expect(
page.getByText('0 records'),
'Table should show 0 records after deletion'
).toBeVisible()
// Should NOT show pending changes (queue is off)
await expect(
page.getByText('pending change'),
'No pending changes should appear when queue is disabled'
).not.toBeVisible()
})
test('create a table in a single transaction', async ({ page, ref }) => {
const tableName = 'pw_table_create_transaction'
await using _ = await withSetupCleanup(
async () => {
// Nothing
},
async () => {
await dropTable(tableName)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await page.getByRole('button', { name: 'New table' }).click()
await page.getByLabel('Name', { exact: true }).fill(tableName)
await page.getByRole('button', { name: 'Add column' }).click()
await page.getByLabel('Column name').nth(2).fill('pw_column')
await page.getByRole('combobox').filter({ hasText: 'Choose a column type...' }).click()
await page.getByRole('option').filter({ hasText: 'int8' }).click()
await page.getByLabel('Column default value').nth(2).fill('invalid')
await page.getByRole('button', { name: 'Save' }).click()
await expect(page.getByText('invalid input syntax')).toBeVisible()
await page.getByLabel('Column default value').nth(2).fill('10')
await page.getByRole('button', { name: 'Save' }).click()
await expect(page.getByText(`Table ${tableName} is good to go!`)).toBeVisible()
await expect(page.getByRole('button', { name: `View ${tableName}`, exact: true })).toBeVisible()
// copies table schema to clipboard when copy schema option is clicked
await page
.getByRole('button', { name: `View ${tableName}`, exact: true })
.getByRole('button')
.nth(2)
.click()
await page.getByRole('menuitem', { name: 'Copy table schema' }).click()
await expectClipboardValue({
page,
value: `create table public.${tableName} (
id bigint generated by default as identity not null,
created_at timestamp with time zone not null default now(),
pw_column bigint null default '10'::bigint,
constraint ${tableName}_pkey primary key (id)
) TABLESPACE pg_default;`,
exact: true,
})
})
test('inserting a row with NULL value for nullable text column with default', async ({
page,
ref,
}) => {
const tableName = 'pw_table_null_text_insert'
const columnName = 'description'
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(),
${columnName} text null default 'hello world'
)`
)
},
async () => {
await dropTable(tableName)
}
)
const tableEditorLoadWait = createApiResponseWaiter(
page,
'pg-meta',
ref,
'query?key=entity-types-public-'
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await tableEditorLoadWait
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
// Open side panel to insert a new row
await page.getByTestId('table-editor-insert-new-row').click()
await page.getByRole('menuitem', { name: 'Insert row' }).click()
// Open the field actions dropdown and set the text column to NULL
await page.getByTestId(`${columnName}-field-actions`).click()
await page.getByRole('menuitem', { name: 'Set to NULL' }).click()
// Save the row
const insertPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
method: 'POST',
})
await page.getByTestId('action-bar-save-row').click()
await insertPromise
// Should show success toast
await expect(
page.getByText('Successfully created row'),
'Success toast should appear after row creation'
).toBeVisible({ timeout: 10000 })
// The new row's text column should show NULL in the grid
await expect(
page.getByRole('gridcell', { name: 'NULL' }).first(),
'Text column should display NULL after setting to NULL via row editor'
).toBeVisible()
})
test('copying cell content from referencing record peek copies the correct value', async ({
page,
ref,
}) => {
const targetTable = 'pw_fk_peek_target'
const sourceTable = 'pw_fk_peek_source'
await using _ = await withSetupCleanup(
async () => {
await query(`
CREATE TABLE IF NOT EXISTS ${targetTable} (
id bigint generated by default as identity primary key,
name text
)
`)
await query(`ALTER TABLE public.${targetTable} ENABLE ROW LEVEL SECURITY`)
await query(`INSERT INTO ${targetTable} (name) VALUES ('target_value')`)
await query(`
CREATE TABLE IF NOT EXISTS ${sourceTable} (
id bigint generated by default as identity primary key,
label text,
target_id bigint references ${targetTable}(id)
)
`)
await query(`ALTER TABLE public.${sourceTable} ENABLE ROW LEVEL SECURITY`)
await query(`INSERT INTO ${sourceTable} (label, target_id) VALUES ('source_label', 1)`)
},
async () => {
await dropTable(sourceTable)
await dropTable(targetTable)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await page.getByRole('button', { name: `View ${sourceTable}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
await expect(page.getByRole('grid')).toBeVisible()
await expect(page.getByRole('gridcell', { name: 'source_label' })).toBeVisible()
await page.getByRole('button', { name: 'View referencing record' }).click()
const popover = page.getByText(`Referencing record from public.${targetTable}`)
await expect(popover).toBeVisible()
const popoverContent = page.locator('[data-radix-popper-content-wrapper]')
await expect(popoverContent.getByRole('gridcell', { name: 'target_value' })).toBeVisible()
// Right-click on the target_value cell inside the popover to open the peek context menu.
// Before the fix, this would trigger the main grid's context menu via React portal
// event bubbling, and "Copy cell" would copy the wrong value from the main grid.
await popoverContent.getByRole('gridcell', { name: 'target_value' }).click({ button: 'right' })
// The peek grid's own context menu should appear with "Copy cell"
await page.getByRole('menuitem', { name: 'Copy cell' }).click()
// Verify the correct referenced value was copied (not the main grid's FK value "1")
await expectClipboardValue({
page,
value: 'target_value',
exact: true,
})
})
test('Can double-click a column name to copy it', async ({ page, ref }) => {
const tableName = 'pw_table_column_click'
// create table + verify that this exists.
await using _ = await withSetupCleanup(
() => createTableWithRLS(tableName, 'pw_column'),
async () => {
await dropTable(tableName)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await page.getByLabel(`View ${tableName}`).click()
await expect(page.getByRole('button', { name: 'pw_column', exact: true })).toBeVisible()
await page.getByRole('button', { name: 'pw_column', exact: true }).dblclick()
await page.keyboard.press('ControlOrMeta+C')
await expectClipboardValue({
page,
value: 'pw_column',
exact: true,
})
})
})