fix: add status message element and fix column resizing bug
Some checks failed
Some checks failed
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 <noreply@anthropic.com>
This commit is contained in:
parent
e4af7d3e53
commit
fb96963b48
37
css/main.css
37
css/main.css
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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';
|
||||||
|
|
||||||
|
|||||||
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