Compare commits

..

17 Commits

Author SHA1 Message Date
semantic-release-bot
caebefdce8 chore(release): 2.3.1 [skip ci]
## [2.3.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.3.0...v2.3.1) (2025-10-07)

### Bug Fixes

* add missing DOMPoint properties to getPointAtLength mock ([1e17278](1e17278f94))
* add missing name property to Passenger test mocks ([f8ca248](f8ca248844))
* add non-null assertions to skillConfiguration utilities ([9c71092](9c71092278))
* add optional chaining to stepBeadHighlights access ([a5fac5c](a5fac5c75c))
* add showAsAbacus property to ComplementQuestion type ([4adcc09](4adcc09643))
* add userId to optimistic player in useCreatePlayer ([5310463](5310463bec))
* change TypeScript moduleResolution from bundler to node ([327aee0](327aee0b4b))
* convert Jest mocks to Vitest in useSteamJourney tests ([e067271](e06727160c))
* convert player IDs from number to string in arcade tests ([72db1f4](72db1f4a2c))
* rewrite layout.nav.test to match actual RootLayout props ([a085de8](a085de816f))
* update useArcadeGuard tests with proper useViewerId mock ([4eb49d1](4eb49d1d44))
* use Object.defineProperty for NODE_ENV in middleware tests ([e73191a](e73191a729))
* wrap Buffer in Uint8Array for Next.js Response API ([98384d2](98384d264e))

### Documentation

* add explicit package.json script references to regime docs ([3353bca](3353bcadc2))
* establish mandatory code quality regime for Claude Code ([dd11043](dd1104310f))
* expand quality regime to define "done" for all work ([f92f7b5](f92f7b592a))
2025-10-07 20:45:07 +00:00
Thomas Hallock
a5fac5c75c fix: add optional chaining to stepBeadHighlights access
Add optional chaining (?.) when accessing stepBeadHighlights
to handle cases where it may be undefined.

Provides fallback to empty array when stepBeadHighlights is
not present, preventing potential runtime errors.

Fixes potential TS18048 error in progressive-test-suite.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 15:39:12 -05:00
Thomas Hallock
9c71092278 fix: add non-null assertions to skillConfiguration utilities
Add ! non-null assertion operator to target.basic,
target.advanced, and target.expert property accesses.

These objects are conditionally created earlier in the
function and are guaranteed to exist when accessed. The
assertions inform TypeScript of this runtime guarantee.

Fixes 9 TS18046 errors in skillConfiguration.ts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 15:38:59 -05:00
Thomas Hallock
5310463bec fix: add userId to optimistic player in useCreatePlayer
Add temporary userId property to optimistic player object
to satisfy Player type requirements.

The Player type requires userId, but createPlayer only
accepts name/emoji/color. The optimistic update now includes
a temporary userId that gets replaced by the server response.

Fixes 1 TS2741 error in useUserPlayers.ts:108.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 15:38:53 -05:00
Thomas Hallock
4eb49d1d44 fix: update useArcadeGuard tests with proper useViewerId mock
Replace invalid userId parameter with proper useViewerId mock
that returns a UseQueryResult object.

The useArcadeGuard hook uses useViewerId() which returns a
React Query result object, not a plain string. Updated mocks
return {data, isLoading, error} to match the actual hook.

Also fixed all renderHook call syntax errors from previous
automated replacements.

Fixes ~15 TypeScript errors in useArcadeGuard.test.ts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 15:38:46 -05:00
Thomas Hallock
a085de816f fix: rewrite layout.nav.test to match actual RootLayout props
Remove tests for non-existent 'nav' slot prop and rewrite
tests to match the actual RootLayout implementation.

The RootLayout component only accepts children, not a nav
prop. Updated tests verify the actual component behavior
with ClientProviders wrapper.

Fixes 2 TS2322 errors in layout.nav.test.tsx.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 15:38:38 -05:00
Thomas Hallock
72db1f4a2c fix: convert player IDs from number to string in arcade tests
Change all player ID values from numeric (1) to string ("1")
to match the arcade session schema.

The arcade session system uses string IDs for players, not
numbers. This aligns test data with production types.

Fixes 8 TS2322 errors in arcade session tests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 15:38:28 -05:00
Thomas Hallock
1e17278f94 fix: add missing DOMPoint properties to getPointAtLength mock
Add w, z, matrixTransform, and toJSON properties to mock
getPointAtLength return value to satisfy DOMPoint interface.

The SVGPathElement.getPointAtLength() method returns a full
DOMPoint object, not just {x, y}. This fix ensures the mock
matches the real interface.

Fixes 2 TS2322 errors in useTrackManagement tests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 15:38:13 -05:00
Thomas Hallock
f8ca248844 fix: add missing name property to Passenger test mocks
Add required name property to Passenger mock objects in
usePassengerAnimations and useTrackManagement tests.

The Passenger interface requires a name property. Adding it
to test mocks ensures type correctness.

Fixes 5 TS2741 errors in passenger-related tests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 15:16:29 -05:00
Thomas Hallock
4adcc09643 fix: add showAsAbacus property to ComplementQuestion type
Import ComplementQuestion type from gameTypes and add
showAsAbacus property to test mocks and component interfaces.

The ComplementQuestion interface requires showAsAbacus as a
required property. Using the imported type ensures consistency
and fixes missing property errors.

Fixes ~34 TS2741 errors in complement-race components/tests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 15:16:22 -05:00
Thomas Hallock
e06727160c fix: convert Jest mocks to Vitest in useSteamJourney tests
Replace jest.mock() and jest.* calls with vi.mock() and vi.*
for Vitest compatibility.

