Skip to content

Commit cd32286

Browse files
committed
Adding more tests and fixing docker
1 parent 624825b commit cd32286

File tree

9 files changed

+317
-33
lines changed

9 files changed

+317
-33
lines changed

README.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,8 @@ npm test
335335
-**Illegal Move Rejection** - Move validation and error handling
336336
-**Special Rules** - Valid moves API, pawn promotion, castling, en passant
337337
-**Check and Checkmate** - Fool's Mate detection
338+
-**Complete Game** - Scholar's Mate end-to-end scenario
339+
-**UI Interactions** - Chessboard loading, move highlighting, invalid move rejection
338340
-**Non-Goals Validation** - Documented limitations verification
339341
-**API Validation** - All REST endpoints
340342
-**Security Posture** - Authentication, secrets, CORS
@@ -344,17 +346,22 @@ npm test
344346
cd e2e-tests
345347
npm install
346348
npx playwright install chromium
347-
npm test
349+
npm test # Run UI browser tests
350+
npm run test:legacy # Run legacy API-focused tests
348351
```
349352

350353
**Philosophy**: Black box testing from a real user's perspective. Uses only public interfaces (HTTP API and browser UI). Tests behavior, not implementation.
351354

352-
**CI Integration**: E2E tests run automatically on push via GitHub Actions workflow.
353-
354-
### What's Intentionally Missing
355+
**UI Browser Tests** (`tests/chess-ui.test.ts`):
356+
- Browser-based tests using Playwright
357+
- Test complete game scenarios (e.g., Scholar's Mate)
358+
- Validate UI rendering and interactions
359+
- Run with `npm test` (starts services automatically via webServer config)
355360

356-
- **Performance Tests**: No load testing (not a production service)
357-
- **Security Tests**: No penetration testing (local HTTP only)
361+
**Legacy API-Focused Tests** (`tests/chess-api.test.ts`):
362+
- API-centric tests for core functionality
363+
- Validate request/response schemas and status codes
364+
- Run with `npm run test:legacy`
358365

359366
## Security
360367

docker-compose.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@ services:
1616
build:
1717
context: ./frontend
1818
dockerfile: Dockerfile
19+
command: npm run dev -- -H 0.0.0.0
20+
volumes:
21+
- ./frontend:/app:delegated
22+
- /app/node_modules
1923
ports:
2024
- "3000:3000"
2125
environment:
22-
- NEXT_PUBLIC_API_URL=http://localhost:8080/
26+
- NEXT_PUBLIC_API_URL=http://host.docker.internal:8080/
27+
- NODE_ENV=development
2328
depends_on:
2429
- backend
2530
networks:

e2e-tests/package-lock.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

e2e-tests/package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
"version": "1.0.0",
44
"description": "End-to-end QA validation tests for Chess Engine application",
55
"scripts": {
6-
"test": "node qa-validation.js",
7-
"test:report": "node qa-validation.js --report"
6+
"test": "playwright test",
7+
"test:headed": "playwright test --headed",
8+
"test:ui": "playwright test --ui",
9+
"test:report": "playwright show-report",
10+
"test:legacy": "node qa-validation.js"
811
},
912
"dependencies": {
10-
"playwright": "^1.40.0"
13+
"playwright": "^1.40.0",
14+
"@playwright/test": "^1.40.0"
1115
},
1216
"engines": {
1317
"node": ">=18.0.0"

e2e-tests/playwright.config.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { defineConfig, devices } from '@playwright/test';
2+
3+
/**
4+
* @see https://playwright.dev/docs/test-configuration
5+
*/
6+
export default defineConfig({
7+
testDir: './tests',
8+
/* Run tests in files in parallel */
9+
fullyParallel: true,
10+
/* Fail the build on CI if you accidentally left test.only in the source code. */
11+
forbidOnly: !!process.env.CI,
12+
/* Retry on CI only */
13+
retries: process.env.CI ? 2 : 0,
14+
/* Opt out of parallel tests on CI. */
15+
workers: process.env.CI ? 1 : undefined,
16+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
17+
reporter: 'html',
18+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
19+
use: {
20+
/* Base URL to use in actions like `await page.goto('/')`. */
21+
baseURL: 'http://localhost:3000',
22+
23+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
24+
trace: 'on-first-retry',
25+
},
26+
27+
/* Configure projects for major browsers */
28+
projects: [
29+
{
30+
name: 'chromium',
31+
use: { ...devices['Desktop Chrome'] },
32+
},
33+
34+
/* Test against mobile viewports. */
35+
// {
36+
// name: 'Mobile Chrome',
37+
// use: { ...devices['Pixel 5'] },
38+
// },
39+
// {
40+
// name: 'Mobile Safari',
41+
// use: { ...devices['iPhone 12'] },
42+
// },
43+
44+
/* Test against branded browsers. */
45+
// {
46+
// name: 'Microsoft Edge',
47+
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
48+
// },
49+
// {
50+
// name: 'Google Chrome',
51+
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
52+
// },
53+
],
54+
55+
/* Run your local dev server before starting the tests */
56+
webServer: [
57+
{
58+
command: 'cd ../backend && ./gradlew bootRun',
59+
port: 8080,
60+
reuseExistingServer: !process.env.CI,
61+
},
62+
{
63+
command: 'cd ../frontend && npm run dev',
64+
port: 3000,
65+
reuseExistingServer: !process.env.CI,
66+
},
67+
],
68+
});

e2e-tests/tests/chess-ui.test.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('Chess Game UI Tests', () => {
4+
test.beforeEach(async ({ page }) => {
5+
await page.goto('/');
6+
// Wait for the chessboard to load
7+
await page.waitForSelector('[class*="chessboard"], table, [data-testid="chessboard"]', { timeout: 10000 });
8+
});
9+
10+
test('should load the chessboard', async ({ page }) => {
11+
// Verify chessboard is visible
12+
const chessboard = page.locator('[class*="chessboard"], table, [data-testid="chessboard"]');
13+
await expect(chessboard).toBeVisible();
14+
15+
// Verify initial pieces are present (at least some squares have pieces)
16+
const squares = page.locator('td, [data-square], [class*="square"]');
17+
await expect(squares).toHaveCount(64); // 8x8 board
18+
});
19+
20+
test('should start a new game', async ({ page }) => {
21+
// Click start game button if present, or assume game starts automatically
22+
const startButton = page.locator('button:has-text("Start"), [data-testid="start-game"]');
23+
if (await startButton.isVisible()) {
24+
await startButton.click();
25+
}
26+
27+
// Verify game is started (turn indicator shows White)
28+
const turnIndicator = page.locator('[class*="turn"], [data-testid="turn"]');
29+
if (await turnIndicator.isVisible()) {
30+
await expect(turnIndicator).toContainText(/White|white/i);
31+
}
32+
});
33+
34+
test('should play a complete game (Scholar\'s Mate)', async ({ page }) => {
35+
// Start game
36+
const startButton = page.locator('button:has-text("Start"), [data-testid="start-game"]');
37+
if (await startButton.isVisible()) {
38+
await startButton.click();
39+
}
40+
41+
// Scholar's Mate sequence:
42+
// 1. e4 e5
43+
// 2. Bc4 Nc6
44+
// 3. Qh5 Nf6
45+
// 4. Qxf7#
46+
47+
// Helper function to click square by algebraic notation
48+
const clickSquare = async (square: string) => {
49+
const [file, rank] = square.split('');
50+
const col = file.charCodeAt(0) - 'a'.charCodeAt(0) + 1; // a=1, b=2, ..., h=8
51+
const row = parseInt(rank);
52+
53+
// Try different selector patterns
54+
let squareLocator = page.locator(`td[data-row="${row}"][data-col="${col}"]`);
55+
if (!(await squareLocator.isVisible())) {
56+
squareLocator = page.locator(`[data-square="${square}"]`);
57+
}
58+
if (!(await squareLocator.isVisible())) {
59+
squareLocator = page.locator(`.${square}, #${square}`);
60+
}
61+
if (!(await squareLocator.isVisible())) {
62+
// Fallback: find by text content or position
63+
squareLocator = page.locator('td, div').filter({ hasText: new RegExp(square.toUpperCase()) });
64+
}
65+
66+
await squareLocator.click();
67+
};
68+
69+
// 1. White: e2 to e4
70+
await clickSquare('e2');
71+
await clickSquare('e4');
72+
73+
// Verify move was made (e4 should have white pawn)
74+
await page.waitForTimeout(500); // Wait for UI update
75+
76+
// 2. Black: e7 to e5
77+
await clickSquare('e7');
78+
await clickSquare('e5');
79+
80+
// 3. White: Bf1 to c4
81+
await clickSquare('f1');
82+
await clickSquare('c4');
83+
84+
// 4. Black: Nb8 to c6
85+
await clickSquare('b8');
86+
await clickSquare('c6');
87+
88+
// 5. White: Qd1 to h5
89+
await clickSquare('d1');
90+
await clickSquare('h5');
91+
92+
// 6. Black: Ng8 to f6
93+
await clickSquare('g8');
94+
await clickSquare('f6');
95+
96+
// 7. White: Qh5 to f7 (checkmate)
97+
await clickSquare('h5');
98+
await clickSquare('f7');
99+
100+
// Verify checkmate
101+
await page.waitForTimeout(1000); // Wait for checkmate detection
102+
103+
// Check for checkmate message
104+
const checkmateMessage = page.locator('[class*="checkmate"], [data-testid="checkmate"], text=/checkmate|Checkmate/i');
105+
await expect(checkmateMessage).toBeVisible();
106+
107+
// Verify game ended (no more moves possible)
108+
const gameOverIndicator = page.locator('[class*="game-over"], [data-testid="game-over"]');
109+
if (await gameOverIndicator.isVisible()) {
110+
await expect(gameOverIndicator).toContainText(/over|ended|finished/i);
111+
}
112+
});
113+
114+
test('should highlight valid moves', async ({ page }) => {
115+
// Start game
116+
const startButton = page.locator('button:has-text("Start"), [data-testid="start-game"]');
117+
if (await startButton.isVisible()) {
118+
await startButton.click();
119+
}
120+
121+
// Click on e2 pawn
122+
const e2Square = page.locator('td[data-row="2"][data-col="5"], [data-square="e2"]');
123+
await e2Square.click();
124+
125+
// Verify valid moves are highlighted
126+
const highlightedSquares = page.locator('[class*="valid"], [class*="highlight"], [data-valid="true"]');
127+
await expect(highlightedSquares).toHaveCount(2); // e3 and e4
128+
129+
// Click on highlighted e4
130+
const e4Square = page.locator('td[data-row="4"][data-col="5"], [data-square="e4"]');
131+
await e4Square.click();
132+
133+
// Verify move was made and highlights are gone
134+
await expect(highlightedSquares).toHaveCount(0);
135+
});
136+
137+
test('should reject invalid moves', async ({ page }) => {
138+
// Start game
139+
const startButton = page.locator('button:has-text("Start"), [data-testid="start-game"]');
140+
if (await startButton.isVisible()) {
141+
await startButton.click();
142+
}
143+
144+
// Try to move knight like a bishop (invalid)
145+
const b1Square = page.locator('td[data-row="1"][data-col="2"], [data-square="b1"]');
146+
await b1Square.click();
147+
148+
// Try to move to d3 (invalid for knight)
149+
const d3Square = page.locator('td[data-row="3"][data-col="4"], [data-square="d3"]');
150+
await d3Square.click();
151+
152+
// Verify move was not made (turn should still be White)
153+
const turnIndicator = page.locator('[class*="turn"], [data-testid="turn"]');
154+
if (await turnIndicator.isVisible()) {
155+
await expect(turnIndicator).toContainText(/White|white/i);
156+
}
157+
158+
// Verify no error message or invalid move feedback
159+
const errorMessage = page.locator('[class*="error"], [data-testid="error"]');
160+
if (await errorMessage.isVisible()) {
161+
await expect(errorMessage).toBeVisible(); // If error UI exists
162+
}
163+
});
164+
});

