From fb96963b48d85f95fd4b9038a34d17b07e91a430 Mon Sep 17 00:00:00 2001 From: Christoph Wagner Date: Sun, 23 Nov 2025 19:15:50 +0100 Subject: [PATCH] fix: add status message element and fix column resizing bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes two bugs: 1. Issue #7: Missing status message DOM element - Added #status-message div to index.html - Added CSS styling with type-based classes (info, success, error) - Enhanced showMessage() to apply type classes for visual styling - Messages auto-hide after 3 seconds with fade-in animation 2. Column resizing visual bug: - Changed grid-template-columns from flexible (1fr 3fr 1fr) - To fixed minimum widths: minmax(200px, 250px) minmax(600px, 3fr) minmax(200px, 250px) - Prevents columns from resizing when content changes (captured pieces, move history) - Maintains stable layout throughout gameplay Tests: - Added status-message.test.js with 10 test cases - Added column-resize.test.js with 8 test cases - Tests verify DOM element existence, CSS styling, auto-hide behavior, and layout stability 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- css/main.css | 37 +++++- index.html | 1 + js/main.js | 3 + tests/ui/column-resize.test.js | 212 ++++++++++++++++++++++++++++++++ tests/ui/status-message.test.js | 147 ++++++++++++++++++++++ 5 files changed, 399 insertions(+), 1 deletion(-) create mode 100644 tests/ui/column-resize.test.js create mode 100644 tests/ui/status-message.test.js diff --git a/css/main.css b/css/main.css index 5ad1089..9327ff3 100644 --- a/css/main.css +++ b/css/main.css @@ -46,6 +46,8 @@ body { display: flex; gap: 2rem; font-size: 1rem; + flex-wrap: wrap; + align-items: center; } .game-status span { @@ -54,10 +56,43 @@ body { border-radius: 4px; } +.status-message { + display: none; + padding: 0.75rem 1rem; + border-radius: 4px; + font-weight: 500; + text-align: center; + animation: fadeIn 0.3s ease-in; + flex: 1 0 100%; +} + +.status-message.info { + background-color: #d1ecf1; + color: #0c5460; + border: 1px solid #bee5eb; +} + +.status-message.success { + background-color: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.status-message.error { + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-10px); } + to { opacity: 1; transform: translateY(0); } +} + .game-container { flex: 1; display: grid; - grid-template-columns: 1fr 3fr 1fr; + grid-template-columns: minmax(200px, 250px) minmax(600px, 3fr) minmax(200px, 250px); gap: 2rem; padding: 2rem; max-width: 1600px; diff --git a/index.html b/index.html index fe7617e..5280dad 100644 --- a/index.html +++ b/index.html @@ -21,6 +21,7 @@
White's Turn Active +
diff --git a/js/main.js b/js/main.js index 4b90119..a79ba42 100644 --- a/js/main.js +++ b/js/main.js @@ -245,6 +245,9 @@ class ChessApp { console.warn('Status message element not found, using console:', message); return; } + + // Add type class for styling + statusMessage.className = `status-message ${type}`; statusMessage.textContent = message; statusMessage.style.display = 'block'; diff --git a/tests/ui/column-resize.test.js b/tests/ui/column-resize.test.js new file mode 100644 index 0000000..669c21b --- /dev/null +++ b/tests/ui/column-resize.test.js @@ -0,0 +1,212 @@ +/** + * Column Resize Bug Tests + * Tests to verify that columns maintain consistent width during gameplay + */ + +import { test, expect } from '@playwright/test'; + +test.describe('Column Layout Stability', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:8080'); + await page.waitForLoadState('networkidle'); + }); + + test('game container uses fixed minimum widths', async ({ page }) => { + const gameContainer = await page.locator('.game-container'); + + // Check computed styles + const styles = await gameContainer.evaluate((el) => { + const computed = window.getComputedStyle(el); + return { + display: computed.display, + gridTemplateColumns: computed.gridTemplateColumns + }; + }); + + expect(styles.display).toBe('grid'); + // Should use minmax() for fixed minimum widths + expect(styles.gridTemplateColumns).not.toBe('1fr 3fr 1fr'); + }); + + test('left sidebar maintains minimum width', async ({ page }) => { + const leftSidebar = await page.locator('.captured-white'); + + const initialWidth = await leftSidebar.evaluate(el => el.offsetWidth); + expect(initialWidth).toBeGreaterThanOrEqual(200); // minmax(200px, 250px) + }); + + test('right sidebar maintains minimum width', async ({ page }) => { + const rightSidebar = await page.locator('.game-sidebar'); + + const initialWidth = await rightSidebar.evaluate(el => el.offsetWidth); + expect(initialWidth).toBeGreaterThanOrEqual(200); // minmax(200px, 250px) + }); + + test('board section maintains minimum width', async ({ page }) => { + const boardSection = await page.locator('.board-section'); + + const initialWidth = await boardSection.evaluate(el => el.offsetWidth); + expect(initialWidth).toBeGreaterThanOrEqual(600); // minmax(600px, 3fr) + }); + + test('columns do not resize when pieces are captured', async ({ page }) => { + // Get initial widths + const getWidths = async () => { + return await page.evaluate(() => { + const leftSidebar = document.querySelector('.captured-white'); + const boardSection = document.querySelector('.board-section'); + const rightSidebar = document.querySelector('.game-sidebar'); + + return { + left: leftSidebar.offsetWidth, + board: boardSection.offsetWidth, + right: rightSidebar.offsetWidth + }; + }); + }; + + const initialWidths = await getWidths(); + + // Make a move that captures a piece (simulate) + await page.evaluate(() => { + // Add captured piece to test resize behavior + const capturedList = document.querySelector('#captured-white-pieces'); + if (capturedList) { + const piece = document.createElement('div'); + piece.className = 'captured-piece white pawn'; + piece.textContent = '♙'; + capturedList.appendChild(piece); + } + }); + + await page.waitForTimeout(100); // Allow for any layout recalculation + + const afterCaptureWidths = await getWidths(); + + // Columns should maintain their widths (within 1px for rounding) + expect(Math.abs(afterCaptureWidths.left - initialWidths.left)).toBeLessThanOrEqual(1); + expect(Math.abs(afterCaptureWidths.board - initialWidths.board)).toBeLessThanOrEqual(1); + expect(Math.abs(afterCaptureWidths.right - initialWidths.right)).toBeLessThanOrEqual(1); + }); + + test('columns do not resize when multiple pieces are captured', async ({ page }) => { + const getWidths = async () => { + return await page.evaluate(() => { + const leftSidebar = document.querySelector('.captured-white'); + const boardSection = document.querySelector('.board-section'); + const rightSidebar = document.querySelector('.game-sidebar'); + + return { + left: leftSidebar.offsetWidth, + board: boardSection.offsetWidth, + right: rightSidebar.offsetWidth + }; + }); + }; + + const initialWidths = await getWidths(); + + // Add multiple captured pieces + await page.evaluate(() => { + const capturedList = document.querySelector('#captured-white-pieces'); + if (capturedList) { + const pieces = ['♙', '♘', '♗', '♖', '♕']; + pieces.forEach(symbol => { + const piece = document.createElement('div'); + piece.className = 'captured-piece white'; + piece.textContent = symbol; + capturedList.appendChild(piece); + }); + } + }); + + await page.waitForTimeout(100); + + const afterMultipleCapturesWidths = await getWidths(); + + // Columns should still maintain their widths + expect(Math.abs(afterMultipleCapturesWidths.left - initialWidths.left)).toBeLessThanOrEqual(1); + expect(Math.abs(afterMultipleCapturesWidths.board - initialWidths.board)).toBeLessThanOrEqual(1); + expect(Math.abs(afterMultipleCapturesWidths.right - initialWidths.right)).toBeLessThanOrEqual(1); + }); + + test('columns do not resize when move history grows', async ({ page }) => { + const getWidths = async () => { + return await page.evaluate(() => { + const leftSidebar = document.querySelector('.captured-white'); + const boardSection = document.querySelector('.board-section'); + const rightSidebar = document.querySelector('.game-sidebar'); + + return { + left: leftSidebar.offsetWidth, + board: boardSection.offsetWidth, + right: rightSidebar.offsetWidth + }; + }); + }; + + const initialWidths = await getWidths(); + + // Add move history entries + await page.evaluate(() => { + const moveHistory = document.querySelector('#move-history'); + if (moveHistory) { + for (let i = 1; i <= 20; i++) { + const moveEntry = document.createElement('div'); + moveEntry.className = 'move-entry'; + moveEntry.innerHTML = ` + ${i}. + e4 + e5 + `; + moveHistory.appendChild(moveEntry); + } + } + }); + + await page.waitForTimeout(100); + + const afterMovesWidths = await getWidths(); + + // Columns should maintain their widths + expect(Math.abs(afterMovesWidths.left - initialWidths.left)).toBeLessThanOrEqual(1); + expect(Math.abs(afterMovesWidths.board - initialWidths.board)).toBeLessThanOrEqual(1); + expect(Math.abs(afterMovesWidths.right - initialWidths.right)).toBeLessThanOrEqual(1); + }); + + test('layout remains stable across window resize', async ({ page }) => { + // Set initial viewport + await page.setViewportSize({ width: 1400, height: 900 }); + + const getWidths = async () => { + return await page.evaluate(() => { + const leftSidebar = document.querySelector('.captured-white'); + const boardSection = document.querySelector('.board-section'); + const rightSidebar = document.querySelector('.game-sidebar'); + + return { + left: leftSidebar.offsetWidth, + board: boardSection.offsetWidth, + right: rightSidebar.offsetWidth + }; + }); + }; + + const widthsBefore = await getWidths(); + + // Resize window + await page.setViewportSize({ width: 1600, height: 900 }); + await page.waitForTimeout(200); + + const widthsAfter = await getWidths(); + + // Sidebar widths should remain close to their minimum (200px) + expect(widthsAfter.left).toBeGreaterThanOrEqual(200); + expect(widthsAfter.left).toBeLessThanOrEqual(250); + expect(widthsAfter.right).toBeGreaterThanOrEqual(200); + expect(widthsAfter.right).toBeLessThanOrEqual(250); + + // Board should be able to grow + expect(widthsAfter.board).toBeGreaterThanOrEqual(600); + }); +}); diff --git a/tests/ui/status-message.test.js b/tests/ui/status-message.test.js new file mode 100644 index 0000000..4383cf7 --- /dev/null +++ b/tests/ui/status-message.test.js @@ -0,0 +1,147 @@ +/** + * Status Message Display Tests - Issue #7 + * Tests for the status message element and its functionality + */ + +import { test, expect } from '@playwright/test'; + +test.describe('Status Message Display', () => { + test.beforeEach(async ({ page }) => { + await page.goto('http://localhost:8080'); + await page.waitForLoadState('networkidle'); + }); + + test('status message element exists in DOM', async ({ page }) => { + const statusMessage = await page.locator('#status-message'); + await expect(statusMessage).toBeAttached(); + }); + + test('status message is hidden by default', async ({ page }) => { + const statusMessage = await page.locator('#status-message'); + await expect(statusMessage).toHaveCSS('display', 'none'); + }); + + test('new game shows status message', async ({ page }) => { + const newGameBtn = await page.locator('#btn-new-game'); + + // Accept the confirm dialog + page.on('dialog', dialog => dialog.accept()); + await newGameBtn.click(); + + const statusMessage = await page.locator('#status-message'); + await expect(statusMessage).toBeVisible(); + await expect(statusMessage).toContainText('New game started!'); + }); + + test('status message auto-hides after 3 seconds', async ({ page }) => { + const newGameBtn = await page.locator('#btn-new-game'); + + // Accept the confirm dialog + page.on('dialog', dialog => dialog.accept()); + await newGameBtn.click(); + + const statusMessage = await page.locator('#status-message'); + await expect(statusMessage).toBeVisible(); + + // Wait for message to auto-hide + await page.waitForTimeout(3100); + await expect(statusMessage).toHaveCSS('display', 'none'); + }); + + test('check message displays with info styling', async ({ page }) => { + // Create a check situation (this would require setting up a specific board state) + // For now, we'll test that the element can receive the info class + await page.evaluate(() => { + const app = window.app; + if (app && app.showMessage) { + app.showMessage('Check! black king is in check', 'info'); + } + }); + + const statusMessage = await page.locator('#status-message'); + await expect(statusMessage).toBeVisible(); + await expect(statusMessage).toHaveClass(/info/); + await expect(statusMessage).toContainText('Check!'); + }); + + test('checkmate message displays with success styling', async ({ page }) => { + await page.evaluate(() => { + const app = window.app; + if (app && app.showMessage) { + app.showMessage('Checkmate! white wins!', 'success'); + } + }); + + const statusMessage = await page.locator('#status-message'); + await expect(statusMessage).toBeVisible(); + await expect(statusMessage).toHaveClass(/success/); + await expect(statusMessage).toContainText('Checkmate!'); + }); + + test('error messages display with error styling', async ({ page }) => { + await page.evaluate(() => { + const app = window.app; + if (app && app.showMessage) { + app.showMessage('Invalid move!', 'error'); + } + }); + + const statusMessage = await page.locator('#status-message'); + await expect(statusMessage).toBeVisible(); + await expect(statusMessage).toHaveClass(/error/); + }); + + test('multiple messages display sequentially', async ({ page }) => { + // Show first message + await page.evaluate(() => { + const app = window.app; + if (app && app.showMessage) { + app.showMessage('First message', 'info'); + } + }); + + const statusMessage = await page.locator('#status-message'); + await expect(statusMessage).toContainText('First message'); + + // Show second message (should replace first) + await page.evaluate(() => { + const app = window.app; + if (app && app.showMessage) { + app.showMessage('Second message', 'success'); + } + }); + + await expect(statusMessage).toContainText('Second message'); + await expect(statusMessage).toHaveClass(/success/); + }); + + test('status message has correct CSS classes', async ({ page }) => { + await page.evaluate(() => { + const app = window.app; + if (app && app.showMessage) { + app.showMessage('Test message', 'info'); + } + }); + + const statusMessage = await page.locator('#status-message'); + await expect(statusMessage).toHaveClass('status-message info'); + }); + + test('console warning not shown when element exists', async ({ page }) => { + const consoleWarnings = []; + page.on('console', msg => { + if (msg.type() === 'warning') { + consoleWarnings.push(msg.text()); + } + }); + + await page.evaluate(() => { + const app = window.app; + if (app && app.showMessage) { + app.showMessage('Test message', 'info'); + } + }); + + expect(consoleWarnings.filter(w => w.includes('Status message element not found'))).toHaveLength(0); + }); +});