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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user