This project uses Vitest, not Jest. The Jest namespace was
causing TS2694 "namespace cannot be used as a value" errors.

Fixes ~20 TypeScript errors in passenger test files.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 15:16:14 -05:00
Thomas Hallock
98384d264e fix: wrap Buffer in Uint8Array for Next.js Response API
Wrap Buffer objects with new Uint8Array() to satisfy Next.js
Response BodyInit type requirements.

Next.js 13+ requires BodyInit types (not Buffer) for Response
constructors. This change maintains binary compatibility while
satisfying the type checker.

Fixes 3 TS2345 errors in API routes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 15:16:06 -05:00
Thomas Hallock
e73191a729 fix: use Object.defineProperty for NODE_ENV in middleware tests
Replace direct NODE_ENV assignments with Object.defineProperty
to avoid "Cannot assign to read-only property" TypeScript errors.

This allows tests to safely override the readonly NODE_ENV
environment variable for testing different environments.

Fixes 4 TS2540 errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 15:15:58 -05:00
Thomas Hallock
327aee0b4b fix: change TypeScript moduleResolution from bundler to node
Change moduleResolution from "bundler" to "node" for better
compatibility with pnpm workspace package resolution.

This helps TypeScript better resolve workspace dependencies
while maintaining compatibility with Next.js.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 15:15:49 -05:00
Thomas Hallock
3353bcadc2 docs: add explicit package.json script references to regime docs
Update .claude documentation to reference the actual npm scripts
from package.json, making it crystal clear what commands to run
and what they do under the hood.

**Added:**
- Exact script definitions from package.json
- What each script does (tsc, Biome, ESLint)
- Tool descriptions (TypeScript, Biome, ESLint)
- Quick reference sections for fast lookup

**Why:**
Makes it easier for Claude Code sessions to know exactly which
commands to run without ambiguity.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 12:54:25 -05:00
Thomas Hallock
f92f7b592a docs: expand quality regime to define "done" for all work
Update regime documentation to clarify that quality checks must pass
not just before commits, but before declaring ANY work complete.

**"Done" means:**
- npm run pre-commit passes (0 errors, 0 warnings)
- All TypeScript errors fixed
- All code formatted
- All linting passed

**Quality checks required before:**
- Committing code
- Saying work is "done" or "complete"
- Marking tasks as finished
- Telling the user something is "working" or "fixed"

This ensures quality standards are maintained throughout development,
not just at commit time.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 12:53:31 -05:00
Thomas Hallock
dd1104310f docs: establish mandatory code quality regime for Claude Code
Add comprehensive documentation and tooling to enforce code quality
checks before every commit. This regime persists across all Claude
Code sessions via `.claude/` directory files.

**The Regime** (mandatory before every commit):
1. TypeScript type checking (0 errors)
2. Biome formatting (auto-applied)
3. Linting with auto-fix (0 errors, 0 warnings)
4. Final verification

**Implementation:**
- `.claude/CLAUDE.md`: Quick reference for Claude Code sessions
- `.claude/CODE_QUALITY_REGIME.md`: Detailed regime documentation
- `npm run pre-commit`: Single command to run all checks

**Why no pre-commit hooks:**
Avoided for religious reasons. Claude Code is responsible for
enforcing quality checks through session-persistent documentation.

