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 @@
-
-
-
-
Edit {{ entityName }}
-
-
-
-
{{ errorMessage }}
-
Default: {{ defaultValue }}
-
-
-
-
-
-
-
-
-
-
-
-
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.
-
+
+
+
+
+
+
+
+
+ This reward is pending.
Would you like to cancel the request?
+
+
+
+
+
+
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 @@