Files
supabase/e2e/studio/features/table-editor.spec.ts
Gildas Garcia bc3dc73240 chore: migrate old <Select /> usage to the new Shadcn component (#45232)
## Problem

We want to reduce the code we ship and maintain.

## Solution

Migrate old `<Select />` usage to the new Shadcn component.

## Screenshots

### `www` Pricing 

Before:
<img width="637" height="697" alt="image"
src="https://github.com/user-attachments/assets/b6f261de-e587-411b-9408-faf94d709f1c"
/>

After:
<img width="644" height="756" alt="image"
src="https://github.com/user-attachments/assets/8cc4894c-64da-4e6a-960c-77cd162ac71d"
/>

### Observability

Before:
<img width="1015" height="452" alt="image"
src="https://github.com/user-attachments/assets/3d7e8613-e7a6-461d-a50d-e66c7c85fef1"
/>

After:
<img width="833" height="467" alt="image"
src="https://github.com/user-attachments/assets/98ace34f-25ec-48b5-aad3-fe812307b01d"
/>

### Docs Realtime

Used in pages:
- https://supabase.com/docs/guides/realtime/postgres-changes
- https://supabase.com/docs/guides/realtime/benchmarks

Before:
<img width="578" height="437" alt="image"
src="https://github.com/user-attachments/assets/22fa0048-be07-42e0-9153-65171fa3ccb9"
/>

After:
<img width="571" height="423" alt="image"
src="https://github.com/user-attachments/assets/e0adbde9-0c6f-48da-b377-516392185fb0"
/>

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

* **Refactor**
* Updated dropdown/select controls across the app to a consistent,
composable implementation
* Replaced advanced JWT generator in docs with a simplified JWT
generator component

* **Chores**
  * Removed legacy select component, associated styles and exports
  * Updated theme and tests to align with the new select implementation
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-27 15:35:50 +02:00

1888 lines
74 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 expect(page.getByRole('button', { name: 'RLS disabled' })).toBeVisible()
})
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
await page.goto(toUrl(`/project/${ref}/editor`))
await waitForTableToLoad(page, ref) // 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 a new row into').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 a new row into').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 Insert a new 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)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await waitForTableToLoad(page, ref)
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' })
await expect(openInSqlEditorLink).toHaveAttribute('href', /security_invoker%20%3D%20true/)
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 Insert a new 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 Insert a new 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 Insert a new 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 Insert a new 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 Insert a new 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 Insert a new 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 Insert a new 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()
// Open foreign key selector
await page.getByRole('button', { name: 'Add foreign key relation' }).click()
// Select schema (should default to public)
await expect(page.getByRole('button', { name: 'Select a schema' })).toContainText('public')
// Select target table
const tableQueryPromise = waitForApiResponseWithTimeout(page, (response) =>
response.url().includes(`table-public-${targetTableName}`)
)
await page.getByRole('button', { name: 'Select a table to reference to' }).click()
await page.getByRole('menuitem', { 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('button', { name: '---' }).first().click()
await page.getByRole('menuitem', { name: 'id int8' }).click()
// Wait for the first dropdown to update - there should only be one '---' button left now
await expect(page.getByRole('button', { name: '---' })).toHaveCount(1)
// Select target column (id from target table)
await page.getByRole('button', { name: '---' }).first().click()
await page.getByRole('menuitem', { 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'),
15000
)
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'),
15000
)
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('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};`)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await waitForTableToLoad(page, ref)
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 Insert a new 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 Insert a new 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)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await waitForTableToLoad(page, ref)
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 Insert a new 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)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await waitForTableToLoad(page, ref)
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)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await waitForTableToLoad(page, ref)
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)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await waitForTableToLoad(page, ref)
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)
}
)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await waitForTableToLoad(page, ref)
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 Insert a new 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,
})
})
})