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, `query?key=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('protected schema empty tables do not expose CSV import actions', async ({ page, ref }) => { const emptyAuthTables = await query<{ relname: string }>(` select relname from pg_stat_user_tables where schemaname = 'auth' and n_live_tup = 0 and relname <> 'schema_migrations' order by relname limit 1; `) test.skip(emptyAuthTables.length === 0, 'Requires an empty auth table in the test dataset') const [{ relname: tableName }] = emptyAuthTables await page.goto(toUrl(`/project/${ref}/editor?schema=public`)) await page.getByTestId('schema-selector').click() await page.getByPlaceholder('Find schema...').fill('auth') const tableLoadPromise = waitForTableToLoad(page, ref, 'auth') await page.getByRole('option', { name: 'auth' }).click() await tableLoadPromise await expect(page.getByRole('button', { name: `View ${tableName}`, exact: true })).toBeVisible() await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click() await page.waitForURL(/\/editor\/\d+\?schema=auth$/) await expect(page.getByText('This table is empty')).toBeVisible() await expect(page.getByRole('button', { name: 'Import data from CSV' })).not.toBeVisible() await expect(page.getByText('or drag and drop a CSV file here')).not.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('copying cell values preserves false and zero', async ({ page, ref }) => { const tableName = 'pw_table_copy_falsy_values' await using _ = await withSetupCleanup( async () => { await query(` create table if not exists public.${tableName} ( bool_false boolean, bool_true boolean, zero_int integer ); `) await query(` insert into public.${tableName} (bool_false, bool_true, zero_int) values (false, true, 0); `) }, 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.getByRole('grid')).toBeVisible() const falseCell = page.getByRole('gridcell', { name: 'FALSE' }).first() await expect(falseCell).toBeVisible() await falseCell.click({ button: 'right' }) await page.getByRole('menuitem', { name: 'Copy cell' }).click() await expectClipboardValue({ page, value: 'false', exact: true }) const zeroCell = page.getByRole('gridcell', { name: '0' }).first() await expect(zeroCell).toBeVisible() await zeroCell.click({ button: 'right' }) await page.getByRole('menuitem', { name: 'Copy cell' }).click() await expectClipboardValue({ page, value: '0', exact: true }) const trueCell = page.getByRole('gridcell', { name: 'TRUE' }).first() await expect(trueCell).toBeVisible() await trueCell.click({ button: 'right' }) await page.getByRole('menuitem', { name: 'Copy cell' }).click() await expectClipboardValue({ page, value: 'true', 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 insert a row into a table with a generated column', async ({ page, ref }) => { const tableName = 'pw_table_generated_column' await using _ = await withSetupCleanup( async () => { await query(` create table public.${tableName} ( id bigint generated by default as identity primary key, base_price int, discounted_price int, is_discounted boolean generated always as ( base_price is not null and discounted_price is not null and base_price is distinct from discounted_price ) stored ); `) }, 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 a row, filling only the non-generated columns await page.getByTestId('table-editor-insert-new-row').click() await page.getByRole('menuitem', { name: 'Insert row' }).click() const sidePanel = page.getByTestId('side-panel-row-editor') await page.getByTestId('base_price-input').fill('100') await page.getByTestId('discounted_price-input').fill('80') const insertPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST', }) await page.getByTestId('action-bar-save-row').click() await insertPromise await expect( sidePanel, 'Row editor should close after inserting a row into a table with a generated column' ).not.toBeVisible() await expect( page.getByRole('gridcell', { name: 'TRUE' }), 'Generated column value should be computed by the database' ).toBeVisible() // the generated column should not be editable in the insert form await page.getByTestId('table-editor-insert-new-row').click() await page.getByRole('menuitem', { name: 'Insert row' }).click() await expect(sidePanel.getByText('base_price')).toBeVisible() await expect( sidePanel.getByText('is_discounted'), 'Generated column should not be shown in the insert form' ).not.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', exact: true }).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, }) }) })