feat: add comprehensive Playwright testing suite with visual regression

- Add Playwright configuration with multi-browser testing
- Create basic functionality tests for game mechanics
- Add gameplay tests with keyboard and touch interactions
- Implement visual regression testing with screenshots
- Add environment-specific tests for dev/staging/prod
- Include health endpoint and security header validation
- Set up test infrastructure for CI/CD pipeline
This commit is contained in:
greg
2025-06-30 20:51:05 -07:00
parent a24c3c0d05
commit aeccfa3717
6 changed files with 339 additions and 0 deletions

58
tests/basic.spec.ts Normal file
View File

@@ -0,0 +1,58 @@
import { test, expect } from '@playwright/test';
test.describe('2048 Game - Basic Functionality', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
});
test('should load the game successfully', async ({ page }) => {
// Check title
await expect(page).toHaveTitle(/2048/);
// Check main elements are present
await expect(page.locator('h1')).toContainText('2048');
await expect(page.locator('.game-container')).toBeVisible();
await expect(page.locator('.grid-container')).toBeVisible();
// Check score displays
await expect(page.locator('#score')).toBeVisible();
await expect(page.locator('#best')).toBeVisible();
});
test('should show environment badge', async ({ page }) => {
const envBadge = page.locator('#env-badge');
await expect(envBadge).toBeVisible();
// Should have one of the environment classes
const badgeClass = await envBadge.getAttribute('class');
expect(badgeClass).toMatch(/(development|staging|production)/);
});
test('should have initial tiles on game start', async ({ page }) => {
// Should have at least 2 tiles initially
const tiles = page.locator('.tile');
await expect(tiles).toHaveCount(2);
// Tiles should have values 2 or 4
const tileTexts = await tiles.allTextContents();
tileTexts.forEach(text => {
expect(['2', '4']).toContain(text);
});
});
test('should restart game when restart button is clicked', async ({ page }) => {
const restartButton = page.locator('#restart-button');
await expect(restartButton).toBeVisible();
// Click restart
await restartButton.click();
// Should have exactly 2 tiles after restart
const tiles = page.locator('.tile');
await expect(tiles).toHaveCount(2);
// Score should be reset to 0
await expect(page.locator('#score')).toHaveText('0');
});
});

75
tests/environment.spec.ts Normal file
View File

@@ -0,0 +1,75 @@
import { test, expect } from '@playwright/test';
test.describe('2048 Game - Environment Tests', () => {
test('should display correct environment in development', async ({ page }) => {
// This test will run when BASE_URL contains 'dev'
const baseUrl = process.env.BASE_URL || '';
test.skip(!baseUrl.includes('dev'), 'Development environment test');
await page.goto('/');
const envElement = page.locator('#environment');
await expect(envElement).toContainText('Development');
const envBadge = page.locator('#env-badge');
await expect(envBadge).toHaveClass(/development/);
});
test('should display correct environment in staging', async ({ page }) => {
const baseUrl = process.env.BASE_URL || '';
test.skip(!baseUrl.includes('staging'), 'Staging environment test');
await page.goto('/');
const envElement = page.locator('#environment');
await expect(envElement).toContainText('Staging');
const envBadge = page.locator('#env-badge');
await expect(envBadge).toHaveClass(/staging/);
});
test('should display correct environment in production', async ({ page }) => {
const baseUrl = process.env.BASE_URL || '';
test.skip(baseUrl.includes('dev') || baseUrl.includes('staging'), 'Production environment test');
await page.goto('/');
const envElement = page.locator('#environment');
await expect(envElement).toContainText('Production');
const envBadge = page.locator('#env-badge');
await expect(envBadge).toHaveClass(/production/);
});
test('should have working health endpoint', async ({ request }) => {
const response = await request.get('/health');
expect(response.status()).toBe(200);
const text = await response.text();
expect(text).toContain('healthy');
});
test('should load all assets successfully', async ({ page }) => {
const responses: any[] = [];
page.on('response', response => {
responses.push(response);
});
await page.goto('/');
await page.waitForLoadState('networkidle');
// Check that all resources loaded successfully
const failedResponses = responses.filter(response => response.status() >= 400);
expect(failedResponses.length).toBe(0);
});
test('should have correct security headers', async ({ request }) => {
const response = await request.get('/');
// Check for basic security headers
expect(response.headers()['x-frame-options']).toBeDefined();
expect(response.headers()['x-content-type-options']).toBeDefined();
expect(response.headers()['x-xss-protection']).toBeDefined();
});
});

