Files
supabase/e2e/studio/features/table-editor.spec.ts
Gildas Garcia df7e98bae6 Fix table creation transaction (#43909)
Fixes #42089
Supersedes #43352 and
https://github.com/supabase/postgres-meta/pull/1049

## Problem

When creating a table in the Dashboard, if a column-specific error
occurs (e.g., invalid enum value or broken foreign key), the Dashboard
displays an error toast, but the side panel remains open. In the
background, the table is actually created (partially), leading to a
_42P07: relation already exists_ error if the user tries to click _Save_
again.

## Solution

We actually had nested transactions which is not supported by Postgres.

- [x] Allow generating SQL without transactions from `@supabase/pg-meta`
- [x] Fix the table creation
- [x] Fix some accessibility issues
- [x] Add tests

## How to test

1. Click _New table_ in the Table editor
2. Add a column with an _int8_ type and set its default value to
`bazinga`
3. Click _Save_

You should see an error.
4. Fix the default value by setting it to `10`
5. Click _Save_

Results:
- You should see a success message about the table creation
- The table should have the column with the correct default value
2026-03-19 08:06:54 +01:00

1262 lines
53 KiB
TypeScript

import fs from 'fs'
import path from 'path'
import { expect, Page } from '@playwright/test'
import { env } from '../env.config.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()
await page.waitForTimeout(500)
const copiedTableResult = await page.evaluate(() => navigator.clipboard.readText())
expect(copiedTableResult).toBe('pw_table_actions')
// 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(async () => {
const copiedSchemaResult = await page.evaluate(() => navigator.clipboard.readText())
expect(copiedSchemaResult).toBe(`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;`)
}).toPass({ timeout: 2000 })
// duplicates table
await page
.getByRole('button', { name: `View ${tableNameActions}`, exact: true })
.getByRole('button')
.nth(2)
.click()
await page.getByRole('menuitem', { name: 'Duplicate table' }).click()
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').selectOption('value1')
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').selectOption('value2')
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('filtering rows works as expected', async ({ page, ref }) => {
const tableName = 'pw_table_filtering'
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()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
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 apiPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' })
await page.getByTestId('action-bar-save-row').click()
await apiPromise
}
await page.getByRole('button', { name: 'Filter', exact: true }).click()
await page.getByRole('button', { name: 'Add filter' }).click()
await page.getByRole('dialog').getByRole('button', { name: 'id', exact: true }).click()
await page.getByRole('menuitem', { name: colName }).click()
await page.getByRole('textbox', { name: 'Enter a value' }).fill('789')
const waitForFilterApply = createApiResponseWaiter(
page,
'pg-meta',
ref,
'query?key=table-rows-'
)
await page.getByRole('button', { name: 'Apply filter' }).click()
await waitForFilterApply
await page.waitForTimeout(500)
await page.getByRole('button', { name: 'Filtered by 1 rule' }).click({ force: true })
await expect(page.getByRole('gridcell', { name: '789' })).toBeVisible()
await expect(page.getByRole('gridcell', { name: '456' })).not.toBeVisible()
await expect(page.getByRole('gridcell', { name: '123' })).not.toBeVisible()
})
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('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').nth(1).click()
await page.getByRole('menuitem', { name: 'Copy name' }).click()
await page.waitForTimeout(500)
const copiedTableResult = await page.evaluate(() => navigator.clipboard.readText())
expect(copiedTableResult).toBe(colName)
})
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()
await page.waitForTimeout(500)
// Verify first row value was copied
const firstCopiedValue = await page.evaluate(() => navigator.clipboard.readText())
expect(firstCopiedValue).toBe('first_row_value')
// 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()
await page.waitForTimeout(500)
// Verify second row value was copied
const secondCopiedValue = await page.evaluate(() => navigator.clipboard.readText())
expect(secondCopiedValue).toBe('second_row_value')
})
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.selectOption('true')
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.selectOption('false')
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.selectOption('null')
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.selectOption('false')
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('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 expect(async () => {
const copiedSchemaResult = await page.evaluate(() => navigator.clipboard.readText())
expect(copiedSchemaResult).toBe(`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;`)
}).toPass({ timeout: 2000 })
})
})