From 8e4f89f4ec5f5f72a8272b586ecf7b779401823c Mon Sep 17 00:00:00 2001 From: Ryan Kegel Date: Sat, 25 Jul 2026 12:56:18 -0400 Subject: [PATCH] feat(tutorial): update tutorial labels and modal titles to improve clarity - Changed "Skip tour" button to "Cancel" in various tutorial overlays. - Updated modal titles from "Confirm Task" to "Confirm Penalty" where applicable. - Modified tutorial step call-to-action labels from "Got it" and "Next" to "Continue" for consistency. - Added tests for new tutorial overlay behavior and button interactions. - Refactored tutorial controller to allow ignoring tutorial enabled state in step visibility checks. - Enhanced user profile tutorial restart functionality with appropriate modal messages. - Updated user profile and task confirmation dialog components to reflect new titles and messages. - Adjusted configuration files for local development environments. --- frontend/e2e/.auth/user-cc.json | 12 +- frontend/e2e/.auth/user-tutorial.json | 12 +- frontend/e2e/.auth/user.json | 12 +- .../create-child/validation.spec.ts | 2 +- .../profile-button-temporary.spec.ts | 2 +- .../profile-button/profile-button.spec.ts | 2 +- .../tutorial/dialog-help-button.spec.ts | 2 +- .../e2e/mode_parent/tutorial/tutorial.spec.ts | 2 +- frontend/src/__tests__/UserProfile.spec.ts | 76 ++++++++++-- .../components/child/TaskConfirmDialog.vue | 8 +- .../child/__tests__/TaskConfirmDialog.spec.ts | 16 +++ .../src/components/profile/UserProfile.vue | 10 +- frontend/src/tutorial/HelpButton.vue | 5 +- frontend/src/tutorial/TutorialOverlay.vue | 12 +- .../src/tutorial/__tests__/HelpButton.spec.ts | 6 + .../__tests__/TutorialOverlay.spec.ts | 91 +++++++++++++++ frontend/src/tutorial/controller.ts | 12 +- frontend/src/tutorial/steps.ts | 108 +++++++++--------- opencode.json | 19 ++- opencode.mac.json | 24 ++++ opencode.windows.json | 19 +++ setup-mcp.py | 13 +++ 22 files changed, 347 insertions(+), 118 deletions(-) create mode 100644 frontend/src/tutorial/__tests__/TutorialOverlay.spec.ts create mode 100644 opencode.mac.json create mode 100644 opencode.windows.json create mode 100644 setup-mcp.py diff --git a/frontend/e2e/.auth/user-cc.json b/frontend/e2e/.auth/user-cc.json index d74372e..b8f70d7 100644 --- a/frontend/e2e/.auth/user-cc.json +++ b/frontend/e2e/.auth/user-cc.json @@ -2,20 +2,20 @@ "cookies": [ { "name": "refresh_token", - "value": "qYJH5LMH5O4UK92tdAp7R49rzGACFvTMVj29S-Pzygc", + "value": "hW7OJbUjHr4XntQRPtefKNhvvctK014R6pzLz_vyJ8g", "domain": "localhost", "path": "/api/auth", - "expires": 1792435594.088715, + "expires": 1792774142.289068, "httpOnly": true, "secure": true, "sameSite": "Strict" }, { "name": "access_token", - "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1jY0B0ZXN0LmNvbSIsInVzZXJfaWQiOiI2NmNlMzgzMy0zMGQ3LTRjMTItYjRhMi1iNmIxMjcxYTI1YzEiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODQ2NzAzOTR9.jkyiFTxBQ0oatY6jCAWFG88E8e6mCrDDlN_8I56RMBA", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1jY0B0ZXN0LmNvbSIsInVzZXJfaWQiOiI5MjUwNzY1MS00YjlmLTRkMTMtOTgxYS1hMzA2ZThjY2JmNzciLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODUwMDg5NDJ9.15hDvKf9u95pMzyeSbrHesFBDGgTwnxV1FD175p72-w", "domain": "localhost", "path": "/", - "expires": 1784670394.088677, + "expires": 1785008942.288088, "httpOnly": true, "secure": true, "sameSite": "Lax" @@ -27,11 +27,11 @@ "localStorage": [ { "name": "authSyncEvent", - "value": "{\"type\":\"logout\",\"at\":1784659593888}" + "value": "{\"type\":\"logout\",\"at\":1784998141942}" }, { "name": "parentAuth", - "value": "{\"expiresAt\":1784832394248}" + "value": "{\"expiresAt\":1785170942553}" } ] } diff --git a/frontend/e2e/.auth/user-tutorial.json b/frontend/e2e/.auth/user-tutorial.json index 6e6c776..38af82e 100644 --- a/frontend/e2e/.auth/user-tutorial.json +++ b/frontend/e2e/.auth/user-tutorial.json @@ -2,20 +2,20 @@ "cookies": [ { "name": "refresh_token", - "value": "LZA3Bk15HR3io_q8vfyqD3wlRoyV-GJwY-MjWvtUQzg", + "value": "ND-PngBFXIuDrD3eLsaPs1mWqwzusUSI6nPw-M3JjJo", "domain": "localhost", "path": "/api/auth", - "expires": 1792435594.060453, + "expires": 1792774029.131567, "httpOnly": true, "secure": true, "sameSite": "Strict" }, { "name": "access_token", - "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS10dXRvcmlhbEB0ZXN0LmNvbSIsInVzZXJfaWQiOiJjYjQyMzZkNC1kNDRlLTQ0YjMtOTYzYi0xYTUwMWEzZjM2NTMiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODQ2NzAzOTR9.pPd0JYgaAyn4_0YPOlxKoX1B9Ka4Vq2M3CpNcl7EDAU", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS10dXRvcmlhbEB0ZXN0LmNvbSIsInVzZXJfaWQiOiJkM2E5OWI5MC1kZDdlLTQ5MjQtYTZkZS1hMWU1OTcxMTE3NDciLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODUwMDg4Mjl9.ey7BplN-ghoJ8OGMkdOJavVAYVM_bv_xRBrtL02ASjQ", "domain": "localhost", "path": "/", - "expires": 1784670394.06041, + "expires": 1785008829.130873, "httpOnly": true, "secure": true, "sameSite": "Lax" @@ -27,11 +27,11 @@ "localStorage": [ { "name": "authSyncEvent", - "value": "{\"type\":\"logout\",\"at\":1784659593886}" + "value": "{\"type\":\"logout\",\"at\":1784998028750}" }, { "name": "parentAuth", - "value": "{\"expiresAt\":1784832394232}" + "value": "{\"expiresAt\":1785170829369}" } ] } diff --git a/frontend/e2e/.auth/user.json b/frontend/e2e/.auth/user.json index 60b346b..b02a4a8 100644 --- a/frontend/e2e/.auth/user.json +++ b/frontend/e2e/.auth/user.json @@ -2,20 +2,20 @@ "cookies": [ { "name": "refresh_token", - "value": "MYMeAyxeWQy7LVHrJsvF-10FozU5Ko_QoEvmV3bHwPc", + "value": "PbjRbjt9aqRjxoNjBDVJTuDsEEzgUPoplaNgzhOOwdQ", "domain": "localhost", "path": "/api/auth", - "expires": 1792435592.056481, + "expires": 1792774174.76082, "httpOnly": true, "secure": true, "sameSite": "Strict" }, { "name": "access_token", - "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiJiODJhZDU2Mi03MjMwLTRkN2EtODU3NS04N2FhNzE1ZWNhMjQiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODQ2NzAzOTJ9.iEbCtV3csb9AUWoXL4MJ-GLL1yiJDumvYmKr-vyuwBg", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiI4YzU1MWFiNi1lNmMxLTQ1MzgtOTEzNS0xNDgwN2M2NjM0OTQiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODUwMDg5NzR9._hCAKJRJgq4gcDKppcjkcWf9nX94wjBfa4J94Lh2ZlI", "domain": "localhost", "path": "/", - "expires": 1784670392.056434, + "expires": 1785008974.760293, "httpOnly": true, "secure": true, "sameSite": "Lax" @@ -27,11 +27,11 @@ "localStorage": [ { "name": "authSyncEvent", - "value": "{\"type\":\"logout\",\"at\":1784659591903}" + "value": "{\"type\":\"logout\",\"at\":1784998174529}" }, { "name": "parentAuth", - "value": "{\"expiresAt\":1784832392197}" + "value": "{\"expiresAt\":1785170974948}" } ] } diff --git a/frontend/e2e/mode_parent/create-child/validation.spec.ts b/frontend/e2e/mode_parent/create-child/validation.spec.ts index 4ef72cb..3148f68 100644 --- a/frontend/e2e/mode_parent/create-child/validation.spec.ts +++ b/frontend/e2e/mode_parent/create-child/validation.spec.ts @@ -15,7 +15,7 @@ test.describe('Create Child', () => { // Navigate to parent list and wait for it to fully load before clicking Add Child await gotoParentList(page) // Dismiss tutorial overlay if present — it intercepts pointer events - const skip = page.getByRole('button', { name: 'Skip tour' }) + const skip = page.getByRole('button', { name: 'Cancel' }) if (await skip.isVisible({ timeout: 1000 }).catch(() => false)) { await skip.click() } diff --git a/frontend/e2e/mode_parent/profile-button/profile-button-temporary.spec.ts b/frontend/e2e/mode_parent/profile-button/profile-button-temporary.spec.ts index 3084c97..851cedc 100644 --- a/frontend/e2e/mode_parent/profile-button/profile-button-temporary.spec.ts +++ b/frontend/e2e/mode_parent/profile-button/profile-button-temporary.spec.ts @@ -5,7 +5,7 @@ test.describe('Parent profile button – temporary parent mode', () => { test.beforeEach(async ({ page }) => { await page.goto('/parent') // Dismiss tutorial overlay if present — it intercepts pointer events - const skip = page.getByRole('button', { name: 'Skip tour' }) + const skip = page.getByRole('button', { name: 'Cancel' }) if (await skip.isVisible({ timeout: 1000 }).catch(() => false)) { await skip.click() } diff --git a/frontend/e2e/mode_parent/profile-button/profile-button.spec.ts b/frontend/e2e/mode_parent/profile-button/profile-button.spec.ts index efa0014..39622ba 100644 --- a/frontend/e2e/mode_parent/profile-button/profile-button.spec.ts +++ b/frontend/e2e/mode_parent/profile-button/profile-button.spec.ts @@ -5,7 +5,7 @@ test.describe('Parent profile button – permanent parent mode', () => { test.beforeEach(async ({ page }) => { await page.goto('/parent') // Dismiss tutorial overlay if present — it intercepts pointer events - const skip = page.getByRole('button', { name: 'Skip tour' }) + const skip = page.getByRole('button', { name: 'Cancel' }) if (await skip.isVisible({ timeout: 1000 }).catch(() => false)) { await skip.click() } diff --git a/frontend/e2e/mode_parent/tutorial/dialog-help-button.spec.ts b/frontend/e2e/mode_parent/tutorial/dialog-help-button.spec.ts index b037cd0..1a0ed1f 100644 --- a/frontend/e2e/mode_parent/tutorial/dialog-help-button.spec.ts +++ b/frontend/e2e/mode_parent/tutorial/dialog-help-button.spec.ts @@ -244,7 +244,7 @@ test.describe('Dialog help button and titles', () => { await card.click() await expect(modalBackdrop(page)).toBeVisible({ timeout: 3000 }) - await expect(page.locator('.modal-title')).toHaveText('Confirm Task') + await expect(page.locator('.modal-title')).toHaveText('Confirm Penalty') await expect(helpButton(page)).not.toBeVisible() await page.getByRole('button', { name: 'Cancel' }).click() diff --git a/frontend/e2e/mode_parent/tutorial/tutorial.spec.ts b/frontend/e2e/mode_parent/tutorial/tutorial.spec.ts index 6b15b2c..0fdf3f9 100644 --- a/frontend/e2e/mode_parent/tutorial/tutorial.spec.ts +++ b/frontend/e2e/mode_parent/tutorial/tutorial.spec.ts @@ -146,7 +146,7 @@ async function expectTutorialCard(page: Page, title: string): Promise { async function dismissTutorial(page: Page): Promise { const card = getTutorialCard(page) if (await card.isVisible({ timeout: 1000 }).catch(() => false)) { - // Use "Skip tour" to clear the active step and drain the queue in one + // Use "Cancel" to clear the active step and drain the queue in one // action, avoiding chained steps that would keep the card visible. await page.locator('.tutorial-root .btn-skip').click() await expect(card).not.toBeVisible({ timeout: 5000 }) diff --git a/frontend/src/__tests__/UserProfile.spec.ts b/frontend/src/__tests__/UserProfile.spec.ts index 810374e..7e8a998 100644 --- a/frontend/src/__tests__/UserProfile.spec.ts +++ b/frontend/src/__tests__/UserProfile.spec.ts @@ -34,16 +34,17 @@ vi.mock('../services/pushSubscription', () => ({ getPushPermissionState: vi.fn().mockReturnValue('default'), })) -function stubUserProfile() { - return { - template: '', - } -} +const mockResetAllProgress = vi.fn().mockResolvedValue(undefined) +vi.mock('@/tutorial/controller', () => ({ + tutorialEnabled: { value: true }, + setTutorialEnabled: vi.fn(), + resetAllProgress: () => mockResetAllProgress(), +})) function stubModalDialog() { return { - template: '
', - props: ['show'], + template: '

{{ title }}

', + props: ['title'], } } @@ -607,3 +608,64 @@ describe('UserProfile - Notification Toggles', () => { expect(body.push_notifications_enabled).toBe(true) }) }) + +describe('UserProfile - Tutorial restart', () => { + let wrapper: VueWrapper + + beforeEach(() => { + vi.clearAllMocks() + mockResetAllProgress.mockClear() + ;(global.fetch as any).mockClear() + ;(global.fetch as any).mockResolvedValue({ + ok: true, + json: async () => ({ + image_id: null, + first_name: 'Test', + last_name: 'User', + email: 'test@example.com', + email_digest_enabled: true, + }), + }) + + wrapper = mount(UserProfile, { + global: { + plugins: [mockRouter], + stubs: { + ProfileSection: stubProfileSection(), + ImagePicker: stubImagePicker(), + ToggleField: stubToggleField(), + ModalDialog: stubModalDialog(), + }, + }, + }) + }) + + it('restart confirm modal uses "Tutorial Restart" title and explanatory wording', async () => { + await flushPromises() + await nextTick() + + await wrapper.vm.openRestartConfirm() + await nextTick() + + const modal = wrapper.find('.mock-modal') + expect(modal.exists()).toBe(true) + expect(modal.text()).toContain('Tutorial Restart') + expect(modal.text()).toContain('Start the tour again') + }) + + it('confirming restart resets tutorial progress and shows success modal', async () => { + await flushPromises() + await nextTick() + + await wrapper.vm.openRestartConfirm() + await wrapper.vm.confirmRestartTutorial() + await flushPromises() + await nextTick() + + expect(mockResetAllProgress).toHaveBeenCalledTimes(1) + + const modal = wrapper.find('.mock-modal') + expect(modal.text()).toContain('Tutorial Restart') + expect(modal.text()).toContain('Tutorial mode has been restarted') + }) +}) diff --git a/frontend/src/components/child/TaskConfirmDialog.vue b/frontend/src/components/child/TaskConfirmDialog.vue index cc5d776..46927fe 100644 --- a/frontend/src/components/child/TaskConfirmDialog.vue +++ b/frontend/src/components/child/TaskConfirmDialog.vue @@ -28,9 +28,11 @@ const props = defineProps<{ childName?: string }>() -const dialogTitle = computed(() => - props.task?.type === 'kindness' ? 'Confirm Act' : 'Confirm Task', -) +const dialogTitle = computed(() => { + if (props.task?.type === 'kindness') return 'Confirm Act' + if (props.task?.type === 'penalty') return 'Confirm Penalty' + return 'Confirm Task' +}) defineEmits<{ confirm: [] diff --git a/frontend/src/components/child/__tests__/TaskConfirmDialog.spec.ts b/frontend/src/components/child/__tests__/TaskConfirmDialog.spec.ts index f2d9b68..6aea14b 100644 --- a/frontend/src/components/child/__tests__/TaskConfirmDialog.spec.ts +++ b/frontend/src/components/child/__tests__/TaskConfirmDialog.spec.ts @@ -40,4 +40,20 @@ describe('TaskConfirmDialog', () => { expect(wrapper.findComponent(ModalDialogStub).props('title')).toBe('Confirm Act') }) + + it('renders "Confirm Penalty" title for penalties', () => { + const task: Task = { + id: 'task-3', + name: 'No Screen Time', + type: 'penalty', + points: 5, + image_id: '', + } + const wrapper = mount(TaskConfirmDialog, { + props: { task, childName: 'Test Child' }, + global: { stubs: { ModalDialog: ModalDialogStub } }, + }) + + expect(wrapper.findComponent(ModalDialogStub).props('title')).toBe('Confirm Penalty') + }) }) diff --git a/frontend/src/components/profile/UserProfile.vue b/frontend/src/components/profile/UserProfile.vue index 17cd8e5..e81c39b 100644 --- a/frontend/src/components/profile/UserProfile.vue +++ b/frontend/src/components/profile/UserProfile.vue @@ -156,7 +156,7 @@