Add comprehensive room management UI: - CreateRoomModal: Modal for creating new multiplayer rooms - JoinRoomModal: Modal for joining rooms via code - JoinRoomInput: Reusable input component for room codes - PlayOnlineTab: Tab component for arcade lobby - RecentRoomsList: List of user's recent rooms - /join/[code] page: Direct join link page - E2E test for join flow Includes shareable room links, clipboard integration, and user-friendly error handling. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
297 lines
11 KiB
TypeScript
297 lines
11 KiB
TypeScript
import { expect, test } from '@playwright/test'
|
|
|
|
test.describe('Join Room Flow', () => {
|
|
test.describe('Room Creation', () => {
|
|
test('should create a room from the game page', async ({ page }) => {
|
|
// Navigate to a game
|
|
await page.goto('/games/matching')
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
// Click the (+) Add Player button to open the popover
|
|
const addPlayerButton = page.locator('button[title="Add player"]')
|
|
await expect(addPlayerButton).toBeVisible()
|
|
await addPlayerButton.click()
|
|
|
|
// Wait for popover to appear
|
|
await page.waitForTimeout(300)
|
|
|
|
// Click the "Play Online" or "Invite Players" tab
|
|
const onlineTab = page.locator('button:has-text("Play Online"), button:has-text("Invite")')
|
|
await expect(onlineTab.first()).toBeVisible()
|
|
await onlineTab.first().click()
|
|
|
|
// Click "Create New Room" button
|
|
const createRoomButton = page.locator('button:has-text("Create New Room")')
|
|
await expect(createRoomButton).toBeVisible()
|
|
await createRoomButton.click()
|
|
|
|
// Wait for room creation to complete
|
|
await page.waitForTimeout(1000)
|
|
|
|
// Verify we're now in a room - should see room info in nav
|
|
const roomInfo = page.locator('text=/Room|Code/i')
|
|
await expect(roomInfo).toBeVisible({ timeout: 5000 })
|
|
})
|
|
})
|
|
|
|
test.describe('Join Room by Code', () => {
|
|
let roomCode: string
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
// Create a room first
|
|
await page.goto('/games/matching')
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
const addPlayerButton = page.locator('button[title="Add player"]')
|
|
await addPlayerButton.click()
|
|
await page.waitForTimeout(300)
|
|
|
|
const onlineTab = page.locator('button:has-text("Play Online"), button:has-text("Invite")')
|
|
await onlineTab.first().click()
|
|
|
|
const createRoomButton = page.locator('button:has-text("Create New Room")')
|
|
await createRoomButton.click()
|
|
await page.waitForTimeout(1000)
|
|
|
|
// Extract the room code from the page
|
|
const roomCodeElement = page.locator('text=/[A-Z]{3}-[0-9]{3}/')
|
|
await expect(roomCodeElement).toBeVisible({ timeout: 5000 })
|
|
const roomCodeText = await roomCodeElement.textContent()
|
|
roomCode = roomCodeText?.match(/[A-Z]{3}-[0-9]{3}/)?.[0] || ''
|
|
expect(roomCode).toMatch(/[A-Z]{3}-[0-9]{3}/)
|
|
})
|
|
|
|
test('should join room via direct URL', async ({ page, context }) => {
|
|
// Open a new page (simulating a different user)
|
|
const newPage = await context.newPage()
|
|
|
|
// Navigate to the join URL
|
|
await newPage.goto(`/join/${roomCode}`)
|
|
await newPage.waitForLoadState('networkidle')
|
|
|
|
// Should show "Joining room..." or redirect to game
|
|
await newPage.waitForTimeout(1000)
|
|
|
|
// Should now be in the room
|
|
const url = newPage.url()
|
|
expect(url).toContain('/arcade')
|
|
})
|
|
|
|
test('should show error for invalid room code', async ({ page, context }) => {
|
|
const newPage = await context.newPage()
|
|
|
|
// Try to join with invalid code
|
|
await newPage.goto('/join/INVALID')
|
|
await newPage.waitForLoadState('networkidle')
|
|
|
|
// Should show error message
|
|
const errorMessage = newPage.locator('text=/not found|failed/i')
|
|
await expect(errorMessage).toBeVisible({ timeout: 5000 })
|
|
})
|
|
|
|
test('should show confirmation when switching rooms', async ({ page }) => {
|
|
// User is already in a room from beforeEach
|
|
|
|
// Try to join a different room (we'll create another one)
|
|
const addPlayerButton = page.locator('button[title="Add player"]')
|
|
await addPlayerButton.click()
|
|
await page.waitForTimeout(300)
|
|
|
|
const onlineTab = page.locator('button:has-text("Play Online"), button:has-text("Invite")')
|
|
await onlineTab.first().click()
|
|
|
|
const createRoomButton = page.locator('button:has-text("Create New Room")')
|
|
await createRoomButton.click()
|
|
await page.waitForTimeout(1000)
|
|
|
|
// Get the new room code
|
|
const newRoomCodeElement = page.locator('text=/[A-Z]{3}-[0-9]{3}/')
|
|
await expect(newRoomCodeElement).toBeVisible({ timeout: 5000 })
|
|
const newRoomCodeText = await newRoomCodeElement.textContent()
|
|
const newRoomCode = newRoomCodeText?.match(/[A-Z]{3}-[0-9]{3}/)?.[0] || ''
|
|
|
|
// Navigate to join the new room
|
|
await page.goto(`/join/${newRoomCode}`)
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
// Should show room switch confirmation
|
|
const confirmationDialog = page.locator('text=/Switch Rooms?|already in another room/i')
|
|
await expect(confirmationDialog).toBeVisible({ timeout: 3000 })
|
|
|
|
// Should show both room codes
|
|
await expect(page.locator(`text=${roomCode}`)).toBeVisible()
|
|
await expect(page.locator(`text=${newRoomCode}`)).toBeVisible()
|
|
|
|
// Click "Switch Rooms" button
|
|
const switchButton = page.locator('button:has-text("Switch Rooms")')
|
|
await expect(switchButton).toBeVisible()
|
|
await switchButton.click()
|
|
|
|
// Should navigate to the new room
|
|
await page.waitForTimeout(1000)
|
|
const url = page.url()
|
|
expect(url).toContain('/arcade')
|
|
})
|
|
|
|
test('should stay in current room when canceling switch', async ({ page }) => {
|
|
// User is already in a room from beforeEach
|
|
const originalRoomCode = roomCode
|
|
|
|
// Create another room to try switching to
|
|
const addPlayerButton = page.locator('button[title="Add player"]')
|
|
await addPlayerButton.click()
|
|
await page.waitForTimeout(300)
|
|
|
|
const onlineTab = page.locator('button:has-text("Play Online"), button:has-text("Invite")')
|
|
await onlineTab.first().click()
|
|
|
|
const createRoomButton = page.locator('button:has-text("Create New Room")')
|
|
await createRoomButton.click()
|
|
await page.waitForTimeout(1000)
|
|
|
|
const newRoomCodeElement = page.locator('text=/[A-Z]{3}-[0-9]{3}/')
|
|
const newRoomCodeText = await newRoomCodeElement.textContent()
|
|
const newRoomCode = newRoomCodeText?.match(/[A-Z]{3}-[0-9]{3}/)?.[0] || ''
|
|
|
|
// Navigate to join the new room
|
|
await page.goto(`/join/${newRoomCode}`)
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
// Should show confirmation
|
|
const confirmationDialog = page.locator('text=/Switch Rooms?/i')
|
|
await expect(confirmationDialog).toBeVisible({ timeout: 3000 })
|
|
|
|
// Click "Cancel"
|
|
const cancelButton = page.locator('button:has-text("Cancel")')
|
|
await expect(cancelButton).toBeVisible()
|
|
await cancelButton.click()
|
|
|
|
// Should stay on original room
|
|
await page.waitForTimeout(500)
|
|
const url = page.url()
|
|
expect(url).toContain('/arcade')
|
|
|
|
// Should still see original room code
|
|
await expect(page.locator(`text=${originalRoomCode}`)).toBeVisible()
|
|
})
|
|
})
|
|
|
|
test.describe('Join Room Input Validation', () => {
|
|
test('should format room code as user types', async ({ page }) => {
|
|
await page.goto('/games/matching')
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
// Open the add player popover
|
|
const addPlayerButton = page.locator('button[title="Add player"]')
|
|
await addPlayerButton.click()
|
|
await page.waitForTimeout(300)
|
|
|
|
// Switch to Play Online tab
|
|
const onlineTab = page.locator('button:has-text("Play Online")')
|
|
if (await onlineTab.isVisible()) {
|
|
await onlineTab.click()
|
|
}
|
|
|
|
// Find the room code input
|
|
const codeInput = page.locator('input[placeholder*="ABC"]')
|
|
await expect(codeInput).toBeVisible({ timeout: 3000 })
|
|
|
|
// Type a room code
|
|
await codeInput.fill('abc123')
|
|
|
|
// Should be formatted as ABC-123
|
|
const inputValue = await codeInput.inputValue()
|
|
expect(inputValue).toBe('ABC-123')
|
|
})
|
|
|
|
test('should validate room code in real-time', async ({ page }) => {
|
|
await page.goto('/games/matching')
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
const addPlayerButton = page.locator('button[title="Add player"]')
|
|
await addPlayerButton.click()
|
|
await page.waitForTimeout(300)
|
|
|
|
const onlineTab = page.locator('button:has-text("Play Online")')
|
|
if (await onlineTab.isVisible()) {
|
|
await onlineTab.click()
|
|
}
|
|
|
|
const codeInput = page.locator('input[placeholder*="ABC"]')
|
|
await expect(codeInput).toBeVisible({ timeout: 3000 })
|
|
|
|
// Type an invalid code
|
|
await codeInput.fill('INVALID')
|
|
|
|
// Should show validation icon (❌)
|
|
await page.waitForTimeout(500)
|
|
const validationIcon = page.locator('text=/❌|Room not found/i')
|
|
await expect(validationIcon).toBeVisible({ timeout: 3000 })
|
|
})
|
|
})
|
|
|
|
test.describe('Recent Rooms List', () => {
|
|
test('should show recently joined rooms', async ({ page }) => {
|
|
// Create and join a room
|
|
await page.goto('/games/matching')
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
const addPlayerButton = page.locator('button[title="Add player"]')
|
|
await addPlayerButton.click()
|
|
await page.waitForTimeout(300)
|
|
|
|
const onlineTab = page.locator('button:has-text("Play Online"), button:has-text("Invite")')
|
|
await onlineTab.first().click()
|
|
|
|
const createRoomButton = page.locator('button:has-text("Create New Room")')
|
|
await createRoomButton.click()
|
|
await page.waitForTimeout(1000)
|
|
|
|
// Leave the room
|
|
const leaveButton = page.locator('button:has-text("Leave"), button:has-text("Quit")')
|
|
if (await leaveButton.isVisible()) {
|
|
await leaveButton.click()
|
|
await page.waitForTimeout(500)
|
|
}
|
|
|
|
// Open the popover again
|
|
await addPlayerButton.click()
|
|
await page.waitForTimeout(300)
|
|
|
|
await onlineTab.first().click()
|
|
|
|
// Should see "Recent Rooms" section
|
|
const recentRoomsSection = page.locator('text=/Recent Rooms/i')
|
|
await expect(recentRoomsSection).toBeVisible({ timeout: 3000 })
|
|
|
|
// Should see at least one room in the list
|
|
const roomListItem = page.locator('text=/[A-Z]{3}-[0-9]{3}/')
|
|
await expect(roomListItem.first()).toBeVisible()
|
|
})
|
|
})
|
|
|
|
test.describe('Room Ownership', () => {
|
|
test('creator should see room controls', async ({ page }) => {
|
|
// Create a room
|
|
await page.goto('/games/matching')
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
const addPlayerButton = page.locator('button[title="Add player"]')
|
|
await addPlayerButton.click()
|
|
await page.waitForTimeout(300)
|
|
|
|
const onlineTab = page.locator('button:has-text("Play Online"), button:has-text("Invite")')
|
|
await onlineTab.first().click()
|
|
|
|
const createRoomButton = page.locator('button:has-text("Create New Room")')
|
|
await createRoomButton.click()
|
|
await page.waitForTimeout(1000)
|
|
|
|
// Creator should see room management controls
|
|
// (e.g., leave room, room settings, etc.)
|
|
const roomControls = page.locator('button:has-text("Leave"), button:has-text("Settings")')
|
|
await expect(roomControls.first()).toBeVisible({ timeout: 3000 })
|
|
})
|
|
})
|
|
})
|