chore: add final e2e tests for table-editor and sql-editor (#36917)

* chore: add final e2e tests for table-editor and sql-editor

* chore: update tests to run in staging

* chore: minor updates

* chore: fix PR feedback

---------

Co-authored-by: Jordi Enric <37541088+jordienr@users.noreply.github.com>
This commit is contained in:
Michael Ong
2025-07-19 18:58:25 +08:00
committed by GitHub
parent 43c0c25918
commit 7b91d64b7e
4 changed files with 357 additions and 53 deletions

View File

@@ -10,6 +10,8 @@ Edit the `.env.local` file with your credentials and environment.
### Install the playwright browser
⚠️ This should be done in the `e2e/studio` directory
```bash
pnpm exec playwright install
```

View File

@@ -1,55 +1,159 @@
import { expect } from '@playwright/test'
import { env } from '../env.config'
import { expect, Page } from '@playwright/test'
import { isCLI } from '../utils/is-cli'
import { test } from '../utils/test'
import { toUrl } from '../utils/to-url'
test.describe('SQL Editor', () => {
test('should check if SQL editor can run simple commands', async ({ page }) => {
await page.goto(toUrl(`/project/${env.PROJECT_REF}/sql/new?skip=true`))
const deleteQuery = async (page: Page, queryName: string) => {
const privateSnippet = page.getByLabel('private-snippets')
await privateSnippet.getByText(queryName).first().click({ button: 'right' })
await page.getByRole('menuitem', { name: 'Delete query' }).click()
await expect(page.getByRole('heading', { name: 'Confirm to delete query' })).toBeVisible()
await page.getByRole('button', { name: 'Delete 1 query' }).click()
}
const editor = page.getByRole('code').nth(0)
test.describe('SQL Editor', () => {
let page: Page
const pwTestQueryName = 'pw-test-query'
test.beforeAll(async ({ browser, ref }) => {
test.setTimeout(60000)
// Create a new table for the tests
page = await browser.newPage()
await page.goto(toUrl(`/project/${ref}/sql/new?skip=true`))
await page.evaluate((ref) => {
localStorage.removeItem('dashboard-history-default')
localStorage.removeItem(`dashboard-history-${ref}`)
}, ref)
// intercept AI title generation to prevent flaky tests
await page.route('**/dashboard/api/ai/sql/title-v2', async (route) => {
await route.abort()
})
})
test.beforeEach(async ({ ref }) => {
if ((await page.getByLabel('private-snippets').count()) === 0) {
return
}
// since in local, we don't have access to the supabase platform, reloading would reload all the sql snippets.
if (isCLI()) {
await page.reload()
}
// remove sql snippets for - "Untitled query" and "pw test query"
const privateSnippet = page.getByLabel('private-snippets')
let privateSnippetText = await privateSnippet.textContent()
while (privateSnippetText.includes('Untitled query')) {
deleteQuery(page, 'Untitled query')
await page.waitForResponse(
(response) =>
(response.url().includes(`projects/${ref}/content`) ||
response.url().includes('projects/default/content')) &&
response.request().method() === 'DELETE'
)
await expect(
page.getByText('Successfully deleted 1 query'),
'Delete confirmation toast should be visible'
).toBeVisible({
timeout: 50000,
})
await page.waitForTimeout(1000)
privateSnippetText =
(await page.getByLabel('private-snippets').count()) > 0
? await privateSnippet.textContent()
: ''
}
while (privateSnippetText.includes(pwTestQueryName)) {
deleteQuery(page, pwTestQueryName)
await page.waitForResponse(
(response) =>
(response.url().includes(`projects/${ref}/content`) ||
response.url().includes('projects/default/content')) &&
response.request().method() === 'DELETE'
)
await expect(
page.getByText('Successfully deleted 1 query'),
'Delete confirmation toast should be visible'
).toBeVisible({
timeout: 50000,
})
await page.waitForTimeout(1000)
privateSnippetText =
(await page.getByLabel('private-snippets').count()) > 0
? await privateSnippet.textContent()
: ''
}
})
test('should check if SQL editor can run simple commands', async () => {
await page.getByTestId('sql-editor-new-query-button').click()
await page.getByRole('menuitem', { name: 'Create a new snippet' }).click()
// write some sql in the editor
// This has to be done since the editor is not editable (input, textarea, etc.)
await editor.click()
await page.waitForTimeout(1000)
const editor = page.getByRole('code').nth(0)
await editor.click()
await page.keyboard.press('ControlOrMeta+KeyA')
await page.keyboard.type(`select 'hello world';`)
await page.getByTestId('sql-run-button').click()
await page.getByRole('button', { name: /^Run( CTRL)?$/, exact: false }).click()
// verify the result
await expect(page.getByRole('gridcell', { name: 'hello world' })).toBeVisible({
timeout: 5000,
})
// Should say "Running..."
await expect(page.getByText('Running...')).toBeVisible()
// SQL written in the editor should not be the previous query.
await page.waitForTimeout(1000)
await editor.click()
await page.keyboard.press('ControlOrMeta+KeyA')
await page.keyboard.type(`select length('hello');`)
await page.getByTestId('sql-run-button').click()
// Wait until Running... is not visible
await expect(page.getByText('Running...')).not.toBeVisible()
// verify the result is updated.
await expect(page.getByRole('gridcell', { name: '5' })).toBeVisible({
timeout: 5000,
})
})
// clear the editor
test('destructive query would tripper a warning modal', async () => {
await page.getByTestId('sql-editor-new-query-button').click()
await page.getByRole('menuitem', { name: 'Create a new snippet' }).click()
// write some sql in the editor
// This has to be done since the editor is not editable (input, textarea, etc.)
await page.waitForTimeout(1000)
const editor = page.getByRole('code').nth(0)
await editor.click()
await page.keyboard.press('ControlOrMeta+KeyA')
await page.keyboard.type(`delete table 'test';`)
await page.getByTestId('sql-run-button').click()
// verify warning modal is visible
expect(page.getByRole('heading', { name: 'Potential issue detected with' })).toBeVisible()
expect(page.getByText('Query has destructive')).toBeVisible()
// reset test
await page.getByRole('button', { name: 'Cancel' }).click()
await page.waitForTimeout(500)
await editor.click()
await page.keyboard.press('ControlOrMeta+KeyA')
await page.keyboard.press('Backspace')
// verify the result
const result = page.getByRole('gridcell', { name: 'hello world' })
await expect(result).toBeVisible()
})
})
test.describe('SQL Snippets', () => {
test('should create and load a new snippet', async ({ page }) => {
await page.goto(toUrl(`/project/${env.PROJECT_REF}/sql`))
const addButton = page.getByTestId('sql-editor-new-query-button')
test('should create and load a new snippet', async ({ ref }) => {
const runButton = page.getByTestId('sql-run-button')
await page.getByRole('button', { name: 'Favorites' }).click()
await page.getByRole('button', { name: 'Shared' }).click()
await expect(page.getByText('No shared queries')).toBeVisible()
await expect(page.getByText('No favorite queries')).toBeVisible()
// write some sql in the editor
await addButton.click()
await page.getByTestId('sql-editor-new-query-button').click()
await page.getByRole('menuitem', { name: 'Create a new snippet' }).click()
const editor = page.getByRole('code').nth(0)
await page.waitForTimeout(1000)
await editor.click()
@@ -77,25 +181,52 @@ test.describe('SQL Snippets', () => {
await privateSnippet.getByText('Untitled query').click({ button: 'right' })
await page.getByRole('menuitem', { name: 'Rename query', exact: true }).click()
await expect(page.getByRole('heading', { name: 'Rename' })).toBeVisible()
await page.getByRole('textbox', { name: 'Name' }).fill('test snippet')
await page.getByRole('textbox', { name: 'Name' }).fill(pwTestQueryName)
await page.getByRole('button', { name: 'Rename query', exact: true }).click()
const privateSnippet2 = privateSnippet.getByText('test snippet', { exact: true })
await expect(privateSnippet2).toBeVisible()
await page.waitForResponse(
(response) =>
(response.url().includes(`projects/${ref}/content`) ||
response.url().includes('projects/default/content')) &&
response.request().method() === 'PUT' &&
response.status().toString().startsWith('2')
)
await expect(privateSnippet.getByText(pwTestQueryName, { exact: true })).toBeVisible({
timeout: 50000,
})
const privateSnippet2 = await privateSnippet.getByText(pwTestQueryName, { exact: true })
// share with a team
await privateSnippet2.click({ button: 'right' })
await page.getByRole('menuitem', { name: 'Share query with team' }).click()
await expect(page.getByRole('heading', { name: 'Confirm to share query: test' })).toBeVisible()
await expect(page.getByRole('heading', { name: 'Confirm to share query' })).toBeVisible()
await page.getByRole('button', { name: 'Share query', exact: true }).click()
await page.waitForResponse(
(response) =>
(response.url().includes(`projects/${ref}/content`) ||
response.url().includes('projects/default/content')) &&
response.request().method() === 'PUT' &&
response.status().toString().startsWith('2')
)
const sharedSnippet = await page.getByLabel('project-level-snippets')
await expect(sharedSnippet).toContainText('test snippet')
await expect(sharedSnippet).toContainText(pwTestQueryName)
// unshare a snippet
await sharedSnippet.getByText('test snippet').click({ button: 'right' })
await sharedSnippet.getByText(pwTestQueryName).click({ button: 'right' })
await page.getByRole('menuitem', { name: 'Unshare query with team' }).click()
await expect(page.getByRole('heading', { name: 'Confirm to unshare query:' })).toBeVisible()
await page.getByRole('button', { name: 'Unshare query', exact: true }).click()
await expect(sharedSnippet).not.toBeVisible()
// delete snippet (for non-local environment)
if (!isCLI()) {
deleteQuery(page, pwTestQueryName)
await expect(
page.getByText('Successfully deleted 1 query'),
'Delete confirmation toast should be visible'
).toBeVisible({
timeout: 50000,
})
}
})
})

View File

@@ -1,4 +1,5 @@
import { expect, Page } from '@playwright/test'
import fs from 'fs'
import { test } from '../utils/test'
import { toUrl } from '../utils/to-url'
@@ -15,8 +16,9 @@ const getSelectors = (tableName: string) => ({
saveBtn: (page) => page.getByRole('button', { name: 'Save' }),
definitionTab: (page) => page.getByText('definition', { exact: true }),
viewLines: (page) => page.locator('div.view-lines'),
insertRowBtn: (page) => page.getByTestId('table-editor-insert-new-row'),
insertModal: (page) => page.getByText('Insert a new row into'),
insertBtn: (page) => page.getByTestId('table-editor-insert-new-row'),
insertRow: (page) => page.getByText('Insert a new row into'),
insertColumn: (page) => page.getByText('Insert a new column into'),
defaultValueInput: (page) => page.getByTestId('defaultValueColumn-input'),
actionBarSaveRow: (page) => page.getByTestId('action-bar-save-row'),
grid: (page) => page.getByRole('grid'),
@@ -61,9 +63,6 @@ const createTable = async (page: Page, tableName: string) => {
await s.saveBtn(page).click()
// wait till we see the success toast
// Text: Table tableName is good to go!
await expect(
page.getByText(`Table ${tableName} is good to go!`),
'Success toast should be visible after table creation'
@@ -77,7 +76,7 @@ const createTable = async (page: Page, tableName: string) => {
).toBeVisible()
}
const deleteTables = async (page: Page, tableName: string) => {
const deleteTable = async (page: Page, tableName: string) => {
const s = getSelectors(tableName)
await page.waitForTimeout(500)
@@ -94,11 +93,35 @@ const deleteTables = async (page: Page, tableName: string) => {
).toBeVisible()
}
const deleteEnum = async (page: Page, enumName: string, ref: string) => {
// give it a second for interactions to load
await page.waitForResponse(
(response) =>
response.url().includes(`pg-meta/${ref}/types`) ||
response.url().includes('pg-meta/default/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 expect(page.getByText(`Successfully deleted "${enumName}"`)).toBeVisible()
}
test.describe('Table Editor', () => {
let page: Page
const testTableName = `pw-test-table-editor`
const tableNameRlsEnabled = `pw-test-rls-enabled`
const tableNameRlsDisabled = `pw-test-rls-disabled`
const tableNameEnum = `pw-test-enum`
const tableNameCsv = `pw-test-csv`
test.beforeAll(async ({ browser, ref }) => {
test.setTimeout(60000)
@@ -111,18 +134,22 @@ test.describe('Table Editor', () => {
await page.waitForTimeout(2000)
// delete table name if it exists
await deleteTables(page, testTableName)
await deleteTables(page, tableNameRlsEnabled)
await deleteTables(page, tableNameRlsDisabled)
await deleteTable(page, testTableName)
await deleteTable(page, tableNameRlsEnabled)
await deleteTable(page, tableNameRlsDisabled)
await deleteTable(page, tableNameEnum)
await deleteTable(page, tableNameCsv)
})
test.afterAll(async () => {
test.setTimeout(60000)
// delete all tables related to this test
await deleteTables(page, testTableName)
await deleteTables(page, tableNameRlsEnabled)
await deleteTables(page, tableNameRlsDisabled)
await deleteTable(page, testTableName)
await deleteTable(page, tableNameRlsEnabled)
await deleteTable(page, tableNameRlsDisabled)
await deleteTable(page, tableNameEnum)
await deleteTable(page, tableNameCsv)
})
test('should perform all table operations sequentially', async ({ ref }) => {
@@ -143,14 +170,14 @@ test.describe('Table Editor', () => {
// 2. Insert test data
await page.getByRole('button', { name: `View ${testTableName}` }).click()
await s.insertRowBtn(page).click()
await s.insertModal(page).click()
await s.insertBtn(page).click()
await s.insertRow(page).click()
await s.defaultValueInput(page).fill('100')
await s.actionBarSaveRow(page).click()
await page.getByRole('button', { name: `View ${testTableName}` }).click()
await s.insertRowBtn(page).click()
await s.insertModal(page).click()
await s.insertBtn(page).click()
await s.insertRow(page).click()
await s.defaultValueInput(page).fill('4')
await s.actionBarSaveRow(page).click()
@@ -225,7 +252,7 @@ test.describe('Table Editor', () => {
'Tables list should be visible in public schema'
).toBeVisible()
await deleteTables(page, testTableName)
await deleteTable(page, testTableName)
})
test('should show rls accordingly', async () => {
@@ -253,7 +280,140 @@ test.describe('Table Editor', () => {
await page.getByRole('button', { name: `View ${tableNameRlsDisabled}` }).click()
await expect(page.getByRole('button', { name: 'RLS disabled' })).toBeVisible()
await deleteTables(page, tableNameRlsEnabled)
await deleteTables(page, tableNameRlsDisabled)
await deleteTable(page, tableNameRlsEnabled)
await deleteTable(page, tableNameRlsDisabled)
})
test('add enums and show enums on table', async ({ ref }) => {
const ENUM_NAME = 'test_enum'
const ENUM_COLUMN_NAME = 'test_column'
// clear local storage, as it might result in some flakiness
await page.evaluate((ref) => {
localStorage.removeItem('dashboard-history-default')
localStorage.removeItem(`dashboard-history-${ref}`)
}, ref)
await page.goto(toUrl(`/project/${ref}/database/types?schema=public`))
// delete enum if it exists
await deleteEnum(page, ENUM_NAME, ref)
// 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()
// Wait for enum response to be completed
await page.waitForResponse(
(response) =>
response.url().includes(`pg-meta/${ref}/types`) ||
response.url().includes('pg-meta/default/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`))
const s = getSelectors(tableNameEnum)
await s.newTableBtn(page).click()
await s.tableNameInput(page).fill(tableNameEnum)
await s.createdAtExtraOptions(page).click()
await page.getByText('Is Nullable').click()
await s.createdAtExtraOptions(page).click()
await s.addColumnBtn(page).click()
await s.columnNameInput(page).fill(ENUM_COLUMN_NAME)
await page.getByRole('combobox').filter({ hasText: 'Choose a column type...' }).click()
await page.getByPlaceholder('Search types...').fill(ENUM_NAME)
await page.getByRole('option', { name: ENUM_NAME }).click()
await s.saveBtn(page).click()
await expect(
page.getByText(`Table ${tableNameEnum} is good to go!`),
'Success toast should be visible after table creation'
).toBeVisible({
timeout: 50000,
})
// Wait for the grid to be visible and data to be loaded
await expect(s.grid(page), 'Grid should be visible after inserting data').toBeVisible()
await expect(page.getByRole('columnheader', { name: ENUM_NAME })).toBeVisible()
// insert row with enum value
await s.insertBtn(page).click()
await s.insertRow(page).click()
await page.getByRole('combobox').selectOption('value1')
await s.actionBarSaveRow(page).click()
await expect(page.getByRole('gridcell', { name: 'value1' })).toBeVisible()
// insert row with another enum value
await s.insertBtn(page).click()
await s.insertRow(page).click()
await page.getByRole('combobox').selectOption('value2')
await s.actionBarSaveRow(page).click()
await expect(page.getByRole('gridcell', { name: 'value2' })).toBeVisible()
// delete enum and enum table
await deleteTable(page, tableNameEnum)
await page.goto(toUrl(`/project/${ref}/database/types?schema=public`))
await deleteEnum(page, ENUM_NAME, ref)
// should end at the init link
// clear local storage, as it might result in some flakiness
await page.evaluate((ref) => {
localStorage.removeItem('dashboard-history-default')
localStorage.removeItem(`dashboard-history-${ref}`)
}, ref)
await page.goto(toUrl(`/project/${ref}/editor`))
})
test('csv import works properly', async () => {
// create a new table and insert some data
await createTable(page, tableNameCsv)
const s = getSelectors(tableNameCsv)
await page.getByRole('button', { name: `View ${tableNameCsv}` }).click()
await s.insertBtn(page).click()
await s.insertRow(page).click()
await s.defaultValueInput(page).fill('123')
await s.actionBarSaveRow(page).click()
await s.insertBtn(page).click()
await s.insertRow(page).click()
await s.defaultValueInput(page).fill('456')
await s.actionBarSaveRow(page).click()
await s.insertBtn(page).click()
await s.insertRow(page).click()
await s.defaultValueInput(page).fill('789')
await s.actionBarSaveRow(page).click()
// download csv
const tableBtn = await page.getByRole('button', { name: 'View pw-test-csv' })
await tableBtn.getByRole('button').last().click()
await page.getByRole('menuitem', { name: 'Export data' }).click()
const downloadPromise = page.waitForEvent('download')
await page.getByRole('menuitem', { name: 'Export table as CSV' }).click()
const download = await downloadPromise
expect(download.suggestedFilename()).toContain('.csv')
const downloadPath = await download.path()
// verify file contents
const csvContent = fs.readFileSync(downloadPath, '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 = ['defaultValueColumn', '123', '456', '789']
defaultColumnValues.forEach((expectedValue) => {
expect(expectedDefaultColumnValues).toContain(expectedValue)
})
// remove the downloaded file + clean up tables
fs.unlinkSync(downloadPath)
await deleteTable(page, tableNameCsv)
})
})

View File

@@ -0,0 +1,11 @@
import { env } from '../env.config'
/**
* Returns true if running in CLI/self-hosted mode (IS_PLATFORM=false),
* false if running in hosted mode (IS_PLATFORM=true).
*/
export function isCLI(): boolean {
// IS_PLATFORM=true = hosted mode
// IS_PLATFORM=false = CLI/self-hosted mode
return env.IS_PLATFORM === 'false'
}