**Usage:**
```bash
# Before every commit
npm run pre-commit

# Or run steps individually
npm run type-check
npm run format
npm run lint:fix
npm run lint
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 12:52:29 -05:00
26 changed files with 394 additions and 154 deletions

View File

@@ -1,3 +1,29 @@
## [2.3.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.3.0...v2.3.1) (2025-10-07)
### Bug Fixes
* add missing DOMPoint properties to getPointAtLength mock ([1e17278](https://github.com/antialias/soroban-abacus-flashcards/commit/1e17278f942b3fbcc5d05be746178f2e780f0bd9))
* add missing name property to Passenger test mocks ([f8ca248](https://github.com/antialias/soroban-abacus-flashcards/commit/f8ca2488447e89151085942f708f6acf350a2747))
* add non-null assertions to skillConfiguration utilities ([9c71092](https://github.com/antialias/soroban-abacus-flashcards/commit/9c7109227822884d25f8546739c80c6e7491e28d))
* add optional chaining to stepBeadHighlights access ([a5fac5c](https://github.com/antialias/soroban-abacus-flashcards/commit/a5fac5c75c8cd67b218a5fd5ad98818dad74ab67))
* add showAsAbacus property to ComplementQuestion type ([4adcc09](https://github.com/antialias/soroban-abacus-flashcards/commit/4adcc096430fbb03f0a8b2f0aef4be239aff9cd0))
* add userId to optimistic player in useCreatePlayer ([5310463](https://github.com/antialias/soroban-abacus-flashcards/commit/5310463becd0974291cff49522ae5669a575410d))
* change TypeScript moduleResolution from bundler to node ([327aee0](https://github.com/antialias/soroban-abacus-flashcards/commit/327aee0b4b5c0b0b2bf3eeb48d861bb3068f6127))
* convert Jest mocks to Vitest in useSteamJourney tests ([e067271](https://github.com/antialias/soroban-abacus-flashcards/commit/e06727160c70a1ab38a003104d1fef8fb83ff92d))
* convert player IDs from number to string in arcade tests ([72db1f4](https://github.com/antialias/soroban-abacus-flashcards/commit/72db1f4a2c3f930025cd5ced3fcf7c810dcc569d))
* rewrite layout.nav.test to match actual RootLayout props ([a085de8](https://github.com/antialias/soroban-abacus-flashcards/commit/a085de816fcdeb055addabb8aec391b111cb5f94))
* update useArcadeGuard tests with proper useViewerId mock ([4eb49d1](https://github.com/antialias/soroban-abacus-flashcards/commit/4eb49d1d44e1d85526ef6564f88a8fbcebffb4d2))
* use Object.defineProperty for NODE_ENV in middleware tests ([e73191a](https://github.com/antialias/soroban-abacus-flashcards/commit/e73191a7298dbb6dd15da594267ea6221062c36b))
* wrap Buffer in Uint8Array for Next.js Response API ([98384d2](https://github.com/antialias/soroban-abacus-flashcards/commit/98384d264e4a10d1836aa9f2e69151b122ffa7b0))
### Documentation
* add explicit package.json script references to regime docs ([3353bca](https://github.com/antialias/soroban-abacus-flashcards/commit/3353bcadc2849104248c624973274ed90b86722a))
* establish mandatory code quality regime for Claude Code ([dd11043](https://github.com/antialias/soroban-abacus-flashcards/commit/dd1104310f4e0e85640730ea0e96e4adda4bc505))
* expand quality regime to define "done" for all work ([f92f7b5](https://github.com/antialias/soroban-abacus-flashcards/commit/f92f7b592af38ba9d0f5b1db3a061d63d92a5093))
## [2.3.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.2.1...v2.3.0) (2025-10-07)

View File

@@ -0,0 +1,86 @@
# Claude Code Instructions for apps/web
## MANDATORY: Quality Checks for ALL Work
**BEFORE declaring ANY work complete, fixed, or working**, you MUST run and pass these checks:
### When This Applies
- Before every commit
- Before saying "it's done" or "it's fixed"
- Before marking a task as complete
- Before telling the user something is working
- After any code changes, no matter how small
```bash
npm run pre-commit
```
This single command runs all quality checks in the correct order:
1. `npm run type-check` - TypeScript type checking (must have 0 errors)
2. `npm run format` - Auto-format all code with Biome
3. `npm run lint:fix` - Auto-fix linting issues with Biome + ESLint
4. `npm run lint` - Verify 0 errors, 0 warnings
**DO NOT COMMIT** until all checks pass with zero errors and zero warnings.
## Available Scripts
```bash
npm run type-check # TypeScript: tsc --noEmit
npm run format # Biome: format all files
npm run format:check # Biome: check formatting without fixing
npm run lint # Biome + ESLint: check for errors/warnings
npm run lint:fix # Biome + ESLint: auto-fix issues
npm run check # Biome: full check (format + lint + imports)
npm run pre-commit # Run all checks (type + format + lint)
```
## Workflow
When asked to make ANY changes:
1. Make your code changes
2. Run `npm run pre-commit`
3. If it fails, fix the issues and run again
4. Only after all checks pass can you:
- Say the work is "done" or "complete"
- Mark tasks as finished
- Create commits
- Tell the user it's working
5. Push immediately after committing
**Nothing is complete until `npm run pre-commit` passes.**
## Details
See `.claude/CODE_QUALITY_REGIME.md` for complete documentation.
## No Pre-Commit Hooks
This project does not use git pre-commit hooks for religious reasons.
You (Claude Code) are responsible for enforcing code quality before commits.
## Quick Reference: package.json Scripts
**Primary workflow:**
```bash
npm run pre-commit # ← Use this before every commit
```
**Individual checks (if needed):**
```bash
npm run type-check # TypeScript: tsc --noEmit
npm run format # Biome: format code (--write)
npm run lint # Biome + ESLint: check only
npm run lint:fix # Biome + ESLint: auto-fix
```
**Additional tools:**
```bash
npm run format:check # Check formatting without changing files
npm run check # Biome check (format + lint + organize imports)
```
---
**Remember: Always run `npm run pre-commit` before creating commits.**

View File

@@ -0,0 +1,143 @@
# Code Quality Regime
**MANDATORY**: Before declaring ANY work complete, fixed, or working, Claude MUST run these checks and fix all issues.
## Definition of "Done"
Work is NOT complete until:
- ✅ All TypeScript errors are fixed (0 errors)
- ✅ All code is formatted with Biome
- ✅ All linting passes (0 errors, 0 warnings)
-`npm run pre-commit` exits successfully
**Until these checks pass, the work is considered incomplete.**
## Quality Check Checklist (Always Required)
Run these before:
- Committing code
- Saying work is "done" or "complete"
- Marking tasks as finished
- Telling the user something is "working" or "fixed"
Run these commands in order. All must pass with 0 errors and 0 warnings:
```bash
# 1. Type check
npm run type-check
# 2. Format code
npm run format
# 3. Lint and fix
npm run lint:fix
# 4. Verify clean state
npm run lint && npm run type-check
```
## Quick Command (Run All Checks)
```bash
npm run pre-commit
```
**What it does:**
```json
"pre-commit": "npm run type-check && npm run format && npm run lint:fix && npm run lint"
```
This single command runs:
1. `npm run type-check``tsc --noEmit` (TypeScript errors)
2. `npm run format``npx @biomejs/biome format . --write` (auto-format)
3. `npm run lint:fix``npx @biomejs/biome lint . --write && npx eslint . --fix` (auto-fix)
4. `npm run lint``npx @biomejs/biome lint . && npx eslint .` (verify clean)
Fails fast if any step fails.
## The Regime Rules
### 1. TypeScript Errors: ZERO TOLERANCE
- Run `npm run type-check` before every commit
- Fix ALL TypeScript errors
- No `@ts-ignore` or `@ts-expect-error` without explicit justification
### 2. Formatting: AUTOMATIC
- Run `npm run format` before every commit
- Biome handles all formatting automatically
- Never commit unformatted code
### 3. Linting: ZERO ERRORS, ZERO WARNINGS
- Run `npm run lint:fix` to auto-fix issues
- Then run `npm run lint` to verify 0 errors, 0 warnings
- Fix any remaining issues manually
### 4. Commit Order
1. Make code changes
2. Run `npm run pre-commit`
3. If any check fails, fix and repeat
4. Only commit when all checks pass
5. Push immediately after commit
## Why No Pre-Commit Hooks?
This project intentionally avoids pre-commit hooks due to religious constraints.
Instead, Claude Code is responsible for enforcing this regime through:
1. **This documentation** - Always visible and reference-able
2. **Package.json scripts** - Easy to run checks
3. **Session persistence** - This file lives in `.claude/` and is read by every session
## For Claude Code Sessions
**READ THIS FILE AT THE START OF EVERY SESSION WHERE YOU WILL COMMIT CODE**
When asked to commit:
1. Check if you've run `npm run pre-commit` (or all 4 steps individually)
2. If not, STOP and run the checks first
3. Fix all issues before proceeding with the commit
4. Only create commits when all checks pass
## Complete Scripts Reference
From `apps/web/package.json`:
```json
{
"scripts": {
"type-check": "tsc --noEmit",
"format": "npx @biomejs/biome format . --write",
"format:check": "npx @biomejs/biome format .",
"lint": "npx @biomejs/biome lint . && npx eslint .",
"lint:fix": "npx @biomejs/biome lint . --write && npx eslint . --fix",
"check": "npx @biomejs/biome check .",
"pre-commit": "npm run type-check && npm run format && npm run lint:fix && npm run lint"
}
}
```
**Tools used:**
- TypeScript: `tsc --noEmit` (type checking only, no output)
- Biome: Fast formatter + linter (Rust-based, 10-100x faster than Prettier)
- ESLint: React Hooks rules only (`rules-of-hooks` validation)
## Emergency Override
If you absolutely MUST commit with failing checks:
1. Document WHY in the commit message
2. Create a follow-up task to fix the issues
3. Only use for emergency hotfixes
## Verification
After following this regime, you should see:
```
✓ Type check passed (0 errors)
✓ Formatting applied
✓ Linting passed (0 errors, 0 warnings)
✓ Ready to commit
```
---
**This regime is non-negotiable. Every commit must pass these checks.**

View File

@@ -74,7 +74,10 @@ describe('Middleware E2E', () => {
it('sets secure flag in production', async () => {
const originalEnv = process.env.NODE_ENV
process.env.NODE_ENV = 'production'
Object.defineProperty(process.env, 'NODE_ENV', {
value: 'production',
configurable: true
})
const req = new NextRequest('http://localhost:3000/')
const res = await middleware(req)
@@ -82,12 +85,18 @@ describe('Middleware E2E', () => {
const cookie = res.cookies.get(GUEST_COOKIE_NAME)
expect(cookie?.secure).toBe(true)
process.env.NODE_ENV = originalEnv
Object.defineProperty(process.env, 'NODE_ENV', {
value: originalEnv,
configurable: true
})
})
it('does not set secure flag in development', async () => {
const originalEnv = process.env.NODE_ENV
process.env.NODE_ENV = 'development'
Object.defineProperty(process.env, 'NODE_ENV', {
value: 'development',
configurable: true
})
const req = new NextRequest('http://localhost:3000/')
const res = await middleware(req)
@@ -95,7 +104,10 @@ describe('Middleware E2E', () => {
const cookie = res.cookies.get(GUEST_COOKIE_NAME)
expect(cookie?.secure).toBe(false)
process.env.NODE_ENV = originalEnv
Object.defineProperty(process.env, 'NODE_ENV', {
value: originalEnv,
configurable: true
})
})
it('sets maxAge correctly', async () => {

View File

@@ -11,6 +11,7 @@
"format": "npx @biomejs/biome format . --write",
"format:check": "npx @biomejs/biome format .",
"check": "npx @biomejs/biome check .",
"pre-commit": "npm run type-check && npm run format && npm run lint:fix && npm run lint",
"test": "vitest",
"test:run": "vitest run",
"type-check": "tsc --noEmit",

View File

@@ -1,54 +1,33 @@
import { render, screen } from '@testing-library/react'
import RootLayout from '../layout'
// Mock AppNavBar to verify it receives the nav prop
const MockAppNavBar = ({ navSlot }: { navSlot?: React.ReactNode }) => (
<div data-testid="app-nav-bar">
{navSlot && <div data-testid="nav-slot-content">{navSlot}</div>}
</div>
)
jest.mock('../../components/AppNavBar', () => ({
AppNavBar: MockAppNavBar,
// Mock ClientProviders
vi.mock('../../components/ClientProviders', () => ({
ClientProviders: ({ children }: { children: React.ReactNode }) => (
<div data-testid="client-providers">{children}</div>
),
}))
// Mock all context providers
jest.mock('../../contexts/AbacusDisplayContext', () => ({
AbacusDisplayProvider: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}))
jest.mock('../../contexts/UserProfileContext', () => ({
UserProfileProvider: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}))
jest.mock('../../contexts/GameModeContext', () => ({
GameModeProvider: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}))
jest.mock('../../contexts/FullscreenContext', () => ({
FullscreenProvider: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}))
describe('RootLayout with nav slot', () => {
it('passes nav slot to AppNavBar', () => {
const navContent = <div>Memory Lightning</div>
describe('RootLayout', () => {
it('renders children with ClientProviders', () => {
const pageContent = <div>Page content</div>
render(<RootLayout nav={navContent}>{pageContent}</RootLayout>)
render(<RootLayout>{pageContent}</RootLayout>)
expect(screen.getByTestId('app-nav-bar')).toBeInTheDocument()
expect(screen.getByTestId('nav-slot-content')).toBeInTheDocument()
expect(screen.getByText('Memory Lightning')).toBeInTheDocument()
expect(screen.getByTestId('client-providers')).toBeInTheDocument()
expect(screen.getByText('Page content')).toBeInTheDocument()
})
it('works without nav slot', () => {
const pageContent = <div>Page content</div>
it('renders html and body tags', () => {
const pageContent = <div>Test content</div>
render(<RootLayout nav={null}>{pageContent}</RootLayout>)
const { container } = render(<RootLayout>{pageContent}</RootLayout>)
expect(screen.getByTestId('app-nav-bar')).toBeInTheDocument()
expect(screen.queryByTestId('nav-slot-content')).not.toBeInTheDocument()
expect(screen.getByText('Page content')).toBeInTheDocument()
const html = container.querySelector('html')
const body = container.querySelector('body')
expect(html).toBeInTheDocument()
expect(html).toHaveAttribute('lang', 'en')
expect(body).toBeInTheDocument()
})
})

View File

@@ -23,7 +23,7 @@ export async function GET(_request: NextRequest, { params }: { params: { id: str
console.log('✅ Asset found, serving download')
// Return file with appropriate headers
return new NextResponse(asset.data, {
return new NextResponse(new Uint8Array(asset.data), {
status: 200,
headers: {
'Content-Type': asset.mimeType,

View File

@@ -17,7 +17,7 @@ export async function GET(_request: NextRequest, { params }: { params: { id: str
headers.set('Content-Length', asset.data.length.toString())
headers.set('Cache-Control', 'no-cache, no-store, must-revalidate')
return new NextResponse(asset.data, {
return new NextResponse(new Uint8Array(asset.data), {
status: 200,
headers,
})

View File

@@ -62,7 +62,7 @@ export async function POST(request: NextRequest) {
const filename = `soroban-flashcards-${config.range || 'cards'}.pdf`
// Return PDF directly as download
return new NextResponse(pdfBuffer, {
return new NextResponse(new Uint8Array(pdfBuffer), {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${filename}"`,

