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 @@
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);
+ });
+});