diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..6ab1ef06 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(find:*)", + "Bash(cat:*)", + "Bash(yarn workspace:*)", + "Bash(git commit:*)", + "Bash(git add:*)" + ] + } +} diff --git a/AGENTS.md b/AGENTS.md index 4942f4df..07ad8496 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,16 @@ This project prefers a highly componentized React codebase that avoids duplicate - **Redux**: Compose selectors and helpers rather than copy/pasting logic. UI components should avoid data manipulation—use Redux selectors to transform and format data instead of doing it in components. - **Error Handling**: Keep error handling reasonable but not excessive. This is a small app - simple null checks and basic 404s are fine. Don't over-engineer with detailed error types for every edge case. - **Unit Tests**: Write unit tests for important service functions, especially those involving business logic or data transformations. -- **Testing**: After changes, run `yarn test:codex` from the repository root to ensure all tests pass. +- **Testing**: After changes, run `yarn test:codex` from the repository root to ensure all tests pass. Requires `dangerouslyDisableSandbox` (MongoMemoryServer binds to `0.0.0.0`). + +## Screenshot Tests (Remote) + +Screenshot tests run via GitHub Actions since they need Playwright with a browser. + +1. Run `./scripts/test-screenshots-remote.sh` (`dangerouslyDisableSandbox` required) — pushes a temp branch, triggers the `test.yml` workflow +2. Parse the branch name from stdout (format: `screenshot-test-`) +3. Poll for the run ID: `gh run list --workflow=test.yml --branch=$BRANCH --limit=1 --json databaseId,status` +4. Watch it: `gh run watch $RUN_ID` +5. Clean up: `git push origin --delete $BRANCH` Follow these guidelines to keep the codebase clean and maintainable. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/apps/react/src/components/FlashCardOptionsMenu.tsx b/apps/react/src/components/FlashCardOptionsMenu.tsx index 343ade7e..9106fd91 100644 --- a/apps/react/src/components/FlashCardOptionsMenu.tsx +++ b/apps/react/src/components/FlashCardOptionsMenu.tsx @@ -9,6 +9,7 @@ import { useIsCardOwner } from '../utils/useIsCardOwner'; import { useAppDispatch, useAppSelector } from 'MemoryFlashCore/src/redux/store'; import { updateHiddenCards } from 'MemoryFlashCore/src/redux/actions/update-hidden-cards-action'; import { deleteCard } from 'MemoryFlashCore/src/redux/actions/delete-card-action'; +import { recordAttempt } from 'MemoryFlashCore/src/redux/actions/record-attempt-action'; import { CardWithAttempts, selectHiddenCardIds, @@ -52,7 +53,10 @@ export const FlashCardOptionsMenu: React.FC = ({ if (canReveal) { items.unshift({ label: 'Reveal answer', - onClick: () => setShowAnswer(true), + onClick: () => { + dispatch(recordAttempt(false)); + setShowAnswer(true); + }, }); } diff --git a/apps/react/src/components/keyboard/KeyBoard.tsx b/apps/react/src/components/keyboard/KeyBoard.tsx index b9384e07..b5cfcfe2 100644 --- a/apps/react/src/components/keyboard/KeyBoard.tsx +++ b/apps/react/src/components/keyboard/KeyBoard.tsx @@ -7,8 +7,8 @@ import { WhiteKey } from './WhiteKey'; export const Keyboard = () => { return ( -
-
+
+
{/*
*/} {[1, 2, 3, 4, 5].map((octave) => ( diff --git a/apps/react/src/components/layout/Layout.tsx b/apps/react/src/components/layout/Layout.tsx index df94c586..136f3711 100644 --- a/apps/react/src/components/layout/Layout.tsx +++ b/apps/react/src/components/layout/Layout.tsx @@ -19,7 +19,10 @@ export const Layout: React.FC = ({ back, }) => { return ( -
+
= ({ tone, required, onToggle }) => ( className={`px-2 py-0.5 rounded text-xs font-medium transition-colors ${ required ? 'bg-blue-600 text-white' - : 'bg-gray-200 text-gray-600 border border-dashed border-gray-400' + : 'bg-gray-200 dark:bg-gray-600 text-gray-600 dark:text-gray-200 border border-dashed border-gray-400 dark:border-gray-500' }`} > {tone} @@ -32,7 +32,7 @@ export const ChordToneDisplay: React.FC = ({ chord, onTog return (
{chord.chordName}
{isInvalid ? ( diff --git a/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts b/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts index 4fcdb10b..17cb2bf1 100644 --- a/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts +++ b/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts @@ -78,7 +78,7 @@ test('Create custom deck with Chord Memory card, study it, then edit it', async await page.evaluate(() => { window.scrollTo(0, 0); - document.querySelector('.overflow-scroll')?.scrollTo(0, 600); + document.querySelector('.overflow-y-auto')?.scrollTo(0, 600); }); await expect(output).toHaveScreenshot('custom-deck-chord-memory-edit.png', screenshotOpts); diff --git a/apps/react/tests/custom-deck-notation-to-study.spec.ts b/apps/react/tests/custom-deck-notation-to-study.spec.ts index f7a44bea..c13161b7 100644 --- a/apps/react/tests/custom-deck-notation-to-study.spec.ts +++ b/apps/react/tests/custom-deck-notation-to-study.spec.ts @@ -94,7 +94,7 @@ test('Create custom deck, add notation and text cards, then study', async ({ await expect(page.locator('#text-prompt')).toHaveValue(promptText); await page.evaluate(() => { window.scrollTo(0, 0); - document.querySelector('.overflow-scroll')?.scrollTo(0, 600); + document.querySelector('.overflow-y-auto')?.scrollTo(0, 600); }); await expect(output).toHaveScreenshot( 'custom-deck-notation-to-study-notation-input-edit.png', diff --git a/apps/react/tests/helpers/visual.ts b/apps/react/tests/helpers/visual.ts index e33c29be..71d1e2e9 100644 --- a/apps/react/tests/helpers/visual.ts +++ b/apps/react/tests/helpers/visual.ts @@ -16,7 +16,7 @@ export async function setStaticScroll( await page.evaluate( ({ windowTop: winTop, overflowTop: overTop }) => { window.scrollTo(0, winTop); - document.querySelector('.overflow-scroll')?.scrollTo(0, overTop); + document.querySelector('.overflow-y-auto')?.scrollTo(0, overTop); }, { windowTop, overflowTop }, ); diff --git a/apps/react/tests/notation-input-e-major-g-sharp.spec.ts b/apps/react/tests/notation-input-e-major-g-sharp.spec.ts index d535210d..d43a7c5f 100644 --- a/apps/react/tests/notation-input-e-major-g-sharp.spec.ts +++ b/apps/react/tests/notation-input-e-major-g-sharp.spec.ts @@ -18,7 +18,7 @@ test('NotationInputScreen E major G# rendering', async ({ page }) => { // Wait a bit for rendering await page.evaluate(() => { window.scrollTo(0, 0); - document.querySelector('.overflow-scroll')?.scrollTo(0, 300); + document.querySelector('.overflow-y-auto')?.scrollTo(0, 300); }); await expect(output).toHaveScreenshot('notation-input-e-major-g-sharp.png', screenshotOpts); diff --git a/apps/react/tests/notation-input-text-prompt.spec.ts b/apps/react/tests/notation-input-text-prompt.spec.ts index 14abb4a7..64e301df 100644 --- a/apps/react/tests/notation-input-text-prompt.spec.ts +++ b/apps/react/tests/notation-input-text-prompt.spec.ts @@ -13,7 +13,7 @@ test('NotationInputScreen text prompt card type', async ({ page }) => { await page.waitForTimeout(200); await page.evaluate(() => { window.scrollTo(0, 0); - document.querySelector('.overflow-scroll')?.scrollTo(0, 300); + document.querySelector('.overflow-y-auto')?.scrollTo(0, 300); }); await expect(output).toHaveScreenshot('notation-input-text-prompt.png', screenshotOpts); diff --git a/package.json b/package.json index 6fb0a4a0..2c059177 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "concurrently \"yarn workspace MemoryFlashServer dev\" \"yarn workspace MemoryFlashReact dev\"", "test": "yarn workspace MemoryFlashServer test && yarn workspace MemoryFlashCore test && yarn workspace MemoryFlashReact test:screenshots", - "test:codex": "yarn workspace MemoryFlashServer build && yarn workspace MemoryFlashReact build && yarn workspace MemoryFlashServer test && yarn workspace MemoryFlashCore test" + "test:codex": "yarn workspace MemoryFlashServer build && yarn workspace MemoryFlashReact build && yarn workspace MemoryFlashServer test && yarn workspace MemoryFlashCore test", + "test:screenshots:remote": "./scripts/test-screenshots-remote.sh" }, "workspaces": [ "apps/*", diff --git a/scripts/test-screenshots-remote.sh b/scripts/test-screenshots-remote.sh new file mode 100755 index 00000000..942ccd4a --- /dev/null +++ b/scripts/test-screenshots-remote.sh @@ -0,0 +1,57 @@ +#!/bin/bash +set -e + +# Get current branch and create temp branch name +ORIGINAL_BRANCH=$(git rev-parse --abbrev-ref HEAD) +TEMP_BRANCH="screenshot-test-$(date +%s)" + +echo "Creating temporary branch: $TEMP_BRANCH" + +# Stash any changes (including untracked) +STASH_RESULT=$(git stash push -u -m "temp-screenshot-test" 2>&1) || true +HAS_STASH=false +if [[ "$STASH_RESULT" != *"No local changes"* ]]; then + HAS_STASH=true +fi + +# Create and switch to temp branch +git checkout -b "$TEMP_BRANCH" + +# Apply stashed changes if we had any +if [ "$HAS_STASH" = true ]; then + git stash pop +fi + +# Commit everything +git add -A +git commit -m "Screenshot test - temporary commit" --allow-empty + +# Push temp branch +git push origin "$TEMP_BRANCH" + +# Trigger workflow +echo "Triggering workflow on $TEMP_BRANCH..." +gh workflow run test.yml --ref "$TEMP_BRANCH" + +# Stash changes again before switching back (so we can restore them) +if [ "$HAS_STASH" = true ]; then + git stash push -u -m "restore-to-original" +fi + +# Switch back to original branch +git checkout "$ORIGINAL_BRANCH" + +# Restore uncommitted changes to original branch +if [ "$HAS_STASH" = true ]; then + git stash pop +fi + +# Delete local temp branch +git branch -D "$TEMP_BRANCH" + +echo "" +echo "Workflow triggered on branch: $TEMP_BRANCH" +echo "Run 'gh run watch' to monitor progress" +echo "" +echo "To clean up remote branch after tests complete:" +echo " git push origin --delete $TEMP_BRANCH"