import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { shouldShowStep, clearChainProgress, tutorialEnabled, tutorialProgress, sessionSkipped, maybeShow, dismissActive, activeStep, } from '../controller' function setKebabAnchorHtml() { document.body.innerHTML = `
` } describe('tutorial controller - shouldShowStep', () => { beforeEach(() => { tutorialEnabled.value = true tutorialProgress.value = {} sessionSkipped.value = false activeStep.value = null document.body.innerHTML = '' global.fetch = vi.fn().mockResolvedValue({ ok: true }) }) afterEach(() => { activeStep.value = null document.body.innerHTML = '' vi.restoreAllMocks() }) it('returns true when tips are enabled, the step is unseen, and the session is not skipped', () => { expect(shouldShowStep('create-chore')).toBe(true) }) it('returns false when tips are disabled for a normal step', () => { tutorialEnabled.value = false expect(shouldShowStep('create-penalty')).toBe(false) }) it('returns true for a locally-cleared step even when tips are disabled', () => { tutorialEnabled.value = false clearChainProgress('create-reward') expect(shouldShowStep('create-reward')).toBe(true) }) it('still blocks locally-cleared steps when the session has been skipped', () => { tutorialEnabled.value = false sessionSkipped.value = true clearChainProgress('create-kindness') expect(shouldShowStep('create-kindness')).toBe(false) }) it('returns false when the step has already been seen', () => { tutorialProgress.value = { 'notification-click': true } expect(shouldShowStep('notification-click')).toBe(false) }) it('ignores the enabled flag when ignoreEnabled is true', () => { tutorialEnabled.value = false expect(shouldShowStep('status-pending', true)).toBe(true) }) }) describe('tutorial controller - maybeShow', () => { beforeEach(() => { tutorialEnabled.value = true tutorialProgress.value = {} sessionSkipped.value = false activeStep.value = null setKebabAnchorHtml() global.fetch = vi.fn().mockResolvedValue({ ok: true }) }) afterEach(() => { activeStep.value = null document.body.innerHTML = '' vi.restoreAllMocks() }) it('promotes a step when the anchor is present', () => { maybeShow('chore-kebab-menu') expect(activeStep.value?.def.id).toBe('chore-kebab-menu') }) it('does not promote a step that has already been seen', () => { tutorialProgress.value = { 'chore-kebab-menu': true } maybeShow('chore-kebab-menu') expect(activeStep.value).toBeNull() }) it('is idempotent for the currently active step', () => { maybeShow('chore-kebab-menu') expect(activeStep.value?.def.id).toBe('chore-kebab-menu') maybeShow('chore-kebab-menu') expect(activeStep.value?.def.id).toBe('chore-kebab-menu') }) it('drops a step whose anchor is missing', () => { document.body.innerHTML = '' maybeShow('chore-kebab-menu') expect(activeStep.value).toBeNull() }) }) describe('tutorial controller - dismissActive', () => { beforeEach(() => { tutorialEnabled.value = true tutorialProgress.value = {} sessionSkipped.value = false activeStep.value = null setKebabAnchorHtml() global.fetch = vi.fn().mockResolvedValue({ ok: true }) }) afterEach(() => { activeStep.value = null document.body.innerHTML = '' vi.restoreAllMocks() }) it('marks the active step seen and chains the next step', () => { maybeShow('chore-kebab-menu') expect(activeStep.value?.def.id).toBe('chore-kebab-menu') dismissActive(true) expect(tutorialProgress.value['chore-kebab-menu']).toBe(true) expect(activeStep.value?.def.id).toBe('chore-edit-points') }) it('does not duplicate a chained step that is already queued', () => { // Set up: open the chore kebab menu, then queue the next step early. maybeShow('chore-kebab-menu') maybeShow('chore-edit-points') expect(activeStep.value?.def.id).toBe('chore-kebab-menu') // Dismissing the menu should chain to chore-edit-points only once. dismissActive(true) expect(activeStep.value?.def.id).toBe('chore-edit-points') // If a duplicate had been added, we would still be on chore-edit-points. dismissActive(true) expect(activeStep.value?.def.id).toBe('chore-schedule') }) it('does not re-chain a step that is currently active', () => { maybeShow('chore-kebab-menu') dismissActive(true) expect(activeStep.value?.def.id).toBe('chore-edit-points') // Dismissing chore-edit-points should move to chore-schedule, not loop. dismissActive(true) expect(activeStep.value?.def.id).toBe('chore-schedule') }) it('does not mark the step seen when markSeen is false', () => { maybeShow('chore-kebab-menu') dismissActive(false) expect(tutorialProgress.value['chore-kebab-menu']).toBeUndefined() expect(activeStep.value).toBeNull() }) })