diff --git a/apps/react/src/components/notation/CardTypeOptions.tsx b/apps/react/src/components/notation/CardTypeOptions.tsx index 579e690..95870a1 100644 --- a/apps/react/src/components/notation/CardTypeOptions.tsx +++ b/apps/react/src/components/notation/CardTypeOptions.tsx @@ -39,7 +39,9 @@ export const CardTypeOptions: React.FC = ({ settings, onCh {settings.cardType === 'Chord Memory' && ( onChange({ chordMemory })} + onTextPromptChange={(textPrompt) => onChange({ textPrompt })} /> )} diff --git a/apps/react/src/components/notation/ChordProgressionInput.tsx b/apps/react/src/components/notation/ChordProgressionInput.tsx index dbb423b..3110e45 100644 --- a/apps/react/src/components/notation/ChordProgressionInput.tsx +++ b/apps/react/src/components/notation/ChordProgressionInput.tsx @@ -7,12 +7,16 @@ import { toggleChordTone } from './toggleChordTone'; interface ChordProgressionInputProps { chordMemory: ChordMemorySettings; + textPrompt: string; onChange: (chordMemory: ChordMemorySettings) => void; + onTextPromptChange: (textPrompt: string) => void; } export const ChordProgressionInput: React.FC = ({ chordMemory, + textPrompt, onChange, + onTextPromptChange, }) => { const handleProgressionChange = (progression: string) => { onChange({ ...chordMemory, progression, chordTones: parseChordProgression(progression) }); @@ -22,10 +26,6 @@ export const ChordProgressionInput: React.FC = ({ onChange(toggleChordTone(chordMemory, chordIndex, tone)); }; - const handleTextPromptChange = (textPrompt: string) => { - onChange({ ...chordMemory, textPrompt }); - }; - return (
= ({ id="chord-text-prompt" label="Text Prompt (optional)" placeholder="e.g., Autumn Leaves - Verse" - value={chordMemory.textPrompt} - onChange={(e) => handleTextPromptChange(e.target.value)} + value={textPrompt} + onChange={(e) => onTextPromptChange(e.target.value)} /> {chordMemory.chordTones.length > 0 && (
diff --git a/apps/react/src/components/notation/defaultSettings.ts b/apps/react/src/components/notation/defaultSettings.ts index a1281a0..97112cc 100644 --- a/apps/react/src/components/notation/defaultSettings.ts +++ b/apps/react/src/components/notation/defaultSettings.ts @@ -5,7 +5,6 @@ import { ChordMemoryChord } from 'MemoryFlashCore/src/types/Cards'; export interface ChordMemorySettings { progression: string; chordTones: ChordMemoryChord[]; - textPrompt: string; } export interface NotationSettingsState { @@ -32,6 +31,5 @@ export const defaultSettings: NotationSettingsState = { chordMemory: { progression: '', chordTones: [], - textPrompt: '', }, }; diff --git a/apps/react/src/screens/NotationInputScreen.tsx b/apps/react/src/screens/NotationInputScreen.tsx index a5bb5c4..342fc5e 100644 --- a/apps/react/src/screens/NotationInputScreen.tsx +++ b/apps/react/src/screens/NotationInputScreen.tsx @@ -7,7 +7,7 @@ import { majorKeys } from 'MemoryFlashCore/src/lib/notes'; import { addCardsToDeck } from 'MemoryFlashCore/src/redux/actions/add-cards-to-deck'; import { updateCard } from 'MemoryFlashCore/src/redux/actions/update-card-action'; import { setPresentationMode } from 'MemoryFlashCore/src/redux/actions/set-presentation-mode'; -import { AnswerType, CardTypeEnum } from 'MemoryFlashCore/src/types/Cards'; +import { AnswerType, CardTypeEnum, ChordMemoryAnswer } from 'MemoryFlashCore/src/types/Cards'; import { useDeckIdPath } from './useDeckIdPath'; import { useNetworkState } from 'MemoryFlashCore/src/redux/selectors/useNetworkState'; import { useParams } from 'react-router-dom'; @@ -40,14 +40,22 @@ export const NotationInputScreen = () => { if (card && card.type === CardTypeEnum.MultiSheet) { const text = card.question.presentationModes?.find((p) => p.id === 'Text Prompt'); const idx = majorKeys.indexOf(card.question.key); + const isChordMemory = card.answer.type === AnswerType.ChordMemory; + const chordMemoryAnswer = isChordMemory ? (card.answer as ChordMemoryAnswer) : null; setSettings((prev) => ({ ...prev, keySig: card.question.key, selected: majorKeys.map((_, i) => i === idx), - cardType: text ? 'Text Prompt' : 'Sheet Music', + cardType: isChordMemory ? 'Chord Memory' : text ? 'Text Prompt' : 'Sheet Music', textPrompt: text?.text || '', // If editing a Text Prompt card, default to previewing the text prompt preview: !!text, + chordMemory: chordMemoryAnswer + ? { + progression: chordMemoryAnswer.chords.map((c) => c.chordName).join(' '), + chordTones: chordMemoryAnswer.chords, + } + : prev.chordMemory, })); setQuestion(card.question); setComplete(true); @@ -79,7 +87,7 @@ export const NotationInputScreen = () => { dispatch(setPresentationMode(CardTypeEnum.MultiSheet, 'Text Prompt')); dispatch(addCardsToDeck(deckId, toAdd)); } else if (settings.cardType === 'Chord Memory') { - const text = settings.chordMemory.textPrompt || settings.chordMemory.progression; + const text = settings.textPrompt || settings.chordMemory.progression; toAdd = previews.map((q) => ({ ...q, presentationModes: [{ id: 'Text Prompt', text }], @@ -130,12 +138,7 @@ export const NotationInputScreen = () => { keySig={settings.keySig} previews={previews} cardType={settings.cardType} - textPrompt={ - settings.cardType === 'Chord Memory' - ? settings.chordMemory.textPrompt || - settings.chordMemory.progression - : settings.textPrompt - } + textPrompt={settings.textPrompt} previewTextCard={settings.preview} />
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 new file mode 100644 index 0000000..4fcdb10 --- /dev/null +++ b/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts @@ -0,0 +1,105 @@ +import { + test, + expect, + screenshotOpts, + uiLogin, + seedTestData, + initDeterministicEnv, + runRecorderEvents, + createCourse, + createDeck, +} from './helpers'; + +// Cm7 chord (C, Eb, G, Bb) +const cm7Notes = [60, 63, 67, 70]; // C4, Eb4, G4, Bb4 + +test('Create custom deck with Chord Memory card, study it, then edit it', async ({ + page, + getButton, + clickButton, +}) => { + await initDeterministicEnv(page); + await seedTestData(page); + await uiLogin(page, 't@example.com', 'Testing123!'); + + const courseId = await createCourse(page, 'Chord Memory Test Course'); + const deckId = await createDeck(page, courseId, 'Chord Memory Deck'); + await page.waitForURL(new RegExp(`/study/${deckId}/notation`)); + + // Select Chord Memory card type + await page.locator('button:has-text("Sheet Music")').click(); + await page.getByRole('menuitem', { name: 'Chord Memory' }).click(); + + // Enter chord progression + await page.fill('#chord-progression', 'Cm7'); + await page.fill('#chord-text-prompt', 'C Minor 7 Practice'); + + const [addResp] = await Promise.all([ + page.waitForResponse( + (r) => r.url().includes(`/decks/${deckId}/cards`) && r.request().method() === 'POST', + ), + clickButton('Add Card'), + ]); + expect(addResp.ok()).toBeTruthy(); + + // Navigate to study screen + await page.goto(`/study/${deckId}`); + const output = page.locator('#root'); + await output.waitFor(); + await expect(output).toHaveScreenshot( + 'custom-deck-chord-memory-to-study-question.png', + screenshotOpts, + ); + + // Open list view and ensure chord memory card previews correctly + await page.click('a[href="list"], a[href$="/list"]'); + await page.waitForURL(new RegExp(`/study/${deckId}/list`)); + await expect(page.locator('.card-container')).toHaveCount(1); + await page.getByText('C Minor 7 Practice', { exact: true }).waitFor(); + await expect(output).toHaveScreenshot( + 'custom-deck-chord-memory-to-study-list.png', + screenshotOpts, + ); + + // Edit chord memory card and ensure it loads with Chord Memory type (not Text Prompt) + const chordCard = page.locator('.card-container', { hasText: 'C Minor 7 Practice' }); + await chordCard.getByRole('button', { name: 'Card options' }).click(); + await page.getByRole('menuitem', { name: 'Edit card' }).click(); + await page.waitForURL(new RegExp(`/study/${deckId}/edit/`)); + + // Verify that "Chord Memory" is shown, not "Text Prompt" + await expect(await getButton('Chord Memory')).toBeVisible(); + await expect(page.locator('#chord-progression')).toHaveValue('Cm7'); + await expect(page.locator('#chord-text-prompt')).toHaveValue('C Minor 7 Practice'); + + // Update the card title and save + await page.fill('#chord-text-prompt', 'Updated Cm7 Practice'); + await expect(page.locator('#chord-text-prompt')).toHaveValue('Updated Cm7 Practice'); + + await page.evaluate(() => { + window.scrollTo(0, 0); + document.querySelector('.overflow-scroll')?.scrollTo(0, 600); + }); + await expect(output).toHaveScreenshot('custom-deck-chord-memory-edit.png', screenshotOpts); + + const [updateResp] = await Promise.all([ + page.waitForResponse( + (r) => r.url().includes('/cards/') && r.request().method() === 'PATCH', + ), + clickButton('Update Card'), + ]); + expect(updateResp.ok()).toBeTruthy(); + + // Go back to study screen and verify the updated title is shown + await page.goto(`/study/${deckId}`); + await page.locator('.card-container').first().waitFor(); + await expect(output).toHaveScreenshot( + 'custom-deck-chord-memory-updated-question.png', + screenshotOpts, + ); + + await runRecorderEvents(page, undefined, [cm7Notes], 'custom-deck-chord-memory-answer-step'); + + // Cleanup any remaining routes to avoid teardown errors + await page.unrouteAll({ behavior: 'ignoreErrors' }); +}); diff --git a/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts-snapshots/custom-deck-chord-memory-answer-step-1.png b/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts-snapshots/custom-deck-chord-memory-answer-step-1.png new file mode 100644 index 0000000..547bc74 Binary files /dev/null and b/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts-snapshots/custom-deck-chord-memory-answer-step-1.png differ diff --git a/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts-snapshots/custom-deck-chord-memory-edit.png b/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts-snapshots/custom-deck-chord-memory-edit.png new file mode 100644 index 0000000..245b8ce Binary files /dev/null and b/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts-snapshots/custom-deck-chord-memory-edit.png differ diff --git a/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts-snapshots/custom-deck-chord-memory-to-study-list.png b/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts-snapshots/custom-deck-chord-memory-to-study-list.png new file mode 100644 index 0000000..38b2151 Binary files /dev/null and b/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts-snapshots/custom-deck-chord-memory-to-study-list.png differ diff --git a/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts-snapshots/custom-deck-chord-memory-to-study-question.png b/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts-snapshots/custom-deck-chord-memory-to-study-question.png new file mode 100644 index 0000000..80e16fa Binary files /dev/null and b/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts-snapshots/custom-deck-chord-memory-to-study-question.png differ diff --git a/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts-snapshots/custom-deck-chord-memory-updated-question.png b/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts-snapshots/custom-deck-chord-memory-updated-question.png new file mode 100644 index 0000000..2a9082b Binary files /dev/null and b/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts-snapshots/custom-deck-chord-memory-updated-question.png differ diff --git a/packages/MemoryFlashCore/src/redux/actions/update-card-action.ts b/packages/MemoryFlashCore/src/redux/actions/update-card-action.ts index 06f6015..5ba2ffc 100644 --- a/packages/MemoryFlashCore/src/redux/actions/update-card-action.ts +++ b/packages/MemoryFlashCore/src/redux/actions/update-card-action.ts @@ -9,7 +9,7 @@ function prepareQuestion( cardType: string, textPrompt?: string, ): MultiSheetQuestion { - if (cardType === 'Text Prompt') { + if (cardType === 'Text Prompt' || cardType === 'Chord Memory') { return { ...question, presentationModes: [{ id: 'Text Prompt', text: textPrompt ?? '' }],