View File

@@ -10,6 +10,7 @@ import {
type DisembarkingAnimation,
usePassengerAnimations,
} from '../../hooks/usePassengerAnimations'
import type { ComplementQuestion } from '../../lib/gameTypes'
import { useSteamJourney } from '../../hooks/useSteamJourney'
import { useTrackManagement } from '../../hooks/useTrackManagement'
import { useTrainTransforms } from '../../hooks/useTrainTransforms'
@@ -80,7 +81,7 @@ interface SteamTrainJourneyProps {
trainPosition: number
pressure: number
elapsedTime: number
currentQuestion: { number: number; targetSum: number; correctAnswer: number } | null
currentQuestion: ComplementQuestion | null
currentInput: string
}

View File

@@ -29,6 +29,7 @@ describe('GameHUD', () => {
const mockPassenger: Passenger = {
id: 'passenger-1',
name: 'Test Passenger',
avatar: '👨',
originStationId: 'station-1',
destinationStationId: 'station-2',
@@ -49,6 +50,7 @@ describe('GameHUD', () => {
number: 3,
targetSum: 10,
correctAnswer: 7,
showAsAbacus: false,
},
currentInput: '7',
}

View File

@@ -44,6 +44,7 @@ describe('usePassengerAnimations', () => {
// Create mock passengers
mockPassenger1 = {
id: 'passenger-1',
name: 'Passenger 1',
avatar: '👨',
originStationId: 'station-1',
destinationStationId: 'station-2',
@@ -54,6 +55,7 @@ describe('usePassengerAnimations', () => {
mockPassenger2 = {
id: 'passenger-2',
name: 'Passenger 2',
avatar: '👩',
originStationId: 'station-1',
destinationStationId: 'station-2',

View File

@@ -15,9 +15,9 @@ import type { Passenger, Station } from '../../lib/gameTypes'
import { useSteamJourney } from '../useSteamJourney'
// Mock sound effects
jest.mock('../useSoundEffects', () => ({
vi.mock('../useSoundEffects', () => ({
useSoundEffects: () => ({
playSound: jest.fn(),
playSound: vi.fn(),
}),
}))
@@ -53,12 +53,12 @@ const _testStations: Station[] = [
describe('useSteamJourney - Passenger Boarding', () => {
beforeEach(() => {
jest.useFakeTimers()
vi.useFakeTimers()
})
afterEach(() => {
jest.runOnlyPendingTimers()
jest.useRealTimers()
vi.runOnlyPendingTimers()
vi.useRealTimers()
})
test('passenger boards when train reaches their origin station', () => {
@@ -106,7 +106,7 @@ describe('useSteamJourney - Passenger Boarding', () => {
// Advance timers to trigger the interval
act(() => {
jest.advanceTimersByTime(100)
vi.advanceTimersByTime(100)
})
// Verify passenger boarded
@@ -150,7 +150,7 @@ describe('useSteamJourney - Passenger Boarding', () => {
// Advance timers
act(() => {
jest.advanceTimersByTime(100)
vi.advanceTimersByTime(100)
})
// All three passengers should board (one per car)
@@ -190,7 +190,7 @@ describe('useSteamJourney - Passenger Boarding', () => {
pressure: 120,
elapsedTime: 1000 + pos * 50,
})
jest.advanceTimersByTime(50)
vi.advanceTimersByTime(50)
})
// Check if passenger boarded
@@ -239,7 +239,7 @@ describe('useSteamJourney - Passenger Boarding', () => {
})
act(() => {
jest.advanceTimersByTime(100)
vi.advanceTimersByTime(100)
})
// p2 should board (on car 1 since car 0 is occupied)
@@ -282,7 +282,7 @@ describe('useSteamJourney - Passenger Boarding', () => {
})
act(() => {
jest.advanceTimersByTime(100)
vi.advanceTimersByTime(100)
})
// Passenger should be delivered

View File

@@ -17,7 +17,11 @@ describe('useTrackManagement - Passenger Display', () => {
mockPath.getPointAtLength = vi.fn((distance: number) => ({
x: distance,
y: 300,
}))
w: 1,
z: 0,
matrixTransform: () => new DOMPoint(),
toJSON: () => ({ x: distance, y: 300, w: 1, z: 0 }),
})) as any
mockPathRef = { current: mockPath }
// Mock track generator

View File

@@ -25,7 +25,11 @@ describe('useTrackManagement', () => {
mockPath.getPointAtLength = vi.fn((distance: number) => ({
x: distance,
y: 300,
}))
w: 1,
z: 0,
matrixTransform: () => new DOMPoint(),
toJSON: () => ({ x: distance, y: 300, w: 1, z: 0 }),
})) as any
mockPathRef = { current: mockPath }
// Mock track generator
@@ -52,6 +56,7 @@ describe('useTrackManagement', () => {
mockPassengers = [
{
id: 'passenger-1',
name: 'Passenger 1',
avatar: '👨',
originStationId: 'station-1',
destinationStationId: 'station-2',
@@ -73,6 +78,8 @@ describe('useTrackManagement', () => {
pathRef: mockPathRef,
stations: mockStations,
passengers: mockPassengers,
maxCars: 3,
carSpacing: 7,
})
)
@@ -90,6 +97,8 @@ describe('useTrackManagement', () => {
pathRef: mockPathRef,
stations: mockStations,
passengers: mockPassengers,
maxCars: 3,
carSpacing: 7,
})
)
@@ -107,6 +116,8 @@ describe('useTrackManagement', () => {
pathRef: mockPathRef,
stations: mockStations,
passengers: mockPassengers,
maxCars: 3,
carSpacing: 7,
})
)
@@ -123,6 +134,8 @@ describe('useTrackManagement', () => {
pathRef: mockPathRef,
stations: mockStations,
passengers: mockPassengers,
maxCars: 3,
carSpacing: 7,
})
)
@@ -233,6 +246,7 @@ describe('useTrackManagement', () => {
const newPassengers: Passenger[] = [
{
id: 'passenger-2',
name: 'Passenger 2',
avatar: '👩',
originStationId: 'station-1',
destinationStationId: 'station-2',

View File

@@ -10,6 +10,7 @@ import {
type DisembarkingAnimation,
usePassengerAnimations,
} from '../../hooks/usePassengerAnimations'
import type { ComplementQuestion } from '../../lib/gameTypes'
import { useSteamJourney } from '../../hooks/useSteamJourney'
import { useTrackManagement } from '../../hooks/useTrackManagement'
import { useTrainTransforms } from '../../hooks/useTrainTransforms'
@@ -80,7 +81,7 @@ interface SteamTrainJourneyProps {
trainPosition: number
pressure: number
elapsedTime: number
currentQuestion: { number: number; targetSum: number; correctAnswer: number } | null
currentQuestion: ComplementQuestion | null
currentInput: string
}

View File

@@ -49,6 +49,7 @@ describe('GameHUD', () => {
number: 3,
targetSum: 10,
correctAnswer: 7,
showAsAbacus: false,
},
currentInput: '7',
}

View File

@@ -15,9 +15,9 @@ import type { Passenger, Station } from '../../lib/gameTypes'
import { useSteamJourney } from '../useSteamJourney'
// Mock sound effects
jest.mock('../useSoundEffects', () => ({
vi.mock('../useSoundEffects', () => ({
useSoundEffects: () => ({
playSound: jest.fn(),
playSound: vi.fn(),
}),
}))
@@ -53,12 +53,12 @@ const _testStations: Station[] = [
describe('useSteamJourney - Passenger Boarding', () => {
beforeEach(() => {
jest.useFakeTimers()
vi.useFakeTimers()
})
afterEach(() => {
jest.runOnlyPendingTimers()
jest.useRealTimers()
vi.runOnlyPendingTimers()
vi.useRealTimers()
})
test('passenger boards when train reaches their origin station', () => {
@@ -106,7 +106,7 @@ describe('useSteamJourney - Passenger Boarding', () => {
// Advance timers to trigger the interval
act(() => {
jest.advanceTimersByTime(100)
vi.advanceTimersByTime(100)
})
// Verify passenger boarded
@@ -150,7 +150,7 @@ describe('useSteamJourney - Passenger Boarding', () => {
// Advance timers
act(() => {
jest.advanceTimersByTime(100)
vi.advanceTimersByTime(100)
})
// All three passengers should board (one per car)
@@ -190,7 +190,7 @@ describe('useSteamJourney - Passenger Boarding', () => {
pressure: 120,
elapsedTime: 1000 + pos * 50,
})
jest.advanceTimersByTime(50)
vi.advanceTimersByTime(50)
})
// Check if passenger boarded
@@ -239,7 +239,7 @@ describe('useSteamJourney - Passenger Boarding', () => {
})
act(() => {
jest.advanceTimersByTime(100)
vi.advanceTimersByTime(100)
})
// p2 should board (on car 1 since car 0 is occupied)
@@ -282,7 +282,7 @@ describe('useSteamJourney - Passenger Boarding', () => {
})
act(() => {
jest.advanceTimersByTime(100)
vi.advanceTimersByTime(100)
})
// Passenger should be delivered

View File

@@ -3,6 +3,7 @@ import * as nextNavigation from 'next/navigation'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useArcadeGuard } from '../useArcadeGuard'
import * as arcadeSocket from '../useArcadeSocket'
import * as viewerId from '../useViewerId'
// Mock Next.js navigation
vi.mock('next/navigation', () => ({
@@ -15,6 +16,11 @@ vi.mock('../useArcadeSocket', () => ({
useArcadeSocket: vi.fn(),
}))
// Mock useViewerId
vi.mock('../useViewerId', () => ({
useViewerId: vi.fn(),
}))
describe('useArcadeGuard', () => {
const mockRouter = {
push: vi.fn(),
@@ -36,6 +42,11 @@ describe('useArcadeGuard', () => {
vi.spyOn(nextNavigation, 'useRouter').mockReturnValue(mockRouter as any)
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade/matching')
vi.spyOn(arcadeSocket, 'useArcadeSocket').mockReturnValue(mockUseArcadeSocket)
vi.spyOn(viewerId, 'useViewerId').mockReturnValue({
data: 'test-user',
isLoading: false,
error: null,
} as any)
global.fetch = vi.fn()
})
@@ -45,11 +56,7 @@ describe('useArcadeGuard', () => {
status: 404,
})
const { result } = renderHook(() =>
useArcadeGuard({
userId: 'test-user',
})
)
const { result } = renderHook(() => useArcadeGuard())
expect(result.current.loading).toBe(true)
expect(result.current.hasActiveSession).toBe(false)
@@ -68,11 +75,7 @@ describe('useArcadeGuard', () => {
json: async () => mockSession,
})
const { result } = renderHook(() =>
useArcadeGuard({
userId: 'test-user',
})
)
const { result } = renderHook(() => useArcadeGuard())
await waitFor(() => {
expect(result.current.loading).toBe(false)
@@ -100,11 +103,7 @@ describe('useArcadeGuard', () => {
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade/matching')
renderHook(() =>
useArcadeGuard({
userId: 'test-user',
})
)
renderHook(() => useArcadeGuard())
await waitFor(() => {
expect(mockRouter.push).toHaveBeenCalledWith('/arcade/memory-quiz')
@@ -125,11 +124,7 @@ describe('useArcadeGuard', () => {
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade/matching')
renderHook(() =>
useArcadeGuard({
userId: 'test-user',
})
)
renderHook(() => useArcadeGuard())
await waitFor(() => {
expect(global.fetch).toHaveBeenCalled()
@@ -144,11 +139,7 @@ describe('useArcadeGuard', () => {
status: 404,
})
const { result } = renderHook(() =>
useArcadeGuard({
userId: 'test-user',
})
)
const { result } = renderHook(() => useArcadeGuard())
await waitFor(() => {
expect(result.current.loading).toBe(false)
@@ -173,12 +164,7 @@ describe('useArcadeGuard', () => {
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade/matching')
renderHook(() =>
useArcadeGuard({
userId: 'test-user',
onRedirect,
})
)
renderHook(() => useArcadeGuard({ onRedirect }))
await waitFor(() => {
expect(onRedirect).toHaveBeenCalledWith('/arcade/memory-quiz')
@@ -186,22 +172,19 @@ describe('useArcadeGuard', () => {
})
it('should not fetch session when disabled', () => {
renderHook(() =>
useArcadeGuard({
userId: 'test-user',
enabled: false,
})
)
renderHook(() => useArcadeGuard({ enabled: false }))
expect(global.fetch).not.toHaveBeenCalled()
})
it('should not fetch session when userId is null', () => {
renderHook(() =>
useArcadeGuard({
userId: null,
})
)
it('should not fetch session when viewerId is null', () => {
vi.spyOn(viewerId, 'useViewerId').mockReturnValue({
data: null,
isLoading: false,
error: null,
} as any)
renderHook(() => useArcadeGuard())
expect(global.fetch).not.toHaveBeenCalled()
})
@@ -212,11 +195,7 @@ describe('useArcadeGuard', () => {
status: 404,
})
renderHook(() =>
useArcadeGuard({
userId: 'test-user',
})
)
renderHook(() => useArcadeGuard())
await waitFor(() => {
expect(mockUseArcadeSocket.joinSession).toHaveBeenCalledWith('test-user')
@@ -227,7 +206,7 @@ describe('useArcadeGuard', () => {
let onSessionStateCallback: ((data: any) => void) | null = null
vi.spyOn(arcadeSocket, 'useArcadeSocket').mockImplementation((events) => {
onSessionStateCallback = events.onSessionState || null
onSessionStateCallback = events?.onSessionState || null
return mockUseArcadeSocket
})
@@ -236,11 +215,7 @@ describe('useArcadeGuard', () => {
status: 404,
})
const { result } = renderHook(() =>
useArcadeGuard({
userId: 'test-user',
})
)
const { result } = renderHook(() => useArcadeGuard())
await waitFor(() => {
expect(result.current.loading).toBe(false)
@@ -268,7 +243,7 @@ describe('useArcadeGuard', () => {
let onSessionEndedCallback: (() => void) | null = null
vi.spyOn(arcadeSocket, 'useArcadeSocket').mockImplementation((events) => {
onSessionEndedCallback = events.onSessionEnded || null
onSessionEndedCallback = events?.onSessionEnded || null
return mockUseArcadeSocket
})
@@ -283,11 +258,7 @@ describe('useArcadeGuard', () => {
json: async () => mockSession,
})
const { result } = renderHook(() =>
useArcadeGuard({
userId: 'test-user',
})
)
const { result } = renderHook(() => useArcadeGuard())
await waitFor(() => {
expect(result.current.hasActiveSession).toBe(true)
@@ -305,11 +276,7 @@ describe('useArcadeGuard', () => {
it('should handle fetch errors gracefully', async () => {
;(global.fetch as any).mockRejectedValue(new Error('Network error'))
const { result } = renderHook(() =>
useArcadeGuard({
userId: 'test-user',
})
)
const { result } = renderHook(() => useArcadeGuard())
await waitFor(() => {
expect(result.current.loading).toBe(false)

View File

@@ -110,6 +110,7 @@ export function useCreatePlayer() {
...newPlayer,
createdAt: new Date(),
isActive: newPlayer.isActive ?? false,
userId: 'temp-user', // Temporary userId, will be replaced by server response
}
queryClient.setQueryData<Player[]>(playerKeys.list(), [
...previousPlayers,

View File

@@ -45,12 +45,12 @@ describe('Arcade Session Integration', () => {
difficulty: 6,
turnTimer: 30,
gamePhase: 'setup',
currentPlayer: 1,
currentPlayer: "1",
matchedPairs: 0,
totalPairs: 6,
moves: 0,
scores: {},
activePlayers: [1],
activePlayers: ["1"],
consecutiveMatches: {},
gameStartTime: null,
gameEndTime: null,
@@ -67,7 +67,7 @@ describe('Arcade Session Integration', () => {
gameName: 'matching',
gameUrl: '/arcade/matching',
initialState,
activePlayers: [1],
activePlayers: ["1"],
})
expect(session).toBeDefined()
@@ -86,7 +86,7 @@ describe('Arcade Session Integration', () => {
playerId: testUserId,
timestamp: Date.now(),
data: {
activePlayers: [1],
activePlayers: ["1"],
},
}
@@ -147,12 +147,12 @@ describe('Arcade Session Integration', () => {
difficulty: 6,
turnTimer: 30,
gamePhase: 'playing',
currentPlayer: 1,
currentPlayer: "1",
matchedPairs: 0,
totalPairs: 6,
moves: 0,
scores: { 1: 0 },
activePlayers: [1],
activePlayers: ["1"],
consecutiveMatches: { 1: 0 },
gameStartTime: Date.now(),
gameEndTime: null,
@@ -169,7 +169,7 @@ describe('Arcade Session Integration', () => {
gameName: 'matching',
gameUrl: '/arcade/matching',
initialState: playingState,
activePlayers: [1],
activePlayers: ["1"],
})
// First move: flip card 1

View File

@@ -103,7 +103,7 @@ describe('session-manager', () => {
gameName: 'matching',
gameUrl: '/arcade/matching',
initialState: {},
activePlayers: [1],
activePlayers: ["1"],
})
// Verify user lookup by guestId
@@ -159,7 +159,7 @@ describe('session-manager', () => {
gameName: 'matching',
gameUrl: '/arcade/matching',
initialState: {},
activePlayers: [1],
activePlayers: ["1"],
})
// Verify user was created

View File

@@ -49,9 +49,9 @@ const ProgressiveTestComponent: React.FC<{
stepIndices.forEach((stepIndex, i) => {
const description = fullInstruction.multiStepInstructions?.[i] || `Step ${i + 1}`
const stepBeads = fullInstruction.stepBeadHighlights.filter(
const stepBeads = fullInstruction.stepBeadHighlights?.filter(
(bead) => bead.stepIndex === stepIndex
)
) || []
// Calculate the value change for this step by applying all bead movements
let valueChange = 0

View File

@@ -123,11 +123,11 @@ export function skillConfigurationToSkillSets(config: SkillConfiguration): {
}
if (mode === 'target') {
if (!target.basic) target.basic = {} as any
target.basic[skill as keyof typeof required.basic] = true
target.basic![skill as keyof typeof required.basic] = true
}
if (mode === 'forbidden') {
if (!forbidden.basic) forbidden.basic = {} as any
forbidden.basic[skill as keyof typeof required.basic] = true
forbidden.basic![skill as keyof typeof required.basic] = true
}
})
@@ -138,11 +138,11 @@ export function skillConfigurationToSkillSets(config: SkillConfiguration): {
}
if (mode === 'target') {
if (!target.fiveComplements) target.fiveComplements = {} as any
target.fiveComplements[skill as keyof typeof required.fiveComplements] = true
target.fiveComplements![skill as keyof typeof required.fiveComplements] = true
}
if (mode === 'forbidden') {
if (!forbidden.fiveComplements) forbidden.fiveComplements = {} as any
forbidden.fiveComplements[skill as keyof typeof required.fiveComplements] = true
forbidden.fiveComplements![skill as keyof typeof required.fiveComplements] = true
}
})
@@ -153,11 +153,11 @@ export function skillConfigurationToSkillSets(config: SkillConfiguration): {
}
if (mode === 'target') {
if (!target.tenComplements) target.tenComplements = {} as any
target.tenComplements[skill as keyof typeof required.tenComplements] = true
target.tenComplements![skill as keyof typeof required.tenComplements] = true
}
if (mode === 'forbidden') {
if (!forbidden.tenComplements) forbidden.tenComplements = {} as any
forbidden.tenComplements[skill as keyof typeof required.tenComplements] = true
forbidden.tenComplements![skill as keyof typeof required.tenComplements] = true
}
})

View File

@@ -8,7 +8,7 @@
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",

View File

@@ -1,6 +1,6 @@
{
"name": "soroban-monorepo",
"version": "2.3.0",
"version": "2.3.1",
"private": true,
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
"workspaces": [