Fix Issue #7: Add status message element and fix column resizing bug #8

Open
Weyoun wants to merge 6 commits from fix/issue-7-and-column-resize into main
5 changed files with 399 additions and 1 deletions
Showing only changes of commit fb96963b48 - Show all commits

View File

@ -46,6 +46,8 @@ body {
display: flex; display: flex;
gap: 2rem; gap: 2rem;
font-size: 1rem; font-size: 1rem;
flex-wrap: wrap;
align-items: center;
} }
.game-status span { .game-status span {
@ -54,10 +56,43 @@ body {
border-radius: 4px; 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 { .game-container {
flex: 1; flex: 1;
display: grid; display: grid;
grid-template-columns: 1fr 3fr 1fr; grid-template-columns: minmax(200px, 250px) minmax(600px, 3fr) minmax(200px, 250px);
gap: 2rem; gap: 2rem;
padding: 2rem; padding: 2rem;
max-width: 1600px; max-width: 1600px;

View File

@ -21,6 +21,7 @@
<div class="game-status"> <div class="game-status">
<span id="current-turn">White's Turn</span> <span id="current-turn">White's Turn</span>
<span id="game-state">Active</span> <span id="game-state">Active</span>
<div id="status-message" class="status-message"></div>
</div> </div>
</header> </header>

View File

@ -245,6 +245,9 @@ class ChessApp {
console.warn('Status message element not found, using console:', message); console.warn('Status message element not found, using console:', message);
return; return;
} }
// Add type class for styling
statusMessage.className = `status-message ${type}`;
statusMessage.textContent = message; statusMessage.textContent = message;
statusMessage.style.display = 'block'; statusMessage.style.display = 'block';

View 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);
});
});

View 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);
});
});