mirror of
https://github.com/supabase/supabase.git
synced 2026-09-08 19:08:44 +08:00
Pulls the framework-agnostic E2E test fixes out of the TanStack Start migration PR (#46424) so they can land on `master` independently and scope that PR down. Test files only — no app/source changes. > [!NOTE] > **These changes originate from #46424** (the Next.js → TanStack Start migration). They were made while getting the E2E suite green on both builds, but they're pure test-file changes that are framework-agnostic and already pass on the current Next.js build. A few are *also* written to tolerate TanStack behaviour (called out in code comments) — harmless and correct on Next today. **Changed:** - `realtime-inspector.spec.ts` — drop the racy `waitForResponse(/settings/)` (the "Join a channel" UI assertion already gates on settings loading); click the message row's timestamp instead of its center, which the wide untruncated-JSON cell pushes under the always-present detail panel. - `database.spec.ts` — gate the three Schema Visualizer / Tables tests on the visualizer UI (`focusTableInVisualizer` already auto-waits for the schema, including the freshly-created table) instead of the `pg-meta … public-infinite_tables` XHR. - `database-webhooks.spec.ts` — wait on the `Database Webhooks` heading instead of marketing copy that was removed when the page moved to the new integrations UI. - `queue-integration.spec.ts` — wait on the unconditional "Create queue" button instead of the grid (which only renders once a queue exists); assert creation by polling for either valid end-state. ## To test - Run the affected specs against a local self-hosted studio and confirm they pass: `pnpm --prefix e2e/studio run e2e -- features/realtime-inspector.spec.ts features/database.spec.ts features/database-webhooks.spec.ts features/queue-integration.spec.ts` - Or just let the studio E2E suite run on this PR (Next.js build). Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
226 lines
8.8 KiB
TypeScript
226 lines
8.8 KiB
TypeScript
import { expect, Page } from '@playwright/test'
|
|
|
|
import { createTable, dropTable } from '../utils/db/index.js'
|
|
import { test } from '../utils/test.js'
|
|
import { toUrl } from '../utils/to-url.js'
|
|
|
|
const ensureWebhooksEnabled = async (page: Page, ref: string) => {
|
|
await page.goto(toUrl(`/project/${ref}/integrations/webhooks/overview`))
|
|
// The original "allow you to send real-time data" copy was removed when the
|
|
// webhooks page was migrated to the new integrations UI (PR #44277). Wait
|
|
// on the page-header heading instead — it's rendered whether or not the
|
|
// supabase_functions extension is enabled.
|
|
await expect(page.getByRole('heading', { name: 'Database Webhooks' })).toBeVisible({
|
|
timeout: 30000,
|
|
})
|
|
|
|
const enableButton = page.getByRole('button', { name: 'Enable webhooks' })
|
|
if ((await enableButton.count()) > 0) {
|
|
await enableButton.click()
|
|
await expect(page.getByText('Successfully enabled webhooks')).toBeVisible({ timeout: 15000 })
|
|
}
|
|
}
|
|
|
|
const navigateToWebhooksList = async (page: Page, ref: string) => {
|
|
await page.goto(toUrl(`/project/${ref}/integrations/webhooks/webhooks`))
|
|
await expect(page.getByPlaceholder('Search for a webhook')).toBeVisible({ timeout: 30000 })
|
|
}
|
|
|
|
const createWebhookViaUI = async (page: Page, hookName: string, tableName: string) => {
|
|
await page.getByRole('button', { name: 'Create a new hook' }).click()
|
|
await expect(page.getByText('Create a new database webhook')).toBeVisible({ timeout: 10000 })
|
|
|
|
await page.locator('input[name="name"]').fill(hookName)
|
|
|
|
await page.getByRole('combobox').filter({ hasText: 'Select a table' }).click()
|
|
await page.getByRole('option', { name: new RegExp(tableName) }).click()
|
|
|
|
await page.locator('#event-INSERT').click()
|
|
|
|
await page
|
|
.getByPlaceholder('http://api.com/path/resource')
|
|
.fill('http://localhost:3000/test-webhook')
|
|
|
|
await page.getByRole('button', { name: 'Create webhook' }).click()
|
|
await expect(page.getByText(`Successfully created new webhook "${hookName}"`)).toBeVisible({
|
|
timeout: 10000,
|
|
})
|
|
}
|
|
|
|
const openWebhookEditor = async (page: Page, hookName: string) => {
|
|
const webhookText = page.getByText(hookName, { exact: true })
|
|
const webhookRow = page.locator('tr').filter({ has: webhookText })
|
|
await webhookRow.getByRole('button').click()
|
|
await page.getByRole('menuitem', { name: 'Edit hook' }).click()
|
|
await expect(page.getByText(`Update webhook ${hookName}`)).toBeVisible({ timeout: 10000 })
|
|
}
|
|
|
|
const addCustomHeader = async (page: Page, name: string, value: string) => {
|
|
await page.getByRole('button', { name: 'Add a new header' }).click()
|
|
await page.getByPlaceholder('Header name').last().fill(name)
|
|
await page.getByPlaceholder('Header value').last().fill(value)
|
|
}
|
|
|
|
const deleteWebhookViaUI = async (page: Page, name: string) => {
|
|
const webhookText = page.getByText(name, { exact: true })
|
|
const webhookRow = page.locator('tr').filter({ has: webhookText })
|
|
await webhookRow.getByRole('button').click()
|
|
|
|
await page.getByRole('menuitem', { name: 'Delete hook' }).click()
|
|
|
|
await expect(page.getByRole('heading', { name: 'Delete database webhook' })).toBeVisible()
|
|
await page.getByPlaceholder('Type in name of webhook').fill(name)
|
|
await page.getByRole('button', { name: `Delete ${name}` }).click()
|
|
|
|
await expect(page.getByText(`Successfully deleted ${name}`)).toBeVisible({ timeout: 10000 })
|
|
await expect(webhookRow).not.toBeVisible({ timeout: 10000 })
|
|
}
|
|
|
|
const setupTable = async (tableName: string) => {
|
|
await dropTable(tableName)
|
|
await createTable(tableName, 'data')
|
|
}
|
|
|
|
const uniqueNames = (prefix: string) => {
|
|
const i = test.info().parallelIndex
|
|
return { table: `pw_wh_${prefix}_tbl_${i}`, hook: `pw_wh_${prefix}_${i}` }
|
|
}
|
|
|
|
test.describe('Database Webhooks', () => {
|
|
test('can view webhooks list page', async ({ page, ref }) => {
|
|
await ensureWebhooksEnabled(page, ref)
|
|
await navigateToWebhooksList(page, ref)
|
|
|
|
await expect(page.getByPlaceholder('Search for a webhook')).toBeVisible()
|
|
await expect(page.getByRole('button', { name: 'Create a new hook' })).toBeVisible()
|
|
})
|
|
|
|
test('can create a new webhook with correct toast message', async ({ page, ref }) => {
|
|
const { table, hook } = uniqueNames('create')
|
|
await setupTable(table)
|
|
|
|
try {
|
|
await ensureWebhooksEnabled(page, ref)
|
|
await navigateToWebhooksList(page, ref)
|
|
|
|
await page.getByRole('button', { name: 'Create a new hook' }).click()
|
|
await expect(page.getByText('Create a new database webhook')).toBeVisible({ timeout: 10000 })
|
|
|
|
await page.locator('input[name="name"]').fill(hook)
|
|
await page.getByRole('combobox').filter({ hasText: 'Select a table' }).click()
|
|
await page.getByRole('option', { name: new RegExp(table) }).click()
|
|
await page.locator('#event-INSERT').click()
|
|
await page
|
|
.getByPlaceholder('http://api.com/path/resource')
|
|
.fill('http://localhost:3000/test-webhook')
|
|
|
|
await page.getByRole('button', { name: 'Create webhook' }).click()
|
|
|
|
await expect(page.getByText(`Successfully created new webhook "${hook}"`)).toBeVisible({
|
|
timeout: 10000,
|
|
})
|
|
|
|
await expect(page.getByText(hook, { exact: true })).toBeVisible({ timeout: 10000 })
|
|
} finally {
|
|
await dropTable(table)
|
|
}
|
|
})
|
|
|
|
test('can edit a webhook with correct toast message', async ({ page, ref }) => {
|
|
const { table, hook } = uniqueNames('edit')
|
|
await setupTable(table)
|
|
|
|
try {
|
|
await ensureWebhooksEnabled(page, ref)
|
|
await navigateToWebhooksList(page, ref)
|
|
await createWebhookViaUI(page, hook, table)
|
|
|
|
const webhookText = page.getByText(hook, { exact: true })
|
|
const webhookRow = page.locator('tr').filter({ has: webhookText })
|
|
await webhookRow.getByRole('button').click()
|
|
|
|
await page.getByRole('menuitem', { name: 'Edit hook' }).click()
|
|
|
|
await expect(page.getByText(`Update webhook ${hook}`)).toBeVisible({ timeout: 10000 })
|
|
|
|
const urlInput = page.getByPlaceholder('http://api.com/path/resource')
|
|
await urlInput.clear()
|
|
await urlInput.fill('http://localhost:3000/test-webhook-updated')
|
|
|
|
await page.getByRole('button', { name: 'Update webhook' }).click()
|
|
|
|
await expect(page.getByText(`Successfully updated webhook "${hook}"`)).toBeVisible({
|
|
timeout: 10000,
|
|
})
|
|
|
|
await expect(page.getByText('Webhook not found')).not.toBeVisible()
|
|
} finally {
|
|
await dropTable(table)
|
|
}
|
|
})
|
|
|
|
test('preserves webhook URL path and custom headers after editing', async ({ page, ref }) => {
|
|
const { table, hook } = uniqueNames('edit_persist')
|
|
const originalUrl = 'http://localhost:3000/test-webhook'
|
|
const updatedUrl = 'http://localhost:3000/test-webhook-updated/path'
|
|
const headerName = 'X-API-Key'
|
|
const headerValue = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ12'
|
|
|
|
await setupTable(table)
|
|
|
|
try {
|
|
await ensureWebhooksEnabled(page, ref)
|
|
await navigateToWebhooksList(page, ref)
|
|
await createWebhookViaUI(page, hook, table)
|
|
|
|
await openWebhookEditor(page, hook)
|
|
await addCustomHeader(page, headerName, headerValue)
|
|
await page.getByRole('button', { name: 'Update webhook' }).click()
|
|
|
|
await expect(page.getByText(`Successfully updated webhook "${hook}"`)).toBeVisible({
|
|
timeout: 10000,
|
|
})
|
|
|
|
await openWebhookEditor(page, hook)
|
|
await expect(page.getByPlaceholder('http://api.com/path/resource')).toHaveValue(originalUrl)
|
|
await expect(page.getByPlaceholder('Header name').last()).toHaveValue(headerName)
|
|
await expect(page.getByPlaceholder('Header value').last()).toHaveValue(headerValue)
|
|
|
|
const urlInput = page.getByPlaceholder('http://api.com/path/resource')
|
|
await urlInput.clear()
|
|
await urlInput.fill(updatedUrl)
|
|
await page.getByRole('button', { name: 'Update webhook' }).click()
|
|
|
|
await expect(page.getByText(`Successfully updated webhook "${hook}"`)).toBeVisible({
|
|
timeout: 10000,
|
|
})
|
|
|
|
await openWebhookEditor(page, hook)
|
|
await expect(page.getByPlaceholder('http://api.com/path/resource')).toHaveValue(updatedUrl)
|
|
await expect(page.getByPlaceholder('Header name').last()).toHaveValue(headerName)
|
|
await expect(page.getByPlaceholder('Header value').last()).toHaveValue(headerValue)
|
|
} finally {
|
|
await dropTable(table)
|
|
}
|
|
})
|
|
|
|
test('can delete a webhook with correct toast message', async ({ page, ref }) => {
|
|
const { table, hook } = uniqueNames('delete')
|
|
await setupTable(table)
|
|
|
|
try {
|
|
await ensureWebhooksEnabled(page, ref)
|
|
await navigateToWebhooksList(page, ref)
|
|
await createWebhookViaUI(page, hook, table)
|
|
|
|
await deleteWebhookViaUI(page, hook)
|
|
|
|
await expect(page.getByText(`Successfully deleted ${hook}`)).toBeVisible({ timeout: 10000 })
|
|
|
|
await expect(page.getByText(hook, { exact: true })).not.toBeVisible()
|
|
} finally {
|
|
await dropTable(table)
|
|
}
|
|
})
|
|
})
|