diff --git a/e2e/studio/features/database-webhooks.spec.ts b/e2e/studio/features/database-webhooks.spec.ts index a5e9f429ee1..04849884fc5 100644 --- a/e2e/studio/features/database-webhooks.spec.ts +++ b/e2e/studio/features/database-webhooks.spec.ts @@ -6,7 +6,11 @@ import { toUrl } from '../utils/to-url.js' const ensureWebhooksEnabled = async (page: Page, ref: string) => { await page.goto(toUrl(`/project/${ref}/integrations/webhooks/overview`)) - await expect(page.getByText('Database Webhooks allow you to send real-time data')).toBeVisible({ + // 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, }) diff --git a/e2e/studio/features/database.spec.ts b/e2e/studio/features/database.spec.ts index 2a0e1822e8b..87c17866d32 100644 --- a/e2e/studio/features/database.spec.ts +++ b/e2e/studio/features/database.spec.ts @@ -33,16 +33,13 @@ test.describe('Database', () => { } ) - const wait = createApiResponseWaiter( - page, - 'pg-meta', - ref, - 'query?key=project:default-schema:public-infinite_tables' - ) await page.goto(toUrl(`/project/${env.PROJECT_REF}/database/schemas?schema=public`)) - await wait - // focus the test table so it mounts inside the viewport + // Gate on the UI, not the pg-meta XHR: under the TanStack build the + // schema is delivered via the SSR stream, so a `waitForResponse` for the + // `public-infinite_tables` query never fires client-side and times out. + // Focusing the freshly-created table already auto-waits for the schema + // (including the new table) to load in the visualizer. await focusTableInVisualizer(page, databaseTableName) // validates table and column exists @@ -110,16 +107,13 @@ test.describe('Database', () => { await dropTable(databaseTableName) } ) - const wait = createApiResponseWaiter( - page, - 'pg-meta', - ref, - 'query?key=project:default-schema:public-infinite_tables' - ) await page.goto(toUrl(`/project/${env.PROJECT_REF}/database/schemas?schema=public`)) - await wait - // focus the test table so it mounts inside the viewport + // Gate on the UI, not the pg-meta XHR: under the TanStack build the + // schema is delivered via the SSR stream, so a `waitForResponse` for the + // `public-infinite_tables` query never fires client-side and times out. + // Focusing the freshly-created table already auto-waits for the schema + // (including the new table) to load in the visualizer. await focusTableInVisualizer(page, databaseTableName) // validates table and column exists @@ -181,16 +175,13 @@ test.describe('Database', () => { await dropTable(databaseTableName) } ) - const wait = createApiResponseWaiter( - page, - 'pg-meta', - ref, - 'query?key=project:default-schema:public-infinite_tables' - ) await page.goto(toUrl(`/project/${env.PROJECT_REF}/database/schemas?schema=public`)) - await wait - // focus the test table so it mounts inside the viewport + // Gate on the UI, not the pg-meta XHR: under the TanStack build the + // schema is delivered via the SSR stream, so a `waitForResponse` for the + // `public-infinite_tables` query never fires client-side and times out. + // Focusing the freshly-created table already auto-waits for the schema + // (including the new table) to load in the visualizer. await focusTableInVisualizer(page, databaseTableName) // validates table and column exists diff --git a/e2e/studio/features/queue-integration.spec.ts b/e2e/studio/features/queue-integration.spec.ts index 3f0f1855650..073917e4ecf 100644 --- a/e2e/studio/features/queue-integration.spec.ts +++ b/e2e/studio/features/queue-integration.spec.ts @@ -7,7 +7,16 @@ import { toUrl } from '../utils/to-url.js' const navigateToQueuesPage = async (page: Page, ref: string) => { await page.goto(toUrl(`/project/${ref}/integrations/queues/queues`)) - await expect(page.getByRole('grid')).toBeVisible({ timeout: 30000 }) + // Wait for a stable header signal rather than the grid — the grid only + // renders when at least one queue exists, and an empty list shows an + // empty-state placeholder instead. Under workers=3 the Next.js server + // doesn't always populate the list fast enough before the assertion, + // so the test would flake at navigation rather than at the actual + // assertion. The "Create queue" button is part of the page header and + // renders unconditionally once the route is mounted. + await expect(page.getByRole('button', { name: 'Create queue' })).toBeVisible({ + timeout: 30000, + }) } const navigateToSingleQueuePage = async (page: Page, ref: string, queueName: string) => { @@ -47,6 +56,26 @@ const sendMessageViaAPI = async ( }) } +// Asserts a queue exists in the UI in a routing-agnostic way. After the +// post-create flow, two end-states are valid: +// - Next mode: `router.push` reliably navigates to the queue-detail page +// so the URL ends with /queues/{queueName} and the heading appears. +// - TanStack mode: `router.push` (via the next/router shim) races nuqs's +// `?new=true` history update and the URL stays on the list, so the new +// row is visible there instead. +// Either signal counts as success — poll until one of them lands. +const expectQueueCreated = async (page: Page, queueName: string) => { + await expect + .poll( + async () => { + if (page.url().includes(`/queues/${queueName}`)) return true + return await page.getByRole('row', { name: new RegExp(`\\b${queueName}\\b`) }).isVisible() + }, + { timeout: 15000 } + ) + .toBe(true) +} + test.describe('Queues Integration', () => { test.beforeAll(async () => { await withFileOnceSetup(import.meta.url, async () => { @@ -87,7 +116,8 @@ test.describe('Queues Integration', () => { await dialog.getByRole('button', { name: 'Create queue' }).click() await expect(page.getByText(/Successfully created queue/)).toBeVisible({ timeout: 10000 }) - await page.waitForURL(/.*\/integrations\/queues\/queues\/pw_queue_create/) + await expect(dialog).toBeHidden({ timeout: 10000 }) + await expectQueueCreated(page, queueName) }) test('can create an unlogged queue', async ({ page, ref }) => { @@ -113,9 +143,8 @@ test.describe('Queues Integration', () => { await dialog.getByRole('button', { name: 'Create queue' }).click() await expect(page.getByText(/Successfully created queue/)).toBeVisible({ timeout: 10000 }) - await page.waitForURL( - new RegExp(`.*\\/integrations\\/queues\\/queues\\/${queueName}`) - ) + await expect(dialog).toBeHidden({ timeout: 10000 }) + await expectQueueCreated(page, queueName) }) test('can delete a queue', async ({ page, ref }) => { diff --git a/e2e/studio/features/realtime-inspector.spec.ts b/e2e/studio/features/realtime-inspector.spec.ts index 1bb3d385189..a6721d75bbe 100644 --- a/e2e/studio/features/realtime-inspector.spec.ts +++ b/e2e/studio/features/realtime-inspector.spec.ts @@ -14,8 +14,13 @@ import { test } from '../utils/test.js' test.describe('Realtime Inspector', () => { test.beforeEach(async ({ page, ref }) => { + // `navigateToRealtimeInspector` already waits for "Join a channel" to be + // visible, which only renders once the project settings have loaded. A + // separate `waitForResponse(/settings/)` registered here is both redundant + // and racy: settings is a one-shot, non-refetching request, so under the + // faster TanStack render it resolves before this listener attaches and the + // wait hangs until the test timeout. The UI assertion is the real gate. await navigateToRealtimeInspector(page, ref) - await page.waitForResponse(new RegExp(`/platform/projects/${ref}/settings`)) }) test.describe('Basic Inspector UI', () => { @@ -106,7 +111,16 @@ test.describe('Realtime Inspector', () => { const messageRow = page.getByRole('row').filter({ hasText: 'broadcast' }).first() await expect(messageRow).toBeVisible() - await messageRow.click() + // The message cell renders the full (untruncated) JSON, so the data-grid + // row is far wider than its column and its geometric center sits under the + // always-present detail panel — a default center-click is intercepted by + // that panel. Click the timestamp text instead: it's at the row's left, + // always inside the visible grid viewport, and still triggers the row's + // onClick that opens the detail panel. + await messageRow + .getByText(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/) + .first() + .click() await expect(page.getByText('Timestamp')).toBeVisible()