From 725bf518ea607b86cb9ae7364d721511e2862507 Mon Sep 17 00:00:00 2001 From: Ryan Kegel Date: Thu, 19 Feb 2026 09:57:59 -0500 Subject: [PATCH] Refactor and enhance various components and tests - Remove OverrideEditModal.spec.ts test file. - Update ParentPinSetup.vue to handle Enter key for code and PIN inputs. - Modify ChildEditView.vue to add maxlength for age input. - Enhance ChildView.vue with reward confirmation and cancellation dialogs. - Update ParentView.vue to handle pending rewards and confirm edits. - Revise PendingRewardDialog.vue to accept a dynamic message prop. - Expand ChildView.spec.ts to cover reward dialog interactions. - Add tests for ParentView.vue to validate pending reward handling. - Update UserProfile.vue to simplify button styles. - Adjust RewardView.vue to improve delete confirmation handling. - Modify ChildrenListView.vue to clarify child creation instructions. - Refactor EntityEditForm.vue to improve input handling and focus management. - Enhance ItemList.vue to support item selection. - Update LoginButton.vue to focus PIN input on error. - Change ScrollingList.vue empty state color for better visibility. - Remove capture attribute from ImagePicker.vue file input. - Update router/index.ts to redirect logged-in users from auth routes. - Add authGuard.spec.ts to test router authentication logic. --- backend/config/version.py | 2 +- frontend/vue-app/src/assets/styles.css | 6 + .../src/components/OverrideEditModal.vue | 205 ----------------- .../__tests__/OverrideEditModal.spec.ts | 208 ------------------ .../src/components/auth/ParentPinSetup.vue | 10 +- .../src/components/child/ChildEditView.vue | 3 +- .../src/components/child/ChildView.vue | 97 ++++++++ .../src/components/child/ParentView.vue | 48 +++- .../components/child/PendingRewardDialog.vue | 13 +- .../child/__tests__/ChildView.spec.ts | 139 ++++++++++++ .../child/__tests__/ParentView.spec.ts | 102 +++++++++ .../src/components/profile/UserProfile.vue | 18 +- .../src/components/reward/RewardView.vue | 2 +- .../components/shared/ChildrenListView.vue | 4 +- .../src/components/shared/EntityEditForm.vue | 42 +++- .../src/components/shared/ItemList.vue | 8 + .../src/components/shared/LoginButton.vue | 3 + .../src/components/shared/ScrollingList.vue | 2 +- .../src/components/utils/ImagePicker.vue | 1 - .../src/router/__tests__/authGuard.spec.ts | 153 +++++++++++++ frontend/vue-app/src/router/index.ts | 9 + 21 files changed, 630 insertions(+), 445 deletions(-) delete mode 100644 frontend/vue-app/src/components/OverrideEditModal.vue delete mode 100644 frontend/vue-app/src/components/__tests__/OverrideEditModal.spec.ts create mode 100644 frontend/vue-app/src/router/__tests__/authGuard.spec.ts diff --git a/backend/config/version.py b/backend/config/version.py index 87ab433..6343bb0 100644 --- a/backend/config/version.py +++ b/backend/config/version.py @@ -2,7 +2,7 @@ # file: config/version.py import os -BASE_VERSION = "1.0.4RC3" # update manually when releasing features +BASE_VERSION = "1.0.4RC4" # update manually when releasing features def get_full_version() -> str: """ diff --git a/frontend/vue-app/src/assets/styles.css b/frontend/vue-app/src/assets/styles.css index 1e466bd..174ad02 100644 --- a/frontend/vue-app/src/assets/styles.css +++ b/frontend/vue-app/src/assets/styles.css @@ -85,6 +85,12 @@ pointer-events: none; color: var(--btn-primary); } +@media (max-width: 520px) { + .btn-link { + margin-top: 1rem; + margin-bottom: 1rem; + } +} /* Rounded button */ .round-btn { diff --git a/frontend/vue-app/src/components/OverrideEditModal.vue b/frontend/vue-app/src/components/OverrideEditModal.vue deleted file mode 100644 index e925375..0000000 --- a/frontend/vue-app/src/components/OverrideEditModal.vue +++ /dev/null @@ -1,205 +0,0 @@ - - - - - diff --git a/frontend/vue-app/src/components/__tests__/OverrideEditModal.spec.ts b/frontend/vue-app/src/components/__tests__/OverrideEditModal.spec.ts deleted file mode 100644 index bdaa3b6..0000000 --- a/frontend/vue-app/src/components/__tests__/OverrideEditModal.spec.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' -import { mount, VueWrapper } from '@vue/test-utils' -import { nextTick } from 'vue' -import OverrideEditModal from '../OverrideEditModal.vue' - -// Mock API functions -vi.mock('@/common/api', () => ({ - setChildOverride: vi.fn(), - parseErrorResponse: vi.fn(() => ({ msg: 'Test error', code: 'TEST_ERROR' })), -})) - -import { setChildOverride } from '@/common/api' - -global.alert = vi.fn() - -describe('OverrideEditModal', () => { - let wrapper: VueWrapper - - const defaultProps = { - isOpen: true, - childId: 'child-123', - entityId: 'task-456', - entityType: 'task' as 'task' | 'reward', - entityName: 'Test Task', - defaultValue: 100, - currentOverride: undefined, - } - - beforeEach(() => { - vi.clearAllMocks() - }) - - afterEach(() => { - if (wrapper) { - wrapper.unmount() - } - }) - - describe('Modal Display', () => { - it('renders when isOpen is true', () => { - wrapper = mount(OverrideEditModal, { props: defaultProps }) - expect(wrapper.find('.modal-backdrop').exists()).toBe(true) - expect(wrapper.text()).toContain('Test Task') - }) - - it('does not render when isOpen is false', () => { - wrapper = mount(OverrideEditModal, { props: { ...defaultProps, isOpen: false } }) - expect(wrapper.find('.modal-backdrop').exists()).toBe(false) - }) - - it('displays entity information correctly for tasks', () => { - wrapper = mount(OverrideEditModal, { props: defaultProps }) - expect(wrapper.text()).toContain('Test Task') - expect(wrapper.text()).toContain('New Points') - }) - - it('displays entity information correctly for rewards', () => { - wrapper = mount(OverrideEditModal, { - props: { ...defaultProps, entityType: 'reward', entityName: 'Test Reward' }, - }) - expect(wrapper.text()).toContain('Test Reward') - expect(wrapper.text()).toContain('New Cost') - }) - }) - - describe('Input Validation', () => { - it('initializes with default value when no override exists', async () => { - wrapper = mount(OverrideEditModal, { props: defaultProps }) - await nextTick() - const input = wrapper.find('input[type="number"]') - expect((input.element as HTMLInputElement).value).toBe('100') - }) - - it('initializes with current override value when it exists', async () => { - wrapper = mount(OverrideEditModal, { - props: { ...defaultProps, currentOverride: 150 }, - }) - await nextTick() - const input = wrapper.find('input[type="number"]') - expect((input.element as HTMLInputElement).value).toBe('150') - }) - - it('validates input within range (0-10000)', async () => { - wrapper = mount(OverrideEditModal, { props: defaultProps }) - const input = wrapper.find('input[type="number"]') - const saveButton = wrapper.findAll('button').find((btn) => btn.text() === 'Save') - - // Valid value - await input.setValue(5000) - await nextTick() - expect(saveButton?.attributes('disabled')).toBeUndefined() - - // Zero is valid - await input.setValue(0) - await nextTick() - expect(saveButton?.attributes('disabled')).toBeUndefined() - - // Max is valid - await input.setValue(10000) - await nextTick() - expect(saveButton?.attributes('disabled')).toBeUndefined() - }) - - it('shows error for values outside range', async () => { - wrapper = mount(OverrideEditModal, { props: defaultProps }) - const input = wrapper.find('input[type="number"]') - - // Above max - await input.setValue(10001) - await nextTick() - expect(wrapper.text()).toContain('Value must be between 0 and 10000') - const saveButton = wrapper.findAll('button').find((btn) => btn.text() === 'Save') - expect(saveButton?.attributes('disabled')).toBeDefined() - }) - }) - - describe('User Interactions', () => { - it('emits close event when Cancel is clicked', async () => { - wrapper = mount(OverrideEditModal, { props: defaultProps }) - const cancelButton = wrapper.findAll('button').find((btn) => btn.text() === 'Cancel') - await cancelButton?.trigger('click') - expect(wrapper.emitted('close')).toBeTruthy() - }) - - it('emits close event when clicking backdrop', async () => { - wrapper = mount(OverrideEditModal, { props: defaultProps }) - await wrapper.find('.modal-backdrop').trigger('click') - expect(wrapper.emitted('close')).toBeTruthy() - }) - - it('does not close when clicking modal dialog', async () => { - wrapper = mount(OverrideEditModal, { props: defaultProps }) - await wrapper.find('.modal-dialog').trigger('click') - expect(wrapper.emitted('close')).toBeFalsy() - }) - - it('calls API and emits events on successful save', async () => { - ;(setChildOverride as any).mockResolvedValue({ ok: true }) - - wrapper = mount(OverrideEditModal, { props: defaultProps }) - const input = wrapper.find('input[type="number"]') - await input.setValue(250) - await nextTick() - - const saveButton = wrapper.findAll('button').find((btn) => btn.text() === 'Save') - await saveButton?.trigger('click') - await nextTick() - - expect(setChildOverride).toHaveBeenCalledWith('child-123', 'task-456', 'task', 250) - expect(wrapper.emitted('saved')).toBeTruthy() - expect(wrapper.emitted('close')).toBeTruthy() - }) - - it('shows alert on API error', async () => { - ;(setChildOverride as any).mockResolvedValue({ ok: false, status: 400 }) - - wrapper = mount(OverrideEditModal, { props: defaultProps }) - const saveButton = wrapper.findAll('button').find((btn) => btn.text() === 'Save') - await saveButton?.trigger('click') - await nextTick() - - expect(global.alert).toHaveBeenCalledWith('Error: Test error') - expect(wrapper.emitted('saved')).toBeFalsy() - }) - - it('does not save when validation fails', async () => { - wrapper = mount(OverrideEditModal, { props: defaultProps }) - const input = wrapper.find('input[type="number"]') - await input.setValue(20000) - await nextTick() - - const saveButton = wrapper.findAll('button').find((btn) => btn.text() === 'Save') - await saveButton?.trigger('click') - await nextTick() - - expect(setChildOverride).not.toHaveBeenCalled() - }) - }) - - describe('Modal State Updates', () => { - it('reinitializes value when modal reopens', async () => { - wrapper = mount(OverrideEditModal, { props: { ...defaultProps, isOpen: false } }) - await nextTick() - - await wrapper.setProps({ isOpen: true }) - await nextTick() - - const input = wrapper.find('input[type="number"]') - expect((input.element as HTMLInputElement).value).toBe('100') - }) - - it('uses updated currentOverride when modal reopens', async () => { - wrapper = mount(OverrideEditModal, { - props: { ...defaultProps, isOpen: true, currentOverride: 200 }, - }) - await nextTick() - - await wrapper.setProps({ isOpen: false }) - await nextTick() - - await wrapper.setProps({ isOpen: true, currentOverride: 300 }) - await nextTick() - - const input = wrapper.find('input[type="number"]') - expect((input.element as HTMLInputElement).value).toBe('300') - }) - }) -}) diff --git a/frontend/vue-app/src/components/auth/ParentPinSetup.vue b/frontend/vue-app/src/components/auth/ParentPinSetup.vue index 9d9df99..5bc13c2 100644 --- a/frontend/vue-app/src/components/auth/ParentPinSetup.vue +++ b/frontend/vue-app/src/components/auth/ParentPinSetup.vue @@ -18,7 +18,13 @@ We've sent a 6-digit code to your email. Enter it below to continue. The code is valid for 10 minutes.

- +
+ + + + + + + + + diff --git a/frontend/vue-app/src/components/child/ParentView.vue b/frontend/vue-app/src/components/child/ParentView.vue index 8922add..5ad49fd 100644 --- a/frontend/vue-app/src/components/child/ParentView.vue +++ b/frontend/vue-app/src/components/child/ParentView.vue @@ -1,5 +1,5 @@