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
2 changes: 2 additions & 0 deletions apps/react/src/components/notation/CardTypeOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ export const CardTypeOptions: React.FC<CardTypeOptionsProps> = ({ settings, onCh
{settings.cardType === 'Chord Memory' && (
<ChordProgressionInput
chordMemory={settings.chordMemory}
textPrompt={settings.textPrompt}
onChange={(chordMemory) => onChange({ chordMemory })}
onTextPromptChange={(textPrompt) => onChange({ textPrompt })}
/>
)}
</div>
Expand Down
12 changes: 6 additions & 6 deletions apps/react/src/components/notation/ChordProgressionInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChordProgressionInputProps> = ({
chordMemory,
textPrompt,
onChange,
onTextPromptChange,
}) => {
const handleProgressionChange = (progression: string) => {
onChange({ ...chordMemory, progression, chordTones: parseChordProgression(progression) });
Expand All @@ -22,10 +26,6 @@ export const ChordProgressionInput: React.FC<ChordProgressionInputProps> = ({
onChange(toggleChordTone(chordMemory, chordIndex, tone));
};

const handleTextPromptChange = (textPrompt: string) => {
onChange({ ...chordMemory, textPrompt });
};

return (
<div className="flex flex-col gap-3 w-full">
<InputField
Expand All @@ -39,8 +39,8 @@ export const ChordProgressionInput: React.FC<ChordProgressionInputProps> = ({
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 && (
<div className="flex flex-wrap gap-2">
Expand Down
2 changes: 0 additions & 2 deletions apps/react/src/components/notation/defaultSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { ChordMemoryChord } from 'MemoryFlashCore/src/types/Cards';
export interface ChordMemorySettings {
progression: string;
chordTones: ChordMemoryChord[];
textPrompt: string;
}

export interface NotationSettingsState {
Expand All @@ -32,6 +31,5 @@ export const defaultSettings: NotationSettingsState = {
chordMemory: {
progression: '',
chordTones: [],
textPrompt: '',
},
};
21 changes: 12 additions & 9 deletions apps/react/src/screens/NotationInputScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 }],
Expand Down Expand Up @@ -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}
/>
<div className="w-full max-w-xs">
Expand Down
105 changes: 105 additions & 0 deletions apps/react/tests/custom-deck-chord-memory-to-study.spec.ts
Original file line number Diff line number Diff line change
@@ -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' });
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? '' }],
Expand Down