feat: update tutorial functionality and improve tutorial step management
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m56s
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m56s
This commit is contained in:
@@ -2,20 +2,20 @@
|
||||
"cookies": [
|
||||
{
|
||||
"name": "refresh_token",
|
||||
"value": "5gEK_iLdGl1BAGfetT8hEf6SJK2lQVbYxQjH0BdKWwU",
|
||||
"value": "WGHWfbkdu5a4GEsBQ4exR8DG4W39iOm1b71weDg6RWU",
|
||||
"domain": "localhost",
|
||||
"path": "/api/auth",
|
||||
"expires": 1792813397.168935,
|
||||
"expires": 1793338810.330124,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Strict"
|
||||
},
|
||||
{
|
||||
"name": "access_token",
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS10dXRvcmlhbEB0ZXN0LmNvbSIsInVzZXJfaWQiOiJkODk0ZjA0OS0xODU3LTQwM2UtYjgzZC1jMTY2NTNmZDU2MmEiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODUwNDgxOTd9.S7frWOrZwXC5xVgtG2RDioXGok04qK_jLCYWvfedD7s",
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS10dXRvcmlhbEB0ZXN0LmNvbSIsInVzZXJfaWQiOiJlMzIxNzNmNy1lODM5LTQxNjMtOGUyYS04YmRhZGY5ZTZiZjEiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODU1NzM2MTB9.W08NeVvTAftfz4VQVopd8U38mOEiqUW9qb-EhbttWJk",
|
||||
"domain": "localhost",
|
||||
"path": "/",
|
||||
"expires": 1785048197.167997,
|
||||
"expires": 1785573610.329316,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
@@ -27,11 +27,11 @@
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "authSyncEvent",
|
||||
"value": "{\"type\":\"logout\",\"at\":1785037396813}"
|
||||
"value": "{\"type\":\"logout\",\"at\":1785562810109}"
|
||||
},
|
||||
{
|
||||
"name": "parentAuth",
|
||||
"value": "{\"expiresAt\":1785210197388}"
|
||||
"value": "{\"expiresAt\":1785735610529}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
"cookies": [
|
||||
{
|
||||
"name": "refresh_token",
|
||||
"value": "Ke6qDj_0RYyrMSI1Z9C69DPtvYo26oUUiLZr_pDXZv4",
|
||||
"value": "nT_T0nZ7ZXthfVaOIvMOa-9Z8IJAkT8hByHcv9yxyno",
|
||||
"domain": "localhost",
|
||||
"path": "/api/auth",
|
||||
"expires": 1792813392.716776,
|
||||
"expires": 1793338807.10011,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Strict"
|
||||
},
|
||||
{
|
||||
"name": "access_token",
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiI3NTYwZDBlNy1iMmVlLTRiMjktYTYwNi1lZTM3NmIzN2Y3ODciLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODUwNDgxOTJ9.QB4bXo88KC7jr94QSjJI3Y1eVWeU4KNKdHRi1_7zcD0",
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiJkZmQ4ZjI1NS0yMzkwLTRiOTgtODJmYi1jZGRmOTBjODYwYWMiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODU1NzM2MDd9.Wn8RdLZGFifPe3LwAjljmHIoiFOOIzqeDCfQWvvy-_I",
|
||||
"domain": "localhost",
|
||||
"path": "/",
|
||||
"expires": 1785048192.71579,
|
||||
"expires": 1785573607.099308,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
@@ -27,11 +27,11 @@
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "authSyncEvent",
|
||||
"value": "{\"type\":\"logout\",\"at\":1785037392502}"
|
||||
"value": "{\"type\":\"logout\",\"at\":1785562806880}"
|
||||
},
|
||||
{
|
||||
"name": "parentAuth",
|
||||
"value": "{\"expiresAt\":1785210192904}"
|
||||
"value": "{\"expiresAt\":1785735607328}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -445,16 +445,8 @@ function openRoutineMenu(routineId: string, e: MouseEvent) {
|
||||
'routine-kebab-menu',
|
||||
() => document.querySelector('.kebab-menu') as HTMLElement | null,
|
||||
)
|
||||
tutorialMaybeShow(
|
||||
'kebab-edit-points-cost',
|
||||
() => document.querySelector('[data-tutorial~="kebab-edit-points-cost"]') as HTMLElement | null,
|
||||
)
|
||||
tutorialMaybeShow(
|
||||
'routine-schedule',
|
||||
() => document.querySelector('[data-tutorial="routine-schedule"]') as HTMLElement | null,
|
||||
)
|
||||
})
|
||||
const items = childRoutineListRef.value?.items ?? []
|
||||
const items: ChildRoutine[] = childRoutineListRef.value?.items ?? []
|
||||
const routine = items.find((r) => r.id === routineId)
|
||||
if (routine) {
|
||||
if (isRoutineExpired(routine)) {
|
||||
@@ -740,14 +732,6 @@ function openChoreMenu(taskId: string, e: MouseEvent) {
|
||||
'chore-kebab-menu',
|
||||
() => document.querySelector('.kebab-menu') as HTMLElement | null,
|
||||
)
|
||||
tutorialMaybeShow(
|
||||
'kebab-edit-points-cost',
|
||||
() => document.querySelector('[data-tutorial~="kebab-edit-points-cost"]') as HTMLElement | null,
|
||||
)
|
||||
tutorialMaybeShow(
|
||||
'chore-schedule',
|
||||
() => document.querySelector('[data-tutorial="chore-schedule"]') as HTMLElement | null,
|
||||
)
|
||||
})
|
||||
const items: ChildTask[] = childChoreListRef.value?.items ?? []
|
||||
const task = items.find((t) => t.id === taskId)
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -147,7 +147,7 @@ export function dismissActive(markSeen = true) {
|
||||
}
|
||||
if (current.def.next) {
|
||||
const nextDef = stepRegistry[current.def.next]
|
||||
if (nextDef && shouldShowStep(nextDef.id)) {
|
||||
if (nextDef && shouldShowStep(nextDef.id) && !queue.some((q) => q.def.id === nextDef.id)) {
|
||||
queue.unshift({ def: nextDef, anchor: null })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user