frontend/Dockerfile

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ WORKDIR /app
55
# Copy package files
66
COPY package*.json ./
77

8-
# Install dependencies
8+
# Install dependencies (dev + prod) for build
99
RUN npm ci
1010

1111
# Copy source code
@@ -14,25 +14,30 @@ COPY . .
1414
# Build the Next.js application
1515
RUN npm run build
1616

17+
# Ensure font-manifest.json exists to avoid runtime MODULE_NOT_FOUND
18+
RUN mkdir -p .next/server \
19+
&& if [ ! -f .next/server/font-manifest.json ]; then echo '{}' > .next/server/font-manifest.json; fi
20+
21+
# Remove dev dependencies to keep node_modules production-only
22+
RUN npm prune --production
23+
1724
# Runtime stage
1825
FROM node:18-alpine
1926
WORKDIR /app
2027

2128
# Copy package files
2229
COPY package*.json ./
2330

24-
# Install production dependencies only
25-
RUN npm ci --omit=dev
26-
27-
# Copy built application from build stage
31+
# Copy built application and production node_modules from build stage
2832
COPY --from=build /app/.next ./.next
2933
COPY --from=build /app/public ./public
34+
COPY --from=build /app/node_modules ./node_modules
3035

3136
# Expose port
3237
EXPOSE 3000
3338

3439
# Set environment to production
35-
ENV NODE_ENV=production
40+
# ENV NODE_ENV=production
3641

3742
# Start the application
3843
CMD ["npm", "start"]

frontend/postcss.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
plugins: {
3+
autoprefixer: {},
4+
},
5+
}
6+

0 commit comments

Comments
 (0)