fix: correct tutorial highlighting placeValue to columnIndex conversion

Fixed bug where tutorial highlighting appeared in wrong column:
- placeValue 0 (ones place) now correctly maps to columnIndex 4 (rightmost)
- placeValue 1 (tens place) now correctly maps to columnIndex 3
- Added comprehensive test suite to catch highlighting regressions

Tutorial steps now highlight beads in the correct columns.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-09-21 18:00:17 -05:00
parent ab99053d74
commit 35257b8873
3 changed files with 183 additions and 24 deletions

View File

@@ -374,26 +374,26 @@ export function TutorialPlayer({
return {
beads: currentStep.highlightBeads.reduce((acc, highlight) => {
// Convert columnIndex to placeValue for compatibility
const placeValue = highlight.placeValue ?? (4 - highlight.columnIndex);
// Convert placeValue to columnIndex for AbacusReact compatibility
const columnIndex = highlight.placeValue !== undefined ? (4 - highlight.placeValue) : highlight.columnIndex;
// Initialize column if it doesn't exist
if (!acc[placeValue]) {
acc[placeValue] = {};
if (!acc[columnIndex]) {
acc[columnIndex] = {};
}
// Add the bead style to the appropriate type
if (highlight.beadType === 'earth' && highlight.position !== undefined) {
if (!acc[placeValue].earth) {
acc[placeValue].earth = {};
if (!acc[columnIndex].earth) {
acc[columnIndex].earth = {};
}
acc[placeValue].earth[highlight.position] = {
acc[columnIndex].earth[highlight.position] = {
fill: '#fbbf24',
stroke: '#f59e0b',
strokeWidth: 3
};
} else {
acc[placeValue][highlight.beadType] = {
acc[columnIndex][highlight.beadType] = {
fill: '#fbbf24',
stroke: '#f59e0b',
strokeWidth: 3

View File

@@ -0,0 +1,163 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import { TutorialPlayer } from '../TutorialPlayer'
import { Tutorial } from '../../../types/tutorial'
// Mock AbacusReact to capture the customStyles prop
vi.mock('@soroban/abacus-react', () => ({
AbacusReact: vi.fn(({ customStyles }) => {
// Store the customStyles for testing
(global as any).lastCustomStyles = customStyles
return <div data-testid="mock-abacus" />
})
}))
describe('TutorialPlayer Highlighting', () => {
const mockTutorial: Tutorial = {
id: 'test-tutorial',
title: 'Test Tutorial',
description: 'Test tutorial for highlighting',
category: 'Test',
difficulty: 'beginner',
estimatedDuration: 5,
steps: [
{
id: 'step-1',
title: 'Test Step 1',
problem: '0 + 1',
description: 'Add 1 to the abacus',
startValue: 0,
targetValue: 1,
highlightBeads: [{ placeValue: 0, beadType: 'earth', position: 0 }],
expectedAction: 'add',
actionDescription: 'Click the first earth bead',
tooltip: {
content: 'Adding earth beads',
explanation: 'Earth beads are worth 1 each'
},
errorMessages: {
wrongBead: 'Click the highlighted earth bead',
wrongAction: 'Move the bead UP',
hint: 'Earth beads move up when adding'
}
},
{
id: 'step-2',
title: 'Multi-bead Step',
problem: '3 + 4',
description: 'Use complement: 4 = 5 - 1',
startValue: 3,
targetValue: 7,
highlightBeads: [
{ placeValue: 0, beadType: 'heaven' },
{ placeValue: 0, beadType: 'earth', position: 0 }
],
expectedAction: 'multi-step',
actionDescription: 'Add heaven bead, then remove earth bead',
tooltip: {
content: 'Five Complement',
explanation: '4 = 5 - 1'
},
errorMessages: {
wrongBead: 'Follow the two-step process',
wrongAction: 'Add heaven, then remove earth',
hint: 'Complement thinking: 4 = 5 - 1'
}
}
],
tags: ['test'],
author: 'Test Author',
version: '1.0.0',
createdAt: new Date(),
updatedAt: new Date(),
isPublished: true
}
it('should highlight single bead in correct column (ones place)', () => {
render(<TutorialPlayer tutorial={mockTutorial} />)
const customStyles = (global as any).lastCustomStyles
// placeValue: 0 (ones place) should map to columnIndex: 4 in customStyles
expect(customStyles).toBeDefined()
expect(customStyles.beads).toBeDefined()
expect(customStyles.beads[4]).toBeDefined() // columnIndex 4 = rightmost column = ones place
expect(customStyles.beads[4].earth).toBeDefined()
expect(customStyles.beads[4].earth[0]).toEqual({
fill: '#fbbf24',
stroke: '#f59e0b',
strokeWidth: 3
})
})
it('should highlight multiple beads in same column for complement operations', () => {
// Advance to step 2 (multi-bead highlighting)
render(<TutorialPlayer tutorial={mockTutorial} initialStepIndex={1} />)
const customStyles = (global as any).lastCustomStyles
// Both heaven and earth beads should be highlighted in ones place (columnIndex 4)
expect(customStyles).toBeDefined()
expect(customStyles.beads).toBeDefined()
expect(customStyles.beads[4]).toBeDefined() // columnIndex 4 = rightmost column = ones place
// Heaven bead should be highlighted
expect(customStyles.beads[4].heaven).toEqual({
fill: '#fbbf24',
stroke: '#f59e0b',
strokeWidth: 3
})
// Earth bead position 0 should be highlighted
expect(customStyles.beads[4].earth).toBeDefined()
expect(customStyles.beads[4].earth[0]).toEqual({
fill: '#fbbf24',
stroke: '#f59e0b',
strokeWidth: 3
})
})
it('should not highlight leftmost column when highlighting ones place', () => {
render(<TutorialPlayer tutorial={mockTutorial} />)
const customStyles = (global as any).lastCustomStyles
// columnIndex 0 (leftmost) should NOT be highlighted for ones place operations
expect(customStyles.beads[0]).toBeUndefined()
expect(customStyles.beads[1]).toBeUndefined()
expect(customStyles.beads[2]).toBeUndefined()
expect(customStyles.beads[3]).toBeUndefined()
// Only columnIndex 4 (rightmost = ones place) should be highlighted
expect(customStyles.beads[4]).toBeDefined()
})
it('should convert placeValue to columnIndex correctly', () => {
const testCases = [
{ placeValue: 0, expectedColumnIndex: 4 }, // ones place
{ placeValue: 1, expectedColumnIndex: 3 }, // tens place
{ placeValue: 2, expectedColumnIndex: 2 }, // hundreds place
{ placeValue: 3, expectedColumnIndex: 1 }, // thousands place
{ placeValue: 4, expectedColumnIndex: 0 } // ten-thousands place
]
testCases.forEach(({ placeValue, expectedColumnIndex }) => {
const testTutorial = {
...mockTutorial,
steps: [{
...mockTutorial.steps[0],
highlightBeads: [{ placeValue, beadType: 'earth' as const, position: 0 }]
}]
}
render(<TutorialPlayer tutorial={testTutorial} />)
const customStyles = (global as any).lastCustomStyles
expect(customStyles.beads[expectedColumnIndex]).toBeDefined()
// Cleanup for next iteration
delete (global as any).lastCustomStyles
})
})
})

View File

@@ -19,6 +19,9 @@
.text_blue\.700 {
color: var(--colors-blue-700)
}
.hover\:bg_blue\.50:is(:hover, [data-hover]) {
background: var(--colors-blue-50)
}
.bg_gray\.100 {
background: var(--colors-gray-100)
@@ -63,6 +66,9 @@
.bg_blue\.50 {
background: var(--colors-blue-50)
}
.hover\:bg_blue\.100:is(:hover, [data-hover]) {
background: var(--colors-blue-100)
}
.p_3 {
padding: var(--spacing-3)
@@ -183,6 +189,9 @@
.opacity_0\.5 {
opacity: 0.5
}
.hover\:bg_gray\.50:is(:hover, [data-hover]) {
background: var(--colors-gray-50)
}
.border_blue\.300 {
border-color: var(--colors-blue-300)
@@ -215,6 +224,9 @@
.cursor_not-allowed {
cursor: not-allowed
}
.hover\:bg_blue\.600:is(:hover, [data-hover]) {
background: var(--colors-blue-600)
}
.px_4 {
padding-inline: var(--spacing-4)
@@ -367,20 +379,4 @@
.flex_column {
flex-direction: column
}
.hover\:bg_blue\.50:is(:hover, [data-hover]) {
background: var(--colors-blue-50)
}
.hover\:bg_blue\.100:is(:hover, [data-hover]) {
background: var(--colors-blue-100)
}
.hover\:bg_gray\.50:is(:hover, [data-hover]) {
background: var(--colors-gray-50)
}
.hover\:bg_blue\.600:is(:hover, [data-hover]) {
background: var(--colors-blue-600)
}
}