soroban-abacus-flashcards/apps/web/e2e/know-your-world-layout.spec.ts

264 lines
9.3 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { expect, test } from '@playwright/test'
/**
* Know Your World - Full-Screen Layout E2E Tests
*
* These tests verify:
* - Full-screen, no-scrolling layout works correctly
* - Panel resizing functions properly
* - Map scales dynamically with panel size
* - No content overflow in game info panel
*/
test.describe('Know Your World - Full-Screen Layout', () => {
test.beforeEach(async ({ page }) => {
// Navigate to arcade and start a Know Your World game
await page.goto('/arcade')
await page.waitForLoadState('networkidle')
// Clear any existing session
const returnButton = page.locator('button:has-text("Return to Arcade")')
if (await returnButton.isVisible({ timeout: 1000 }).catch(() => false)) {
await returnButton.click()
await page.waitForLoadState('networkidle')
}
// Select a player if needed
const playerCard = page.locator('[data-testid="player-card"]').first()
if (await playerCard.isVisible({ timeout: 2000 }).catch(() => false)) {
await playerCard.click()
await page.waitForTimeout(500)
}
// Click Know Your World game card
const knowYourWorldCard = page.locator('[data-game="know-your-world"]')
await knowYourWorldCard.click()
await page.waitForLoadState('networkidle')
// Start a game (skip setup if on setup page)
const startButton = page.locator('button:has-text("Start")')
if (await startButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await startButton.click()
await page.waitForLoadState('networkidle')
}
// Skip study phase if present
const skipButton = page.locator('button:has-text("Skip")')
if (await skipButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await skipButton.click()
await page.waitForLoadState('networkidle')
}
})
test('should use StandardGameLayout with no scrolling in playing phase', async ({ page }) => {
// Wait for playing phase to load
await page.waitForSelector('[data-component="playing-phase"]', {
timeout: 5000,
})
// Check that StandardGameLayout wrapper exists
const layout = page.locator('[data-layout="standard-game-layout"]')
await expect(layout).toBeVisible()
// Verify no scrolling on main container
const layoutStyles = await layout.evaluate((el) => {
const styles = window.getComputedStyle(el)
return {
overflow: styles.overflow,
height: styles.height,
}
})
expect(layoutStyles.overflow).toBe('hidden')
expect(layoutStyles.height).toContain('vh') // Should be 100vh
})
test('should render game info panel without scrolling', async ({ page }) => {
await page.waitForSelector('[data-component="game-info-panel"]', {
timeout: 5000,
})
const gameInfoPanel = page.locator('[data-component="game-info-panel"]')
await expect(gameInfoPanel).toBeVisible()
// Check that panel has overflow: hidden
const panelStyles = await gameInfoPanel.evaluate((el) => {
const styles = window.getComputedStyle(el)
return {
overflow: styles.overflow,
scrollHeight: el.scrollHeight,
clientHeight: el.clientHeight,
}
})
expect(panelStyles.overflow).toBe('hidden')
// scrollHeight should equal clientHeight (no overflow)
expect(panelStyles.scrollHeight).toBeLessThanOrEqual(panelStyles.clientHeight + 1) // +1 for rounding
})
test('should display current prompt in game info panel', async ({ page }) => {
await page.waitForSelector('[data-section="current-prompt"]', {
timeout: 5000,
})
const promptSection = page.locator('[data-section="current-prompt"]')
await expect(promptSection).toBeVisible()
// Should have "Find:" label
await expect(promptSection.locator('text=Find:')).toBeVisible()
// Should have a region name or placeholder
const hasRegionName = await promptSection.locator('div').filter({ hasNotText: 'Find:' }).count()
expect(hasRegionName).toBeGreaterThan(0)
})
test('should display progress counter and bar', async ({ page }) => {
await page.waitForSelector('[data-section="progress"]', { timeout: 5000 })
// Progress counter should be visible
const progressCounter = page.locator('[data-section="progress"]').first()
await expect(progressCounter).toBeVisible()
// Should show format like "0/20" or "5/20"
const progressText = await progressCounter.textContent()
expect(progressText).toMatch(/\d+\/\d+/)
// Progress bar should exist (second element with data-section="progress" or within container)
const hasProgressBar = await page.locator('[style*="transition"]').count()
expect(hasProgressBar).toBeGreaterThan(0)
})
test('should render map in bottom panel', async ({ page }) => {
await page.waitForSelector('[data-component="map-panel"]', {
timeout: 5000,
})
const mapPanel = page.locator('[data-component="map-panel"]')
await expect(mapPanel).toBeVisible()
// Map renderer should be inside map panel
const mapRenderer = mapPanel.locator('[data-component="map-renderer"]')
await expect(mapRenderer).toBeVisible()
// SVG map should be rendered
const svg = mapRenderer.locator('svg').first()
await expect(svg).toBeVisible()
})
test('should show game mode and difficulty emojis', async ({ page }) => {
await page.waitForSelector('[data-section="game-info"]', { timeout: 5000 })
const gameInfo = page.locator('[data-section="game-info"]')
await expect(gameInfo).toBeVisible()
// Should contain emojis (game mode: 🤝/🏁/↔️, difficulty: 😊/🤔)
const hasEmojis = await gameInfo.evaluate((el) => {
const text = el.textContent || ''
return /[🤝🏁😊🤔]/u.test(text)
})
expect(hasEmojis).toBe(true)
})
test('should have correct panel structure with resize handle', async ({ page }) => {
// Wait for playing phase
await page.waitForSelector('[data-component="playing-phase"]', {
timeout: 5000,
})
// Should have exactly 2 panels (game info + map)
const panels = page.locator('[data-panel-id]')
const panelCount = await panels.count()
expect(panelCount).toBe(2)
// Resize handle should exist between panels
// PanelResizeHandle doesn't have a data attribute but should be between panels
const playingPhase = page.locator('[data-component="playing-phase"]')
const hasResizeHandle = await playingPhase.evaluate((el) => {
// Look for an element with cursor: row-resize or col-resize
const children = Array.from(el.querySelectorAll('*'))
return children.some((child) => {
const styles = window.getComputedStyle(child)
return styles.cursor === 'row-resize' || styles.cursor === 'col-resize'
})
})
expect(hasResizeHandle).toBe(true)
})
})
test.describe('Know Your World - Viewport Behavior', () => {
test('should fill full viewport height without scrolling', async ({ page }) => {
await page.goto('/arcade')
await page.waitForLoadState('networkidle')
// Start game (simplified)
const knowYourWorldCard = page.locator('[data-game="know-your-world"]')
if (await knowYourWorldCard.isVisible({ timeout: 2000 }).catch(() => false)) {
await knowYourWorldCard.click()
await page.waitForLoadState('networkidle')
// Try to start/skip to playing phase
const startButton = page.locator('button:has-text("Start")')
if (await startButton.isVisible({ timeout: 1000 }).catch(() => false)) {
await startButton.click()
}
const skipButton = page.locator('button:has-text("Skip")')
if (await skipButton.isVisible({ timeout: 1000 }).catch(() => false)) {
await skipButton.click()
}
}
await page.waitForSelector('[data-layout="standard-game-layout"]', {
timeout: 5000,
})
// Check that page has no scroll
const hasScroll = await page.evaluate(() => {
return document.documentElement.scrollHeight > window.innerHeight
})
expect(hasScroll).toBe(false)
})
test('should work on mobile viewport', async ({ page }) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 })
await page.goto('/arcade')
await page.waitForLoadState('networkidle')
// Start game
const knowYourWorldCard = page.locator('[data-game="know-your-world"]')
if (await knowYourWorldCard.isVisible({ timeout: 2000 }).catch(() => false)) {
await knowYourWorldCard.click()
await page.waitForLoadState('networkidle')
const startButton = page.locator('button:has-text("Start")')
if (await startButton.isVisible({ timeout: 1000 }).catch(() => false)) {
await startButton.click()
}
const skipButton = page.locator('button:has-text("Skip")')
if (await skipButton.isVisible({ timeout: 1000 }).catch(() => false)) {
await skipButton.click()
}
}
await page.waitForSelector('[data-component="playing-phase"]', {
timeout: 5000,
})
// Game info panel should not scroll
const gameInfoPanel = page.locator('[data-component="game-info-panel"]')
if (await gameInfoPanel.isVisible()) {
const overflowStyle = await gameInfoPanel.evaluate((el) => {
return window.getComputedStyle(el).overflow
})
expect(overflowStyle).toBe('hidden')
}
// Map should be visible
const mapPanel = page.locator('[data-component="map-panel"]')
await expect(mapPanel).toBeVisible()
})
})