mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 22:18:00 +08:00
chore: E2E tests for Realtime (#42518)
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Minor amount of tests for Realtime because we have zero E2E coverage here <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Added comprehensive end-to-end test coverage for the Realtime Inspector: channel join/leave, start/stop listening, broadcast workflows, message display, JSON validation, and cleanup to improve reliability. * Added supporting test helpers/utilities to enable deterministic navigation, channel operations, message waiting, and modal interactions for stable test execution. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
153
e2e/studio/features/realtime-inspector.spec.ts
Normal file
153
e2e/studio/features/realtime-inspector.spec.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { test } from '../utils/test.js'
|
||||
import {
|
||||
getMessageCount,
|
||||
joinChannel,
|
||||
leaveChannel,
|
||||
navigateToRealtimeInspector,
|
||||
openBroadcastModal,
|
||||
startListening,
|
||||
stopListening,
|
||||
waitForRealtimeMessage,
|
||||
} from '../utils/realtime-helpers.js'
|
||||
|
||||
const testChannelName = 'pw_realtime_test_channel'
|
||||
|
||||
test.describe('Realtime Inspector', () => {
|
||||
test.beforeEach(async ({ page, ref }) => {
|
||||
await navigateToRealtimeInspector(page, ref)
|
||||
})
|
||||
|
||||
test.describe('Basic Inspector UI', () => {
|
||||
test('inspector page loads correctly with empty state', async ({ page }) => {
|
||||
await expect(page.getByRole('button', { name: 'Join a channel' })).toBeVisible()
|
||||
|
||||
const startButton = page.getByRole('button', { name: 'Start listening' })
|
||||
await expect(startButton).toBeVisible()
|
||||
await expect(startButton).toBeDisabled()
|
||||
|
||||
await expect(page.getByText('Create realtime experiences')).toBeVisible()
|
||||
})
|
||||
|
||||
test('channel selection popover opens and works', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Join a channel' }).click()
|
||||
|
||||
await expect(page.getByPlaceholder('Enter a channel name')).toBeVisible({ timeout: 5000 })
|
||||
await expect(page.getByRole('button', { name: 'Listen to channel' })).toBeVisible()
|
||||
await expect(page.getByText('Is channel private?')).toBeVisible()
|
||||
|
||||
await page.keyboard.press('Escape')
|
||||
})
|
||||
|
||||
test('can join and leave a channel', async ({ page }) => {
|
||||
await joinChannel(page, testChannelName)
|
||||
|
||||
await expect(page.getByText('Listening', { exact: true })).toBeVisible({ timeout: 10000 })
|
||||
await expect(page.getByRole('button', { name: `Channel: ${testChannelName}` })).toBeVisible()
|
||||
|
||||
await leaveChannel(page)
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Join a channel' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('start/stop listening button works', async ({ page }) => {
|
||||
await joinChannel(page, testChannelName)
|
||||
|
||||
await expect(page.getByText('Listening', { exact: true })).toBeVisible({ timeout: 10000 })
|
||||
await expect(page.getByRole('button', { name: 'Stop listening' })).toBeVisible()
|
||||
|
||||
await stopListening(page)
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Start listening' })).toBeVisible()
|
||||
await expect(page.getByText('Listening', { exact: true })).not.toBeVisible()
|
||||
|
||||
await startListening(page)
|
||||
|
||||
await expect(page.getByText('Listening', { exact: true })).toBeVisible({ timeout: 10000 })
|
||||
|
||||
await leaveChannel(page)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Broadcast Messages', () => {
|
||||
test('broadcast messages appear in the UI when listening', async ({ page }) => {
|
||||
await joinChannel(page, testChannelName)
|
||||
|
||||
await expect(page.getByText('Listening', { exact: true })).toBeVisible({ timeout: 10000 })
|
||||
|
||||
await openBroadcastModal(page)
|
||||
await page.getByRole('button', { name: 'Confirm' }).click()
|
||||
|
||||
await expect(page.getByText('Successfully broadcasted message')).toBeVisible({
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
const messageRow = await waitForRealtimeMessage(page, { timeout: 30000 })
|
||||
await expect(messageRow).toBeVisible()
|
||||
|
||||
const count = await getMessageCount(page)
|
||||
expect(count).toBeGreaterThanOrEqual(1)
|
||||
|
||||
await leaveChannel(page)
|
||||
})
|
||||
|
||||
test('clicking broadcast message shows detail panel', async ({ page }) => {
|
||||
await joinChannel(page, testChannelName)
|
||||
|
||||
await openBroadcastModal(page)
|
||||
await page.getByRole('button', { name: 'Confirm' }).click()
|
||||
await expect(page.getByText('Successfully broadcasted message')).toBeVisible({
|
||||
timeout: 10000,
|
||||
})
|
||||
await waitForRealtimeMessage(page, { timeout: 30000 })
|
||||
|
||||
const messageRow = page.getByRole('row').filter({ hasText: 'broadcast' }).first()
|
||||
await expect(messageRow).toBeVisible({ timeout: 5000 })
|
||||
await messageRow.click()
|
||||
|
||||
await expect(page.getByText('Timestamp')).toBeVisible({ timeout: 5000 })
|
||||
|
||||
await leaveChannel(page)
|
||||
})
|
||||
|
||||
test('broadcast modal validates JSON payload', async ({ page }) => {
|
||||
await joinChannel(page, testChannelName)
|
||||
|
||||
await openBroadcastModal(page)
|
||||
|
||||
const codeEditor = page.getByRole('textbox', { name: /Editor content/i })
|
||||
await codeEditor.click({ force: true })
|
||||
await page.keyboard.press('ControlOrMeta+KeyA')
|
||||
await page.keyboard.type('{ invalid json }')
|
||||
|
||||
await page.getByRole('button', { name: 'Confirm' }).click()
|
||||
|
||||
await expect(page.getByText('Please provide a valid JSON')).toBeVisible({ timeout: 5000 })
|
||||
|
||||
await page.getByRole('button', { name: 'Cancel' }).click()
|
||||
|
||||
await leaveChannel(page)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Message Display', () => {
|
||||
test('messages counter shows correct count', async ({ page }) => {
|
||||
await joinChannel(page, `${testChannelName}_counter`)
|
||||
|
||||
const initialCount = await getMessageCount(page)
|
||||
|
||||
await openBroadcastModal(page)
|
||||
await page.getByRole('button', { name: 'Confirm' }).click()
|
||||
await expect(page.getByText('Successfully broadcasted message')).toBeVisible({
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
await waitForRealtimeMessage(page, { timeout: 30000 })
|
||||
|
||||
const newCount = await getMessageCount(page)
|
||||
expect(newCount).toBeGreaterThan(initialCount)
|
||||
|
||||
await leaveChannel(page)
|
||||
})
|
||||
})
|
||||
})
|
||||
69
e2e/studio/utils/realtime-helpers.ts
Normal file
69
e2e/studio/utils/realtime-helpers.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { expect, Page } from '@playwright/test'
|
||||
import { toUrl } from './to-url.js'
|
||||
|
||||
export async function navigateToRealtimeInspector(page: Page, ref: string) {
|
||||
await page.goto(toUrl(`/project/${ref}/realtime/inspector`))
|
||||
await expect(page.locator('text=Join a channel')).toBeVisible({ timeout: 30000 })
|
||||
}
|
||||
|
||||
export async function joinChannel(page: Page, channelName: string) {
|
||||
await page.getByRole('button', { name: /Join a channel|Channel:/ }).click()
|
||||
await expect(page.getByPlaceholder('Enter a channel name')).toBeVisible({ timeout: 5000 })
|
||||
await page.getByPlaceholder('Enter a channel name').fill(channelName)
|
||||
await page.getByRole('button', { name: 'Listen to channel' }).click()
|
||||
await expect(page.getByText('Listening', { exact: true })).toBeVisible({ timeout: 10000 })
|
||||
}
|
||||
|
||||
export async function leaveChannel(page: Page) {
|
||||
await page.getByRole('button', { name: /Channel:/ }).click()
|
||||
await expect(page.getByRole('button', { name: 'Leave channel' })).toBeVisible({ timeout: 5000 })
|
||||
await page.getByRole('button', { name: 'Leave channel' }).click()
|
||||
await expect(page.getByRole('button', { name: 'Join a channel' })).toBeVisible({ timeout: 5000 })
|
||||
}
|
||||
|
||||
export async function startListening(page: Page) {
|
||||
const listenButton = page.getByRole('button', { name: 'Start listening' })
|
||||
await expect(listenButton).toBeVisible({ timeout: 5000 })
|
||||
await listenButton.click()
|
||||
await expect(page.getByText('Listening', { exact: true })).toBeVisible({ timeout: 10000 })
|
||||
}
|
||||
|
||||
export async function stopListening(page: Page) {
|
||||
const stopButton = page.getByRole('button', { name: 'Stop listening' })
|
||||
await expect(stopButton).toBeVisible({ timeout: 5000 })
|
||||
await stopButton.click()
|
||||
await expect(page.getByRole('button', { name: 'Start listening' })).toBeVisible({ timeout: 5000 })
|
||||
}
|
||||
|
||||
export async function openBroadcastModal(page: Page) {
|
||||
const broadcastButton = page.getByRole('button', { name: 'Broadcast a message' })
|
||||
await expect(broadcastButton).toBeVisible({ timeout: 5000 })
|
||||
await broadcastButton.click()
|
||||
await expect(page.getByText('Broadcast a message to all clients')).toBeVisible({ timeout: 5000 })
|
||||
}
|
||||
|
||||
export async function waitForRealtimeMessage(page: Page, options?: { timeout?: number }) {
|
||||
const timeout = options?.timeout ?? 30000
|
||||
const gridRow = page.getByRole('row').filter({ hasText: /^\d{4}-\d{2}-\d{2}/ }).first()
|
||||
await expect(gridRow).toBeVisible({ timeout })
|
||||
return gridRow
|
||||
}
|
||||
|
||||
export async function getMessageCount(page: Page): Promise<number> {
|
||||
const countText = page.locator('text=/Found \\d+ messages/')
|
||||
if ((await countText.count()) === 0) {
|
||||
const noMessages = page.locator('text=No message found yet')
|
||||
if ((await noMessages.count()) > 0) {
|
||||
return 0
|
||||
}
|
||||
const maxMessages = page.locator('text=/showing only the latest 100/')
|
||||
if ((await maxMessages.count()) > 0) {
|
||||
return 100
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
const text = await countText.textContent()
|
||||
const match = text?.match(/Found (\d+) messages/)
|
||||
return match ? parseInt(match[1], 10) : 0
|
||||
}
|
||||
Reference in New Issue
Block a user