mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 22:18:00 +08:00
Chore improve e2e tests (#43987)
## Problem Some tests rely on hard coded timeouts. That makes them - brittle if the timeout is not long enough - take longer than necessary if the timeout is too long ## Solution - Rely on playwright `expect` retries when possible - Rely on UI updates when possible
This commit is contained in:
@@ -117,10 +117,6 @@ await waitForApiResponse(page, 'pg-meta', ref, 'tables')
|
||||
// ✅ Acceptable - waiting for client-side debounce
|
||||
await page.getByRole('textbox').fill('search term')
|
||||
await page.waitForTimeout(300) // Allow debounce to complete
|
||||
|
||||
// ✅ Acceptable - waiting for clipboard API
|
||||
await page.evaluate(() => navigator.clipboard.readText())
|
||||
await page.waitForTimeout(500)
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
@@ -274,6 +270,22 @@ import {
|
||||
} from '../utils/wait-for-response.js'
|
||||
```
|
||||
|
||||
### Use the existing assertions utilities
|
||||
|
||||
#### Clipboard assertions
|
||||
|
||||
```ts
|
||||
// ❌ Avoid - brittle hard coded timeout
|
||||
await page.evaluate(() => navigator.clipboard.readText())
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// ✅ Good - this utility function uses Playwright auto-retries mechanisms
|
||||
await expectClipboardValue({
|
||||
page,
|
||||
value: 'expectedValue'
|
||||
})
|
||||
```
|
||||
|
||||
## API Mocking
|
||||
|
||||
### Mock APIs for isolated testing
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { env } from '../env.config.js'
|
||||
import { expectClipboardValue } from '../utils/clipboard.js'
|
||||
import { createTable, dropTable, query } from '../utils/db/index.js'
|
||||
import { test, withSetupCleanup } from '../utils/test.js'
|
||||
import { toUrl } from '../utils/to-url.js'
|
||||
@@ -39,13 +40,15 @@ test.describe('Database', () => {
|
||||
// copies schema definition to clipboard
|
||||
await page.getByRole('button', { name: 'Copy as SQL' }).click()
|
||||
await expect(page.getByTestId('copy-sql-ready')).toBeVisible()
|
||||
const clipboardText = await page.evaluate(() => navigator.clipboard.readText())
|
||||
expect(clipboardText).toContain(`CREATE TABLE public.${databaseTableName} (
|
||||
await expectClipboardValue({
|
||||
page,
|
||||
value: `CREATE TABLE public.${databaseTableName} (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now(),
|
||||
${databaseColumnName} text,
|
||||
CONSTRAINT ${databaseTableName}_pkey PRIMARY KEY (id)
|
||||
);`)
|
||||
);`,
|
||||
})
|
||||
|
||||
// downloads schema diagram when export is triggered
|
||||
const downloadPromise = page.waitForEvent('download')
|
||||
@@ -91,6 +94,7 @@ test.describe('Database', () => {
|
||||
await page.getByText(`${databaseTableName} actions`).click()
|
||||
await expect(page.getByRole('menuitem', { name: 'Edit table' })).toBeVisible()
|
||||
await page.getByRole('menuitem', { name: 'Edit table' }).click({ force: true })
|
||||
await expect(page.getByRole('menuitem', { name: 'Edit table' })).not.toBeVisible()
|
||||
const dialog = page.getByRole('dialog')
|
||||
await expect(dialog).toBeVisible()
|
||||
await expect(dialog.getByText('timestamptz')).toBeVisible()
|
||||
@@ -103,6 +107,7 @@ test.describe('Database', () => {
|
||||
await page.getByText(`${databaseTableName} actions`).click()
|
||||
await expect(page.getByRole('menuitem', { name: 'Edit table' })).toBeVisible()
|
||||
await page.getByRole('menuitem', { name: 'Edit table' }).click()
|
||||
await expect(page.getByRole('menuitem', { name: 'Edit table' })).not.toBeVisible()
|
||||
await expect(page.getByRole('dialog')).toBeVisible()
|
||||
// FIXME: For some reason, the dialog is not stable and rerenders, sometimes preventing the description to be filled
|
||||
await page.waitForTimeout(500)
|
||||
@@ -113,9 +118,8 @@ test.describe('Database', () => {
|
||||
await page.getByText(`${databaseTableName} actions`).click()
|
||||
await expect(page.getByRole('menuitem', { name: 'Copy name' })).toBeVisible()
|
||||
await page.getByRole('menuitem', { name: 'Copy name' }).click()
|
||||
await page.waitForTimeout(500)
|
||||
const copiedTableResult = await page.evaluateHandle(() => navigator.clipboard.readText())
|
||||
expect(await copiedTableResult.jsonValue()).toBe(databaseTableName)
|
||||
await expect(page.getByRole('menuitem', { name: 'Copy name' })).not.toBeVisible()
|
||||
await expectClipboardValue({ page, value: databaseTableName, exact: true })
|
||||
|
||||
await page.getByText(`${databaseTableName} actions`).click()
|
||||
await expect(page.getByRole('menuitem', { name: 'View in Table Editor' })).toBeVisible()
|
||||
@@ -175,9 +179,7 @@ test.describe('Database', () => {
|
||||
.click({ force: true })
|
||||
await expect(page.getByRole('menuitem', { name: 'Copy name' })).toBeVisible()
|
||||
await page.getByRole('menuitem', { name: 'Copy name' }).click()
|
||||
await page.waitForTimeout(500)
|
||||
const copiedTableResult = await page.evaluateHandle(() => navigator.clipboard.readText())
|
||||
expect(await copiedTableResult.jsonValue()).toBe(databaseColumnName)
|
||||
await expectClipboardValue({ page, value: databaseColumnName, exact: true })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { expect, Page } from '@playwright/test'
|
||||
import fs from 'fs'
|
||||
import { expect, Page } from '@playwright/test'
|
||||
|
||||
import { env } from '../env.config.js'
|
||||
import { expectClipboardValue } from '../utils/clipboard.js'
|
||||
import { isCLI } from '../utils/is-cli.js'
|
||||
import { resetLocalStorage } from '../utils/reset-local-storage.js'
|
||||
import { test } from '../utils/test.js'
|
||||
@@ -189,12 +191,8 @@ test.describe('SQL Editor', () => {
|
||||
await page.getByTestId('sql-run-button').click()
|
||||
|
||||
// verify warning modal blocks execution
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Potential issue detected with' })
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
page.getByText('Query will prevent connections to your database')
|
||||
).toBeVisible()
|
||||
await expect(page.getByRole('heading', { name: 'Potential issue detected with' })).toBeVisible()
|
||||
await expect(page.getByText('Query will prevent connections to your database')).toBeVisible()
|
||||
expect(queryDispatched).toBe(false)
|
||||
|
||||
// cancel should dismiss without executing
|
||||
@@ -233,12 +231,8 @@ test.describe('SQL Editor', () => {
|
||||
await page.getByTestId('sql-run-button').click()
|
||||
|
||||
// verify warning modal blocks execution
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Potential issue detected with' })
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
page.getByText('Query will prevent connections to your database')
|
||||
).toBeVisible()
|
||||
await expect(page.getByRole('heading', { name: 'Potential issue detected with' })).toBeVisible()
|
||||
await expect(page.getByText('Query will prevent connections to your database')).toBeVisible()
|
||||
expect(queryDispatched).toBe(false)
|
||||
|
||||
// cancel should dismiss without executing
|
||||
@@ -277,9 +271,7 @@ test.describe('SQL Editor', () => {
|
||||
await page.getByTestId('sql-run-button').click()
|
||||
|
||||
// verify warning modal blocks execution
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Potential issue detected with' })
|
||||
).toBeVisible()
|
||||
await expect(page.getByRole('heading', { name: 'Potential issue detected with' })).toBeVisible()
|
||||
await expect(page.getByText('Query uses update without a where clause')).toBeVisible()
|
||||
expect(queryDispatched).toBe(false)
|
||||
|
||||
@@ -332,27 +324,35 @@ test.describe('SQL Editor', () => {
|
||||
// export as markdown
|
||||
await page.getByRole('button', { name: 'Export' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Copy as markdown' }).click()
|
||||
await page.waitForTimeout(500)
|
||||
const copiedMarkdownResult = await page.evaluate(() => navigator.clipboard.readText())
|
||||
expect(copiedMarkdownResult).toBe(`| ?column? |
|
||||
// Make sure the dropdown has closed otherwise it would make the other assertions unstable
|
||||
await expect(page.getByRole('menuitem', { name: 'Copy as markdown' })).not.toBeVisible()
|
||||
await expectClipboardValue({
|
||||
page,
|
||||
value: `| ?column? |
|
||||
| ----------- |
|
||||
| hello world |`)
|
||||
| hello world |`,
|
||||
exact: true,
|
||||
})
|
||||
|
||||
// export as JSON
|
||||
await page.getByRole('button', { name: 'Export' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Copy as JSON' }).click()
|
||||
await page.waitForTimeout(500)
|
||||
const copiedJsonResult = await page.evaluate(() => navigator.clipboard.readText())
|
||||
expect(copiedJsonResult).toBe(`[
|
||||
await expect(page.getByRole('menuitem', { name: 'Copy as JSON' })).not.toBeVisible()
|
||||
await expectClipboardValue({
|
||||
page,
|
||||
value: `[
|
||||
{
|
||||
"?column?": "hello world"
|
||||
}
|
||||
]`)
|
||||
]`,
|
||||
exact: true,
|
||||
})
|
||||
|
||||
// export as CSV
|
||||
const downloadPromise = page.waitForEvent('download')
|
||||
await page.getByRole('button', { name: 'Export' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Download CSV' }).click()
|
||||
await expect(page.getByRole('menuitem', { name: 'Download CSV' })).not.toBeVisible()
|
||||
const download = await downloadPromise
|
||||
expect(download.suggestedFilename()).toContain('.csv')
|
||||
const downloadPath = await download.path()
|
||||
|
||||
@@ -2,6 +2,7 @@ import path from 'path'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { env } from '../env.config.js'
|
||||
import { expectClipboardValue } from '../utils/clipboard.js'
|
||||
import {
|
||||
createBucket,
|
||||
createFolder,
|
||||
@@ -248,61 +249,55 @@ test.describe('Storage', () => {
|
||||
const folderFile = page.getByTitle(folderFileName)
|
||||
await folderFile.click({ button: 'right' })
|
||||
await page.getByRole('menuitem', { name: 'Get URL' }).click()
|
||||
await expect(async () => {
|
||||
const copiedUrl = await page.evaluate(() => navigator.clipboard.readText())
|
||||
expect(copiedUrl).toContain(
|
||||
`storage/v1/object/public/${bucketName}/${folderName}/${folderFileName}`
|
||||
)
|
||||
}).toPass({ timeout: 2000 })
|
||||
await expectClipboardValue({
|
||||
page,
|
||||
value: `storage/v1/object/public/${bucketName}/${folderName}/${folderFileName}`,
|
||||
})
|
||||
await expect(page.getByRole('menuitem', { name: 'Get URL' })).not.toBeVisible()
|
||||
|
||||
// Right-click on the root file to open context menu while the folder is still open
|
||||
const rootFile = page.getByTitle(rootFileName)
|
||||
await rootFile.click({ button: 'right' })
|
||||
await page.getByRole('menuitem', { name: 'Get URL' }).click()
|
||||
await expect(async () => {
|
||||
const copiedUrl = await page.evaluate(() => navigator.clipboard.readText())
|
||||
expect(copiedUrl).toContain(`storage/v1/object/public/${bucketName}/${rootFileName}`)
|
||||
}).toPass({ timeout: 2000 })
|
||||
await expectClipboardValue({
|
||||
page,
|
||||
value: `storage/v1/object/public/${bucketName}/${rootFileName}`,
|
||||
})
|
||||
await expect(page.getByRole('menuitem', { name: 'Get URL' })).not.toBeVisible()
|
||||
|
||||
// Click the actions button on the folder file to open dropdown menu
|
||||
await page.getByRole('button', { name: `${folderFileName} actions` }).click()
|
||||
await page.getByRole('menuitem', { name: 'Get URL' }).click()
|
||||
await expect(async () => {
|
||||
const copiedUrl = await page.evaluate(() => navigator.clipboard.readText())
|
||||
expect(copiedUrl).toContain(
|
||||
`storage/v1/object/public/${bucketName}/${folderName}/${folderFileName}`
|
||||
)
|
||||
}).toPass({ timeout: 2000 })
|
||||
await expectClipboardValue({
|
||||
page,
|
||||
value: `storage/v1/object/public/${bucketName}/${folderName}/${folderFileName}`,
|
||||
})
|
||||
await expect(page.getByRole('menuitem', { name: 'Get URL' })).not.toBeVisible()
|
||||
|
||||
// Click the actions button on the root file to open dropdown menu while the folder is still open
|
||||
await page.getByRole('button', { name: `${rootFileName} actions` }).click()
|
||||
await page.getByRole('menuitem', { name: 'Get URL' }).click()
|
||||
await expect(async () => {
|
||||
const copiedUrl = await page.evaluate(() => navigator.clipboard.readText())
|
||||
expect(copiedUrl).toContain(`storage/v1/object/public/${bucketName}/${rootFileName}`)
|
||||
}).toPass({ timeout: 2000 })
|
||||
await expectClipboardValue({
|
||||
page,
|
||||
value: `storage/v1/object/public/${bucketName}/${rootFileName}`,
|
||||
})
|
||||
await expect(page.getByRole('menuitem', { name: 'Get URL' })).not.toBeVisible()
|
||||
|
||||
// Click the folder file to open its preview pane
|
||||
await folderFile.click()
|
||||
await page.getByRole('button', { name: 'Get URL' }).click()
|
||||
await expect(async () => {
|
||||
const copiedUrl = await page.evaluate(() => navigator.clipboard.readText())
|
||||
expect(copiedUrl).toContain(
|
||||
`storage/v1/object/public/${bucketName}/${folderName}/${folderFileName}`
|
||||
)
|
||||
}).toPass({ timeout: 2000 })
|
||||
await expectClipboardValue({
|
||||
page,
|
||||
value: `storage/v1/object/public/${bucketName}/${folderName}/${folderFileName}`,
|
||||
})
|
||||
|
||||
// Click the root file to open its preview pane while folder is still open
|
||||
await rootFile.click()
|
||||
await page.getByRole('button', { name: 'Get URL' }).click()
|
||||
await expect(async () => {
|
||||
const copiedUrl = await page.evaluate(() => navigator.clipboard.readText())
|
||||
expect(copiedUrl).toContain(`storage/v1/object/public/${bucketName}/${rootFileName}`)
|
||||
}).toPass({ timeout: 2000 })
|
||||
await expectClipboardValue({
|
||||
page,
|
||||
value: `storage/v1/object/public/${bucketName}/${rootFileName}`,
|
||||
})
|
||||
})
|
||||
|
||||
test('resets folder name when renaming with empty string', async ({ page, ref }) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import path from 'path'
|
||||
import { expect, Page } from '@playwright/test'
|
||||
|
||||
import { env } from '../env.config.js'
|
||||
import { expectClipboardValue } from '../utils/clipboard.js'
|
||||
import { dropTable, query } from '../utils/db/index.js'
|
||||
import { createTable, createTableWithRLS } from '../utils/db/queries.js'
|
||||
import { resetLocalStorage } from '../utils/reset-local-storage.js'
|
||||
@@ -80,9 +81,14 @@ testRunner('table editor', () => {
|
||||
.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')
|
||||
// Make sure the dropdown has closed otherwise it would make the other assertions unstable
|
||||
await expect(page.getByRole('menuitem', { name: 'Copy name' })).not.toBeVisible()
|
||||
|
||||
await expectClipboardValue({
|
||||
page,
|
||||
value: 'pw_table_actions',
|
||||
exact: true,
|
||||
})
|
||||
|
||||
// copies table schema to clipboard when copy schema option is clicked
|
||||
await page
|
||||
@@ -91,15 +97,17 @@ testRunner('table editor', () => {
|
||||
.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 (
|
||||
await expect(page.getByRole('menuitem', { name: 'Copy table schema' })).not.toBeVisible()
|
||||
await expectClipboardValue({
|
||||
page,
|
||||
value: `create table public.pw_table_actions (
|
||||
id bigint generated by default as identity not null,
|
||||
created_at timestamp with time zone null default now(),
|
||||
pw_column text null,
|
||||
constraint pw_table_actions_pkey primary key (id)
|
||||
) TABLESPACE pg_default;`)
|
||||
}).toPass({ timeout: 2000 })
|
||||
) TABLESPACE pg_default;`,
|
||||
exact: true,
|
||||
})
|
||||
|
||||
// duplicates table
|
||||
await page
|
||||
@@ -108,6 +116,7 @@ testRunner('table editor', () => {
|
||||
.nth(2)
|
||||
.click()
|
||||
await page.getByRole('menuitem', { name: 'Duplicate table' }).click()
|
||||
await expect(page.getByRole('menuitem', { name: 'Duplicate table' })).not.toBeVisible()
|
||||
const duplicatePromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
|
||||
method: 'POST',
|
||||
})
|
||||
@@ -547,9 +556,11 @@ testRunner('table editor', () => {
|
||||
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)
|
||||
await expectClipboardValue({
|
||||
page,
|
||||
value: colName,
|
||||
exact: true,
|
||||
})
|
||||
})
|
||||
|
||||
test('importing, pagination and large data actions works as expected', async ({ page, ref }) => {
|
||||
@@ -774,11 +785,13 @@ testRunner('table editor', () => {
|
||||
|
||||
// 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')
|
||||
await expectClipboardValue({
|
||||
page,
|
||||
value: 'first_row_value',
|
||||
exact: true,
|
||||
})
|
||||
|
||||
// Right-click on the second row's cell to open context menu
|
||||
const secondRowCell = page.getByRole('gridcell', { name: 'second_row_value' })
|
||||
@@ -787,11 +800,13 @@ testRunner('table editor', () => {
|
||||
|
||||
// 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')
|
||||
await expectClipboardValue({
|
||||
page,
|
||||
value: 'second_row_value',
|
||||
exact: true,
|
||||
})
|
||||
})
|
||||
|
||||
test('boolean fields can be edited correctly', async ({ page, ref }) => {
|
||||
@@ -1253,14 +1268,15 @@ testRunner('table editor', () => {
|
||||
.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} (
|
||||
await expectClipboardValue({
|
||||
page,
|
||||
value: `create table public.${tableName} (
|
||||
id bigint generated by default as identity not null,
|
||||
created_at timestamp with time zone not null default now(),
|
||||
pw_column bigint null default '10'::bigint,
|
||||
constraint ${tableName}_pkey primary key (id)
|
||||
) TABLESPACE pg_default;`)
|
||||
}).toPass({ timeout: 2000 })
|
||||
) TABLESPACE pg_default;`,
|
||||
exact: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
21
e2e/studio/utils/clipboard.ts
Normal file
21
e2e/studio/utils/clipboard.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { expect, type Page } from '@playwright/test'
|
||||
|
||||
export const expectClipboardValue = ({
|
||||
page,
|
||||
value,
|
||||
exact = false,
|
||||
timeout = 2000,
|
||||
}: {
|
||||
page: Page
|
||||
value: string
|
||||
exact?: boolean
|
||||
timeout?: number
|
||||
}) =>
|
||||
expect(async () => {
|
||||
await using handle = await page.evaluateHandle(() => navigator.clipboard.readText())
|
||||
if (exact) {
|
||||
expect(await handle.jsonValue()).toEqual(value)
|
||||
} else {
|
||||
expect(await handle.jsonValue()).toContain(value)
|
||||
}
|
||||
}).toPass({ timeout })
|
||||
@@ -1,7 +1,8 @@
|
||||
import { expect, Page } from '@playwright/test'
|
||||
import { waitForApiResponse } from './wait-for-response.js'
|
||||
import { toUrl } from './to-url.js'
|
||||
|
||||
import { dismissToastsIfAny } from './dismiss-toast.js'
|
||||
import { toUrl } from './to-url.js'
|
||||
import { waitForApiResponse } from './wait-for-response.js'
|
||||
|
||||
/**
|
||||
* Navigates to a the storage home view
|
||||
@@ -175,14 +176,14 @@ export const uploadFile = async (page: Page, filePath: string, fileName: string)
|
||||
const fileInput = page.locator('input[type="file"]')
|
||||
await fileInput.setInputFiles(filePath)
|
||||
|
||||
// Wait for upload to complete - file should appear in the explorer
|
||||
await page.waitForTimeout(15_000) // Allow time for upload to process
|
||||
|
||||
await expect(page.getByRole('status')).not.toBeVisible()
|
||||
// Verify file appears in the explorer by title
|
||||
await expect(
|
||||
page.getByTitle(fileName),
|
||||
`File ${fileName} should be visible in explorer after upload`
|
||||
).toBeVisible()
|
||||
// Verify its action button is visible too as it means the upload is indeed complete
|
||||
await expect(page.getByRole('button', { name: `${fileName} actions` })).toBeVisible()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user