Fix Issue #7: Add status message element and fix column resizing bug #8
37
css/main.css
37
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;
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
<div class="game-status">
|
||||
<span id="current-turn">White's Turn</span>
|
||||
<span id="game-state">Active</span>
|
||||
<div id="status-message" class="status-message"></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
212
tests/ui/column-resize.test.js
Normal file
212
tests/ui/column-resize.test.js
Normal file
@ -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 = `
|
||||
<span class="move-number">${i}.</span>
|
||||
<span class="move-notation white">e4</span>
|
||||
<span class="move-notation black">e5</span>
|
||||
`;
|
||||
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);
|
||||
});
|
||||
});
|
||||
147
tests/ui/status-message.test.js
Normal file
147
tests/ui/status-message.test.js
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user