feat: update tutorial functionality and improve tutorial step management
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m56s

This commit is contained in:
2026-08-01 01:57:24 -04:00
parent f54833e642
commit db42f81780
7 changed files with 142 additions and 44 deletions
@@ -1,17 +1,37 @@
import { describe, it, expect, beforeEach } from 'vitest'
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 = `
<div class="kebab-menu"></div>
<button data-tutorial="chore-edit-points kebab-edit-points-cost">Edit Points</button>
<button data-tutorial="chore-schedule">Schedule</button>
`
}
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', () => {
@@ -46,3 +66,103 @@ describe('tutorial controller - shouldShowStep', () => {
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()
})
})