Files
soroban-abacus-flashcards/apps/web/e2e/join-room-flow.spec.ts
Thomas Hallock b36df3a40c fix(worksheets): ten-frames not rendering in mastery mode
Fixed two critical bugs preventing ten-frames from rendering:

1. **Mastery mode not handled** (typstGenerator.ts:61)
   - Code only checked for 'smart' | 'manual' modes
   - Mastery mode fell into manual path, tried to use boolean flags that don't exist
   - Resulted in all display options being `undefined`
   - Fix: Check for both 'smart' OR 'mastery' modes (both use displayRules)

2. **Typst array membership syntax** (already fixed in previous commit)
   - Used `(i in array)` which doesn't work in Typst
   - Changed to `array.contains(i)`

Added comprehensive unit tests (tenFrames.test.ts):
- Problem analysis tests (regrouping detection)
- Display rule evaluation tests
- Full Typst template generation tests
- Mastery mode specific tests
- All 14 tests now passing

Added debug logging to trace display rules resolution:
- displayRules.ts: Shows rule evaluation per problem
- typstGenerator.ts: Shows enriched problems and Typst data
- Helps diagnose future issues

The issue was that mastery mode (which uses displayRules like smart mode)
was being treated as manual mode (which uses boolean flags), resulting in
undefined display options.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 10:06:27 -06: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 })
})
})
})