From a2229d84b0e481f43bdbc77a4c56232b26d4d904 Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Wed, 1 Apr 2026 09:02:41 -0600 Subject: [PATCH] test(studio): add E2E integration tests for queues (#44398) ## Summary - Adds 6 Playwright E2E tests for queue integration: view page, create basic queue, create unlogged queue, delete queue, purge messages, and send test message - Adds `aria-label` to icon-only Purge and Delete buttons in `QueueTab.tsx` for reliable test selectors - Follows existing `cron-jobs.spec.ts` patterns for setup/teardown and API helpers ## Test plan - [x] All 6 queue integration tests pass locally against self-hosted Supabase - [x] Tests run stably in parallel (3 workers) - [x] Cleanup via `withSetupCleanup` ensures no leftover test queues --- .../Integrations/Queues/QueueTab.tsx | 2 + e2e/studio/features/queue-integration.spec.ts | 189 ++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 e2e/studio/features/queue-integration.spec.ts diff --git a/apps/studio/components/interfaces/Integrations/Queues/QueueTab.tsx b/apps/studio/components/interfaces/Integrations/Queues/QueueTab.tsx index b1910931300..ef5c39b8a29 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/QueueTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/QueueTab.tsx @@ -121,6 +121,7 @@ export const QueueTab = () => { onClick={() => setPurgeQueueModalShown(true)} icon={} title="Purge messages" + aria-label="Purge messages" tooltip={{ content: { side: 'bottom', text: 'Purge messages' } }} /> @@ -130,6 +131,7 @@ export const QueueTab = () => { onClick={() => setDeleteQueueModalShown(true)} icon={} title="Delete queue" + aria-label="Delete queue" tooltip={{ content: { side: 'bottom', text: 'Delete queue' } }} /> diff --git a/e2e/studio/features/queue-integration.spec.ts b/e2e/studio/features/queue-integration.spec.ts new file mode 100644 index 00000000000..3f0f1855650 --- /dev/null +++ b/e2e/studio/features/queue-integration.spec.ts @@ -0,0 +1,189 @@ +import { expect, Page } from '@playwright/test' + +import { query } from '../utils/db/client.js' +import { releaseFileOnceCleanup, withFileOnceSetup } from '../utils/once-per-file.js' +import { test, withSetupCleanup } from '../utils/test.js' +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 }) +} + +const navigateToSingleQueuePage = async (page: Page, ref: string, queueName: string) => { + await page.goto(toUrl(`/project/${ref}/integrations/queues/queues/${queueName}`)) + await expect(page.getByRole('grid')).toBeVisible({ timeout: 30000 }) +} + +const createQueueViaAPI = async (page: Page, ref: string, queueName: string) => { + await page.request.post(toUrl(`/api/platform/pg-meta/${ref}/query`), { + failOnStatusCode: true, + data: { + query: `SELECT pgmq.create('${queueName}');`, + }, + }) +} + +const deleteQueueViaAPI = async (page: Page, ref: string, queueName: string) => { + await page.request.post(toUrl(`/api/platform/pg-meta/${ref}/query`), { + failOnStatusCode: true, + data: { + query: `SELECT pgmq.drop_queue('${queueName}');`, + }, + }) +} + +const sendMessageViaAPI = async ( + page: Page, + ref: string, + queueName: string, + payload = '{"key":"value"}' +) => { + await page.request.post(toUrl(`/api/platform/pg-meta/${ref}/query`), { + failOnStatusCode: true, + data: { + query: `SELECT pgmq.send('${queueName}', '${payload}', 5);`, + }, + }) +} + +test.describe('Queues Integration', () => { + test.beforeAll(async () => { + await withFileOnceSetup(import.meta.url, async () => { + await query('CREATE EXTENSION IF NOT EXISTS pgmq;') + }) + }) + test.afterAll(async () => { + await releaseFileOnceCleanup(import.meta.url) + }) + + test('can view queues page', async ({ page, ref }) => { + await navigateToQueuesPage(page, ref) + + await expect(page.getByRole('button', { name: 'Create queue' })).toBeVisible() + await expect(page.getByRole('button', { name: 'Refresh' })).toBeVisible() + await expect(page.getByPlaceholder('Search for a queue')).toBeVisible() + }) + + test('can create a new queue', async ({ page, ref }) => { + const queueName = 'pw_queue_create' + await navigateToQueuesPage(page, ref) + await using _ = await withSetupCleanup( + async () => { + // Nothing — the test creates the queue via UI + }, + async () => { + await deleteQueueViaAPI(page, ref, queueName) + } + ) + + await page.getByRole('button', { name: 'Create queue' }).click() + + const dialog = page.getByRole('dialog') + await expect(dialog.getByRole('heading', { name: 'Create a new queue' })).toBeVisible() + + await dialog.getByRole('textbox').fill(queueName) + + 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/) + }) + + test('can create an unlogged queue', async ({ page, ref }) => { + const queueName = 'pw_queue_create_unlogged' + await navigateToQueuesPage(page, ref) + await using _ = await withSetupCleanup( + async () => { + // Nothing — the test creates the queue via UI + }, + async () => { + await deleteQueueViaAPI(page, ref, queueName) + } + ) + + await page.getByRole('button', { name: 'Create queue' }).click() + + const dialog = page.getByRole('dialog') + await expect(dialog.getByRole('heading', { name: 'Create a new queue' })).toBeVisible() + + await dialog.getByRole('textbox').fill(queueName) + await dialog.getByText('Unlogged queue', { exact: true }).click() + + 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}`) + ) + }) + + test('can delete a queue', async ({ page, ref }) => { + const queueName = 'pw_queue_delete' + let shouldCleanup = true + await using _ = await withSetupCleanup( + async () => { + await createQueueViaAPI(page, ref, queueName) + }, + async () => { + if (!shouldCleanup) return + await deleteQueueViaAPI(page, ref, queueName) + } + ) + await navigateToSingleQueuePage(page, ref, queueName) + + await page.getByRole('button', { name: 'Delete queue' }).click() + await expect(page.getByRole('heading', { name: 'Delete this queue' })).toBeVisible() + await page.getByPlaceholder('Type in name of queue').fill(queueName) + await page.getByRole('button', { name: `Delete queue ${queueName}` }).click() + + await expect(page.getByText(/Successfully removed queue/)).toBeVisible({ timeout: 10000 }) + await page.waitForURL(/.*\/integrations\/queues\/queues$/) + shouldCleanup = false + }) + + test('can purge messages from a queue', async ({ page, ref }) => { + const queueName = 'pw_queue_purge' + await using _ = await withSetupCleanup( + async () => { + await createQueueViaAPI(page, ref, queueName) + await sendMessageViaAPI(page, ref, queueName) + await sendMessageViaAPI(page, ref, queueName) + }, + async () => { + await deleteQueueViaAPI(page, ref, queueName) + } + ) + await navigateToSingleQueuePage(page, ref, queueName) + + await page.getByRole('button', { name: 'Purge messages' }).click() + await expect(page.getByRole('heading', { name: 'Purge this queue' })).toBeVisible() + await page.getByPlaceholder('Type in name of queue').fill(queueName) + await page.getByRole('button', { name: `Purge queue ${queueName}` }).click() + + await expect(page.getByText(/Successfully purged queue/)).toBeVisible({ timeout: 10000 }) + }) + + test('can send a test message to a queue', async ({ page, ref }) => { + const queueName = 'pw_queue_send_msg' + await using _ = await withSetupCleanup( + async () => { + await createQueueViaAPI(page, ref, queueName) + }, + async () => { + await deleteQueueViaAPI(page, ref, queueName) + } + ) + await navigateToSingleQueuePage(page, ref, queueName) + + await page.getByRole('button', { name: 'Add message' }).first().click() + + const dialog = page.getByRole('dialog') + await expect(dialog.getByRole('heading', { name: 'Add a message to the queue' })).toBeVisible() + + // Submit with default payload '{}' and delay + await dialog.getByRole('button', { name: 'Add' }).click() + + await expect(page.getByText(/Successfully added a message/)).toBeVisible({ timeout: 10000 }) + }) +})