Files
soroban-abacus-flashcards/apps/web/e2e/join-room-flow.spec.ts
Thomas Hallock 7f95032253 feat: add room creation and join flow UI
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>
2025-10-13 11:23:28 -05:00

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 })
})
})
})