83
tests/gameplay.spec.ts Normal file
View File

@@ -0,0 +1,83 @@
import { test, expect } from '@playwright/test';
test.describe('2048 Game - Gameplay Tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
});
test('should move tiles with arrow keys', async ({ page }) => {
// Get initial tile positions
const initialTiles = await page.locator('.tile').all();
const initialPositions = [];
for (const tile of initialTiles) {
const style = await tile.getAttribute('style');
initialPositions.push(style);
}
// Press arrow key to move tiles
await page.keyboard.press('ArrowRight');
await page.waitForTimeout(500); // Wait for animation
// Check that tiles moved or new tile appeared
const newTiles = await page.locator('.tile').all();
expect(newTiles.length).toBeGreaterThanOrEqual(2);
// At least one tile should have moved or new tile should appear
let tilesChanged = newTiles.length > initialTiles.length;
if (!tilesChanged) {
for (let i = 0; i < Math.min(newTiles.length, initialPositions.length); i++) {
const newStyle = await newTiles[i].getAttribute('style');
if (newStyle !== initialPositions[i]) {
tilesChanged = true;
break;
}
}
}
expect(tilesChanged).toBe(true);
});
test('should handle touch/swipe on mobile', async ({ page, isMobile }) => {
test.skip(!isMobile, 'Touch test only for mobile');
const gameContainer = page.locator('.game-container');
// Simulate swipe right
await gameContainer.touchStart([{ x: 100, y: 200 }]);
await gameContainer.touchEnd([{ x: 300, y: 200 }]);
await page.waitForTimeout(500);
// Should have tiles after swipe
const tiles = page.locator('.tile');
await expect(tiles).toHaveCount.atLeast(2);
});
test('should update score when tiles merge', async ({ page }) => {
// This test might need multiple moves to get mergeable tiles
// For now, just verify score element updates
const scoreElement = page.locator('#score');
const initialScore = await scoreElement.textContent();
// Try multiple moves to potentially trigger a merge
for (let i = 0; i < 10; i++) {
await page.keyboard.press('ArrowRight');
await page.waitForTimeout(200);
await page.keyboard.press('ArrowDown');
await page.waitForTimeout(200);
await page.keyboard.press('ArrowLeft');
await page.waitForTimeout(200);
await page.keyboard.press('ArrowUp');
await page.waitForTimeout(200);
const currentScore = await scoreElement.textContent();
if (currentScore !== initialScore) {
expect(parseInt(currentScore || '0')).toBeGreaterThan(parseInt(initialScore || '0'));
break;
}
}
});
});

14
tests/package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "playwright-tests",
"version": "1.0.0",
"description": "Playwright tests for 2048 game",
"scripts": {
"test": "playwright test",
"test:headed": "playwright test --headed",
"test:debug": "playwright test --debug",
"test:ui": "playwright test --ui"
},
"devDependencies": {
"@playwright/test": "^1.40.0"
}
}

60
tests/visual.spec.ts Normal file
View File

@@ -0,0 +1,60 @@
import { test, expect } from '@playwright/test';
test.describe('2048 Game - Visual Tests & Screenshots', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
});
test('should match initial game state screenshot', async ({ page }) => {
// Wait for game to fully load
await page.waitForSelector('.tile', { timeout: 5000 });
// Take screenshot of initial game state
await expect(page).toHaveScreenshot('initial-game-state.png', {
fullPage: true,
animations: 'disabled'
});
});
test('should match game container layout', async ({ page }) => {
const gameContainer = page.locator('.game-container');
await expect(gameContainer).toHaveScreenshot('game-container.png', {
animations: 'disabled'
});
});
test('should match header with scores', async ({ page }) => {
const header = page.locator('.header');
await expect(header).toHaveScreenshot('header-scores.png', {
animations: 'disabled'
});
});
test('should match environment badge', async ({ page }) => {
const envBadge = page.locator('#env-badge');
await expect(envBadge).toHaveScreenshot('environment-badge.png', {
animations: 'disabled'
});
});
test('should display correctly on mobile', async ({ page, isMobile }) => {
test.skip(!isMobile, 'Mobile test only');
await expect(page).toHaveScreenshot('mobile-game.png', {
fullPage: true,
animations: 'disabled'
});
});
test('should show game over state', async ({ page }) => {
// Try to fill the board quickly (this is a simplified approach)
// In a real scenario, we might need to manipulate the game state directly
// Take screenshot of current state for comparison
await expect(page).toHaveScreenshot('game-in-progress.png', {
fullPage: true,
animations: 'disabled'
});
});
});