Files
supabase/e2e/studio/features/table-editor.spec.ts
T
CharisandAli Waseem 044a575552 test: parallelize table editor tests (#40404)
Table editor tests were being run serially because they would otherwise
stomp all over each other in setup. But this made them really slow. This
PR changes them to run in parallel, and makes sure the setup step runs
only once by using a file lock.

Also cleans up miscellaneous other sources of flakiness in tests.

Co-authored-by: Ali Waseem <waseema393@gmail.com>
2025-11-19 07:14:13 -07:00

721 lines
33 KiB
TypeScript

import { expect, Locator, Page } from '@playwright/test'
import fs from 'fs'
import path from 'path'
import { isCLI } from '../utils/is-cli.js'
import { releaseFileOnceCleanup, withFileOnceSetup } from '../utils/once-per-file.js'
import { resetLocalStorage } from '../utils/reset-local-storage.js'
import { test } from '../utils/test.js'
import { toUrl } from '../utils/to-url.js'
import { waitForApiResponseWithTimeout } from '../utils/wait-for-response-with-timeout.js'
import {
waitForApiResponse,
waitForGridDataToLoad,
waitForTableToLoad,
} from '../utils/wait-for-response.js'
const tableNamePrefix = 'pw_table'
const columnName = 'pw_column'
const dismissToastsIfAny = async (page: Page) => {
const closeButtons = page.getByRole('button', { name: 'Close toast' })
const count = await closeButtons.count()
for (let i = 0; i < count; i++) {
await closeButtons.nth(i).click()
}
}
const createTable = async (page: Page, ref: string, tableName: string) => {
// Ensure no toast overlays block the dialog trigger
await dismissToastsIfAny(page)
const newTableBtn = page.getByRole('button', { name: 'New table', exact: true })
await expect(newTableBtn).toBeVisible()
await newTableBtn.click()
// Check if we're in the unable to find table error
// If so, click Close tab or Head back first
const closeTabBtn = page.getByRole('button', { name: 'Close tab' })
const headBackBtn = page.getByRole('button', { name: 'Head back' })
if ((await closeTabBtn.count()) > 0) {
await closeTabBtn.click()
}
if ((await headBackBtn.count()) > 0) {
await headBackBtn.click()
}
const nameInput = page.getByTestId('table-name-input')
await expect(nameInput).toBeVisible()
await nameInput.fill(tableName)
await expect(nameInput).toHaveValue(tableName)
await page.getByTestId('created_at-extra-options').click()
await page.getByRole('checkbox', { name: 'Is Nullable' }).click()
await page.getByTestId('created_at-extra-options').click({ force: true })
await page.getByRole('button', { name: 'Add column' }).click()
await page.getByRole('textbox', { name: 'column_name' }).fill(columnName)
await page.getByText('Choose a column type...').click()
await page.getByRole('option', { name: 'text Variable-length' }).click()
const createTablePromise = waitForApiResponseWithTimeout(page, (response) =>
response.url().includes('query?key=table-create')
)
// Wait specifically for tables list refresh instead of generic networkidle
const tablesPromise = waitForApiResponseWithTimeout(page, (response) =>
response.url().includes('tables?include_columns=true&included_schemas=public')
)
// wait for tables to load, we don't need to wait here cause this response may complete before the table creation.
const entitiesPromise = waitForApiResponseWithTimeout(page, (response) =>
response.url().includes('query?key=entity-types-public-')
)
await page.getByRole('button', { name: 'Save' }).click()
await Promise.all([createTablePromise, tablesPromise, entitiesPromise])
await page.waitForSelector('[data-testid="table-editor-side-panel"]', { state: 'detached' })
await expect(
page.getByRole('button', { name: `View ${tableName}`, exact: true }),
'Table should be visible after creation'
).toBeVisible({ timeout: 15_000 })
}
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) => {
await waitForApiResponse(page, 'pg-meta', ref, 'types')
// 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()
await page.getByRole('button', { name: 'Confirm delete' }).click()
await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' })
}
test.describe('table editor', () => {
test.beforeAll(async ({ browser, ref }) => {
await withFileOnceSetup(import.meta.url, async () => {
const ctx = await browser.newContext()
const page = await ctx.newPage()
const loadPromise = waitForTableToLoad(page, ref)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await loadPromise
const viewButtons = page.getByRole('button', { name: /^View / })
const names = await Promise.all(
(await viewButtons.all()).map(async (btn) => {
const ariaLabel = await btn.getAttribute('aria-label')
const name = ariaLabel ? ariaLabel.replace(/^View\s+/, '').trim() : ''
return name
})
)
const tablesToDelete = names.filter((tableName) => tableName.startsWith(tableNamePrefix))
for (const tableName of tablesToDelete) {
await deleteTable(page, ref, tableName)
await expect
.poll(async () => {
return await page.getByLabel(`View ${tableName}`, { exact: true }).count()
})
.toBe(0)
}
})
})
test.beforeEach(async ({ page, ref }) => {
const loadPromise = waitForTableToLoad(page, ref)
page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await loadPromise
})
test.afterAll(async () => {
await releaseFileOnceCleanup(import.meta.url)
})
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 createTable(page, ref, tableNameActions)
// copies table name to clipboard when copy table name is clicked
await page.getByRole('button', { name: `View ${tableNameActions}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
await page
.getByRole('button', { name: `View ${tableNameActions}`, exact: true })
.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('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(1)
.click()
await page.getByRole('menuitem', { name: 'Copy table schema' }).click()
await waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-definition-') // wait for endpoint to generate schema
await page.waitForTimeout(500)
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;`)
// duplicates table
await page
.getByRole('button', { name: `View ${tableNameActions}`, exact: true })
.getByRole('button')
.nth(1)
.click()
await page.getByRole('menuitem', { name: 'Duplicate table' }).click()
await page.getByRole('button', { name: 'Save' }).click()
await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' }) // 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'
// 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 createTable(page, ref, tableNameRlsEnabled)
await page.getByRole('button', { name: `View ${tableNameRlsEnabled}` }).click()
await expect(page.getByRole('link', { name: 'Add RLS policy' })).toBeVisible()
// create table with RLS disabled and verify
await page.getByRole('button', { name: 'New table', exact: true }).click()
await page.getByTestId('table-name-input').fill(tableNameRlsDisabled)
await page.getByLabel('Enable Row Level Security (').click()
await page.getByRole('button', { name: 'Confirm' }).click()
const apiPromise = waitForApiResponse(
page,
'pg-meta',
ref,
'tables?include_columns=false&included_schemas=public'
) // wait for table creation
await page.getByRole('button', { name: 'Save' }).click()
await apiPromise
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'
await page.goto(toUrl(`/project/${ref}/database/types?schema=public`))
// delete enum if it exists
await deleteEnumIfExist(page, ref, enum_name)
// create a new enum
await page.getByRole('button', { name: 'Create type' }).click()
await page.getByRole('textbox', { name: 'Name' }).fill(enum_name)
await page.locator('input[name="values.0.value"]').fill('value1')
await page.getByRole('button', { name: 'Add value' }).click()
await page.locator('input[name="values.1.value"]').fill('value2')
await page.getByRole('button', { name: 'Create type' }).click()
await waitForApiResponse(page, 'pg-meta', ref, 'types')
// 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 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.getByRole('textbox', { name: 'column_name' }).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: 10_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)
})
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 createTable(page, ref, tableNameGridEditor)
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)
await page.getByTestId('action-bar-save-row').click()
await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' }) // 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(1)
.click()
await page.getByRole('menuitem', { name: 'Edit table' }).click()
await page.getByTestId('table-name-input').fill(tableNameUpdated)
await page.getByRole('textbox', { name: 'pw_column' }).fill(columnNameUpdated)
await page.getByRole('button', { name: 'Save' }).click()
await waitForApiResponse(page, 'pg-meta', ref, 'query?key=column-update', { method: 'POST' }) // 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: columnName, exact: true })
).not.toBeVisible()
// test export data via csv
await page
.getByRole('button', { name: `View ${tableNameUpdated}`, exact: true })
.getByRole('button')
.nth(1)
.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(1)
.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(1)
.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'
if (!page.url().includes('/editor')) {
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await waitForTableToLoad(page, ref)
}
await createTable(page, ref, tableName)
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('button', { name: 'id', exact: true }).click()
await page.getByRole('menuitem', { name: colName }).click()
await page.getByRole('textbox', { name: 'Enter a value' }).fill('789')
await page.getByRole('button', { name: 'Apply filter' }).click()
await waitForGridDataToLoad(page, ref)
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()
await deleteTable(page, ref, tableName)
})
test('view table definition works as expected', async ({ page, ref }) => {
const tableName = 'pw_table_definition'
const colName = 'pw_column'
if (!page.url().includes('/editor')) {
const tableLoadPromise = waitForTableToLoad(page, ref)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await tableLoadPromise
}
await createTable(page, ref, tableName)
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
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;`
)
await deleteTable(page, ref, tableName)
})
test('sorting rows works as expected', async ({ page, ref }) => {
const tableName = 'pw_table_sorting'
const colName = 'pw_column'
// Ensure we're on editor
if (!page.url().includes('/editor')) {
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await waitForTableToLoad(page, ref)
}
// Create a small table and three rows
await createTable(page, ref, tableName)
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)
await page.getByTestId('action-bar-save-row').click()
await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' })
}
// 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()
await page.getByRole('button', { name: 'Apply sorting' }).click()
await waitForGridDataToLoad(page, ref)
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')
// Cleanup
await deleteTable(page, ref, tableName)
})
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 createTable(page, ref, tableNameDataActions)
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()
await page.getByRole('button', { name: 'Import data' }).click()
await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' }) // 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()
await page.getByRole('button', { name: 'Import data' }).click()
await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' }) // 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()
let footer: Locator
if (isCLI()) {
footer = page.getByLabel('Table grid footer')
} else {
footer = page.locator('[data-sentry-component="GridFooter"]')
}
await footer.getByRole('button').nth(1).click()
await waitForGridDataToLoad(page, ref) // 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: 'rows' }).click()
await page.getByRole('menuitem', { name: '500 rows' }).click()
await waitForGridDataToLoad(page, ref) // 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()
await page.getByRole('button', { name: 'Delete' }).click()
await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' }) // 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(
`[{"idx":0,"id":4,"created_at":"2025-01-01 12:00:00+00","pw_column":"value 4 to export"},{"idx":1,"id":5,"created_at":"2025-01-01 12:00:00+00","pw_column":"value 5 to export"},{"idx":2,"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 = [
columnName,
'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()
await page.getByRole('button', { name: 'Delete' }).click()
await expect(page.getByText('delete the selected 98 rows')).toBeVisible()
await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' }) // 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()
await deleteTable(page, ref, tableNameDataActions)
})
})