Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ yarn.lock
# Compiled JavaScript files
*.js
*.js.map
!e2e/*.spec.js
dist/
build/
out/
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,18 @@ npm run test:e2e:report

E2E tests run across multiple browsers (Chrome, Firefox, Safari) and device types (desktop, tablet, mobile).

## Responsive Breakpoints

* Base `<640px` (mobile portrait): primary target for scaling grid/cell size; covers iPhone SE/8/X and similar compact Android devices. Grid may enable internal scroll while Move controls stay anchored.
* `sm` ≥640px: larger phones in landscape and small tablets; maintains scaled grid while restoring horizontal control layouts.
* `md` ≥768px: tablets in portrait/landscape such as iPad Mini/Air; gameplay grid approaches desktop sizing.
* `lg` ≥1024px: laptop-width viewports; grid reaches full desktop dimensions with generous control spacing.
* `xl` ≥1280px: widescreen desktops/monitors; matches existing desktop layout without additional scaling.
* `2xl` ≥1536px: very wide monitors/TVs; no additional changes planned beyond standard Tailwind defaults.

## Leaderboard

The leaderboard is backed by the Supabase Postgres database named `prbase`. Supabase credentials for local and remote environments live in `.env.local`; confirm they match project secrets before running database tasks.
The leaderboard is backed by the Supabase Postgres database. Supabase credentials for local and remote environments live in `.env.local`; confirm they match project secrets before running database tasks.

### Common Supabase Commands

Expand Down
54 changes: 54 additions & 0 deletions docs/plans/responsive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Responsive Gameplay Grid & Controls Plan

## Goals
- Ensure the gameplay grid scales down gracefully on portrait mobile screens without affecting the current desktop/landscape experience.
- Tweak the Move direction controls so they remain accessible on small screens while keeping desktop sizing intact.
- Improve the Bailout button layout and prominence on portrait mobile, matching the Start Game button's visual weight and floating it to the right of the Move controls.
- On very small viewports (e.g., iPhone SE/iPhone X and smaller), let the gameplay grid container scroll vertically while keeping the Move controls fixed beneath it.

## Constraints & Considerations
- Preserve the existing grid size and layout on desktop and landscape orientations.
- Keep changes scoped to responsive styles and layout containers to limit gameplay logic churn.
- Maintain accessibility: buttons must retain minimum touch targets and focus outlines.
- Verify no regressions in existing tests; add responsive-focused unit/UI checks where practical.

## Work Breakdown
1. **Audit Current Layout Behaviors**
- Capture current grid/container dimensions across breakpoints (mobile portrait, mobile landscape, tablet, desktop).
- Document Tailwind classes or custom CSS that lock grid sizing, gap, or font scales.
- Inspect Move and Bailout button styles and layout wrapper structure.
- Findings (Apr 2025):
- Gameplay grid lives in `src/App.tsx` inside a `div` with Tailwind `grid gap-0 rounded-lg border-2 border-gray-600 bg-gray-800 p-2`; column sizing is controlled via inline `gridTemplateColumns` using a calculated `cellSize` (max 30px) derived from `maxGridWidth = 720` and level size, so large levels (≥18) exceed 540px and overflow small portrait widths.
- Each cell uses inline `width`/`height` equal to `cellSize` with `minWidth/minHeight` of 4px, so scaling is locked to fixed pixels outside Tailwind breakpoints.
- Container is wrapped by a flex column centering everything with `min-h-screen`; no constraints prevent the grid from expanding beyond viewport height/width.
- Move pad sits in a `div` using `grid w-48 grid-cols-3 gap-2` and buttons sized via `px-4 py-3 text-xl`, yielding ~48rem (~192px) width that stays centered; no responsive overrides shrink padding/font below `sm`.
- Bailout button lives in adjacent flex column with `px-4 py-2 text-sm`; on narrow screens it wraps beneath the Move pad because parent stack `flex-col` aligns center with `gap-6` only switching to `sm:flex-row`.

2. **Responsive Grid Sizing Strategy**
- Introduce CSS/Tailwind utilities that cap grid width/height using viewport-fitting rules (`max-w`, `max-h`, `vw`, `vh`).
- Adjust the grid container to honor aspect ratio while shrinking on portrait <=640px (mobile-first) without altering base desktop classes.
- Add safe minimum size thresholds so touch targets remain usable; consider scaling cell font/icons accordingly.
- Gate a `max-h` + `overflow-y-auto` treatment behind a very-small breakpoint so the grid itself scrolls instead of collapsing into the Move controls.
- Implementation approach:
- Introduce a `useViewportSize` hook and clamp `cellSize` via `useMemo`, using viewport width/height bounds with a 30px desktop ceiling and a 2px floor on compact screens so even 100×100 boards stay in view.
- Cap the grid wrapper with computed `maxWidth`/`maxHeight` values and toggle `overflow-y-auto` for widths ≤380px, containing scroll to the grid while keeping Move controls static.
- Retain the desktop experience by falling back to `fit-content` sizing whenever the viewport is ≥640px so existing styling remains unchanged.

3. **Move Buttons Refinement**
- Apply responsive utility classes (e.g., `sm:`/`md:` prefixes) to reduce button padding/font-size by ~20-25% on screens <640px.
- Re-align the button group to the left on small screens via flexbox utilities while preserving current alignment for larger breakpoints.
- Verify spacing between buttons remains consistent; adjust margin/padding stack to avoid overflow.

4. **Bailout Button Enhancements**
- Match typography and padding to Start Game styles by extracting shared Tailwind classes or creating a reusable style helper.
- On portrait mobile, float/position the button to the right of the Move controls using responsive flex or grid layout.
- Ensure wrapping does not occur; test with worst-case localization/label length.

5. **Testing & Validation**
- Update or extend component tests to cover responsive class toggles if testable (snapshot or DOM attribute checks under simulated viewport width).
- Run `npm run test:run` and `npm run test:e2e` to confirm no regressions.
- Manually verify layouts in browser dev tools for key breakpoints (iPhone SE, iPhone 14 Pro Max, iPad, desktop wide).

6. **Documentation & Follow-up**
- Note responsive design rationale in code comments or a README snippet if needed.
- Flag any follow-on tasks (e.g., responsive typography audit) discovered during implementation.
157 changes: 157 additions & 0 deletions e2e/accessibility.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { test, expect } from '@playwright/test';

test.describe('Accessibility Tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});

test('should have proper heading structure', async ({ page }) => {
// Check main heading
const mainHeading = page.getByRole('heading', { level: 1 });
await expect(mainHeading).toBeVisible();
await expect(mainHeading).toHaveText('🔥 HEATSEEKER 🔥');

// Check subheading
const subHeading = page.getByRole('heading', { level: 2 });
await expect(subHeading).toBeVisible();
await expect(subHeading).toHaveText('Game Rules:');
Comment on lines +14 to +17

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Accessibility test looks for a heading that the UI never renders

The accessibility suite asserts that an <h2> contains the text Game Rules:. The start screen’s subheading is currently Don't step in lava!, so the locator will never resolve and the test fails immediately. Either update the test to match the actual heading or render the expected text in the UI.

Useful? React with 👍 / 👎.

});

test('should have accessible buttons', async ({ page }) => {
// Start button should be accessible
const startButton = page.getByRole('button', { name: 'Start Game' });
await expect(startButton).toBeVisible();
await expect(startButton).toBeEnabled();

// Start the game to test mobile controls
await startButton.click();

// Mobile control buttons should be accessible
const upButton = page.getByRole('button', { name: '↑' });
const downButton = page.getByRole('button', { name: '↓' });
const leftButton = page.getByRole('button', { name: '←' });
const rightButton = page.getByRole('button', { name: '→' });

await expect(upButton).toBeVisible();
await expect(upButton).toBeEnabled();
await expect(downButton).toBeVisible();
await expect(downButton).toBeEnabled();
await expect(leftButton).toBeVisible();
await expect(leftButton).toBeEnabled();
await expect(rightButton).toBeVisible();
await expect(rightButton).toBeEnabled();
});

test('should support keyboard navigation', async ({ page }) => {
// Tab to start button
await page.keyboard.press('Tab');

// Start button should be focused
const startButton = page.getByRole('button', { name: 'Start Game' });
await expect(startButton).toBeFocused();

// Press Enter to activate
await page.keyboard.press('Enter');

// Game should start
await expect(page.getByText('Level: 1 of 10')).toBeVisible();
});

test('should support keyboard game controls', async ({ page }) => {
await page.getByRole('button', { name: 'Start Game' }).click();

// Arrow keys should work for game control
await page.keyboard.press('ArrowRight');
await expect(page.getByText('Level Moves: 1')).toBeVisible();

await page.keyboard.press('ArrowUp');
await expect(page.getByText('Level Moves: 2')).toBeVisible();

await page.keyboard.press('ArrowLeft');
await expect(page.getByText('Level Moves: 3')).toBeVisible();

await page.keyboard.press('ArrowDown');
await expect(page.getByText('Level Moves: 4')).toBeVisible();
});

test('should have sufficient color contrast', async ({ page }) => {
// The game uses color to convey important information
// We should ensure text has good contrast against backgrounds

// Check main title contrast
const title = page.getByText('🔥 HEATSEEKER 🔥');
await expect(title).toBeVisible();

// Check button contrast
const startButton = page.getByRole('button', { name: 'Start Game' });
await expect(startButton).toBeVisible();

// Start game and check game UI contrast
await startButton.click();

const levelText = page.getByText('Level: 1 of 10');
await expect(levelText).toBeVisible();

const movesText = page.getByText('Level Moves: 0');
await expect(movesText).toBeVisible();
});

test('should work with reduced motion preferences', async ({ page }) => {
// Simulate reduced motion preference
await page.emulateMedia({ reducedMotion: 'reduce' });

await page.goto('/');

// Game should still be functional
await page.getByRole('button', { name: 'Start Game' }).click();
await expect(page.getByText('Level: 1 of 10')).toBeVisible();

// Controls should still work
await page.getByRole('button', { name: '→' }).click();
await expect(page.getByText('Level Moves: 1')).toBeVisible();
});

test('should handle high contrast mode', async ({ page }) => {
// Simulate high contrast mode
await page.emulateMedia({ colorScheme: 'dark', forcedColors: 'active' });

await page.goto('/');

// Essential elements should still be visible
await expect(page.getByText('🔥 HEATSEEKER 🔥')).toBeVisible();
await expect(page.getByRole('button', { name: 'Start Game' })).toBeVisible();

// Game should be playable
await page.getByRole('button', { name: 'Start Game' }).click();
await expect(page.getByText('Level: 1 of 10')).toBeVisible();
});

test('should be usable with screen reader simulation', async ({ page }) => {
// This test simulates some screen reader behaviors

// Check that important content has text that would be read aloud
await expect(page.getByText('🔥 HEATSEEKER 🔥')).toBeVisible();

const startButton = page.getByRole('button', { name: 'Start Game' });
await expect(startButton).toBeVisible();

await startButton.click();

// Game status should be clearly communicated
await expect(page.getByText('Level: 1 of 10')).toBeVisible();
await expect(page.getByText('Level Moves: 0')).toBeVisible();
});

test('should maintain focus management', async ({ page }) => {
await page.getByRole('button', { name: 'Start Game' }).click();

// Tab through mobile controls
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');

// Should be able to focus on mobile control buttons
const focusedElement = page.locator(':focus');
await expect(focusedElement).toBeVisible();
});
});
Loading