From df505e7066377623575f03f9b464647470b37a2d Mon Sep 17 00:00:00 2001 From: Matthew Wilson <22526682+automationator@users.noreply.github.com> Date: Mon, 30 Mar 2026 20:41:34 -0400 Subject: [PATCH] Replace account page header buttons with cog settings menu Replace the separate "Add Account" and "Edit Order" buttons with a cog icon menu matching the Envelopes page pattern. Update component tests, E2E page object, and fix AccountForm tests for MoneyInput. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/views/accounts/AccountsView.vue | 58 ++++++++++++------- .../component/accounts/AccountForm.spec.ts | 10 ++-- .../accounts/AccountsReorder.spec.ts | 38 +++++------- .../component/accounts/AccountsView.spec.ts | 31 ++++++---- frontend/tests/e2e/pages/accounts.page.ts | 40 +++++++------ 5 files changed, 101 insertions(+), 76 deletions(-) diff --git a/frontend/src/views/accounts/AccountsView.vue b/frontend/src/views/accounts/AccountsView.vue index 021d974..7bcd34d 100644 --- a/frontend/src/views/accounts/AccountsView.vue +++ b/frontend/src/views/accounts/AccountsView.vue @@ -131,29 +131,43 @@ async function handleCreate(data: FormData) {

Accounts

+ + + + + + + Add Account + + + + + {{ accountsStore.isEditMode ? 'Done Editing' : 'Edit Order' }} + + + - - - {{ accountsStore.isEditMode ? 'mdi-check' : 'mdi-pencil' }} - - {{ accountsStore.isEditMode ? 'Done' : 'Edit Order' }} - - - Add Account - diff --git a/frontend/tests/component/accounts/AccountForm.spec.ts b/frontend/tests/component/accounts/AccountForm.spec.ts index d5a7161..11c9970 100644 --- a/frontend/tests/component/accounts/AccountForm.spec.ts +++ b/frontend/tests/component/accounts/AccountForm.spec.ts @@ -35,7 +35,6 @@ describe('AccountForm', () => { describe('create mode', () => { it('shows starting balance field', () => { const { wrapper } = mountComponent() - expect(wrapper.find('input[type="number"]').exists()).toBe(true) expect(wrapper.text()).toContain('Starting Balance') }) @@ -77,7 +76,7 @@ describe('AccountForm', () => { it('hides starting balance field', () => { const { wrapper } = mountComponent({ account: existingAccount }) - expect(wrapper.find('input[type="number"]').exists()).toBe(false) + expect(wrapper.text()).not.toContain('Starting Balance') }) it('shows active toggle', () => { @@ -156,8 +155,11 @@ describe('AccountForm', () => { const nameInput = wrapper.find('input[type="text"]') await nameInput.setValue('New Account') - const balanceInput = wrapper.find('input[type="number"]') - await balanceInput.setValue('123.45') + // MoneyInput uses type="text" with inputmode="numeric" and auto-decimal + // Find the MoneyInput's text input (second text input after name) + const moneyInput = wrapper.findComponent({ name: 'MoneyInput' }) + // Simulate typing digits "12345" which MoneyInput formats as "123.45" + await moneyInput.vm.$emit('update:modelValue', '123.45') await nextTick() await nextTick() diff --git a/frontend/tests/component/accounts/AccountsReorder.spec.ts b/frontend/tests/component/accounts/AccountsReorder.spec.ts index 51f82be..02895bf 100644 --- a/frontend/tests/component/accounts/AccountsReorder.spec.ts +++ b/frontend/tests/component/accounts/AccountsReorder.spec.ts @@ -51,21 +51,21 @@ function el(wrapper: VueWrapper): HTMLElement { return wrapper.element as HTMLElement } -// Helper: enter edit mode by clicking the edit-order button +// Helper: enter edit mode via the settings menu async function enterEditMode(wrapper: VueWrapper) { - const editBtn = wrapper.find('[data-testid="edit-order-button"]') - expect(editBtn.exists()).toBe(true) - await editBtn.trigger('click') + await wrapper.find('[data-testid="accounts-settings-menu"]').trigger('click') + await flushPromises() + await document.querySelector('[data-testid="edit-order-menu-item"]')?.dispatchEvent(new Event('click', { bubbles: true })) await flushPromises() await flushPromises() await flushPromises() } -// Helper: exit edit mode by clicking the done button +// Helper: exit edit mode via the settings menu async function exitEditMode(wrapper: VueWrapper) { - const doneBtn = wrapper.find('[data-testid="edit-order-button"]') - expect(doneBtn.exists()).toBe(true) - await doneBtn.trigger('click') + await wrapper.find('[data-testid="accounts-settings-menu"]').trigger('click') + await flushPromises() + await document.querySelector('[data-testid="edit-order-menu-item"]')?.dispatchEvent(new Event('click', { bubbles: true })) await flushPromises() await flushPromises() } @@ -76,43 +76,37 @@ describe('AccountsView - Edit Mode / Reordering', () => { useReorderHandlers() const { wrapper } = await mountAndSettle() - // Initially shows "Edit Order" button, no reorder buttons - const editBtn = wrapper.find('[data-testid="edit-order-button"]') - expect(editBtn.text()).toContain('Edit Order') + // Initially no reorder buttons visible expect(el(wrapper).querySelector('[data-testid="move-up-button"]')).toBeNull() // Enter edit mode await enterEditMode(wrapper) - // Button text should change to "Done" - expect(wrapper.find('[data-testid="edit-order-button"]').text()).toContain('Done') // Reorder buttons should be visible expect(el(wrapper).querySelector('[data-testid="move-up-button"]')).not.toBeNull() // Exit edit mode await exitEditMode(wrapper) - // Button should revert to "Edit Order" - expect(wrapper.find('[data-testid="edit-order-button"]').text()).toContain('Edit Order') // Reorder buttons should be hidden await flushPromises() expect(el(wrapper).querySelector('[data-testid="move-up-button"]')).toBeNull() }) - it('hides add button in edit mode', async () => { + it('settings menu remains accessible in edit mode', async () => { useReorderHandlers() const { wrapper } = await mountAndSettle() - // Add button visible initially - expect(wrapper.find('[data-testid="add-account-button"]').exists()).toBe(true) + // Settings menu visible initially + expect(wrapper.find('[data-testid="accounts-settings-menu"]').exists()).toBe(true) - // Enter edit mode - add button should be hidden + // Enter edit mode - settings menu should still be visible await enterEditMode(wrapper) - expect(wrapper.find('[data-testid="add-account-button"]').exists()).toBe(false) + expect(wrapper.find('[data-testid="accounts-settings-menu"]').exists()).toBe(true) - // Exit edit mode - add button should reappear + // Exit edit mode - settings menu should still be visible await exitEditMode(wrapper) - expect(wrapper.find('[data-testid="add-account-button"]').exists()).toBe(true) + expect(wrapper.find('[data-testid="accounts-settings-menu"]').exists()).toBe(true) }) }) diff --git a/frontend/tests/component/accounts/AccountsView.spec.ts b/frontend/tests/component/accounts/AccountsView.spec.ts index 3c6c849..6793938 100644 --- a/frontend/tests/component/accounts/AccountsView.spec.ts +++ b/frontend/tests/component/accounts/AccountsView.spec.ts @@ -24,12 +24,11 @@ async function mountAndSettle[0]>( } describe('AccountsView', () => { - it('displays page title and Add Account button', async () => { + it('displays page title and settings menu', async () => { const { wrapper } = await mountAndSettle(AccountsView) expect(wrapper.find('h1').text()).toBe('Accounts') - const addBtn = wrapper.find('[data-testid="add-account-button"]') - expect(addBtn.exists()).toBe(true) - expect(addBtn.text()).toContain('Add Account') + const settingsBtn = wrapper.find('[data-testid="accounts-settings-menu"]') + expect(settingsBtn.exists()).toBe(true) }) it('shows empty state when no accounts', async () => { @@ -48,7 +47,9 @@ describe('AccountsView', () => { const { wrapper } = await mountAndSettle(AccountsView, { attachTo: document.body }) // Open create dialog - await wrapper.find('[data-testid="add-account-button"]').trigger('click') + await wrapper.find('[data-testid="accounts-settings-menu"]').trigger('click') + await flushPromises() + await document.querySelector('[data-testid="add-account-menu-item"]')?.dispatchEvent(new Event('click', { bubbles: true })) await flushPromises() // Dialog should show @@ -77,7 +78,9 @@ describe('AccountsView', () => { it('creates a savings account with description', async () => { const { wrapper } = await mountAndSettle(AccountsView, { attachTo: document.body }) - await wrapper.find('[data-testid="add-account-button"]').trigger('click') + await wrapper.find('[data-testid="accounts-settings-menu"]').trigger('click') + await flushPromises() + await document.querySelector('[data-testid="add-account-menu-item"]')?.dispatchEvent(new Event('click', { bubbles: true })) await flushPromises() // Fill name @@ -95,7 +98,9 @@ describe('AccountsView', () => { it('creates a credit card account', async () => { const { wrapper } = await mountAndSettle(AccountsView, { attachTo: document.body }) - await wrapper.find('[data-testid="add-account-button"]').trigger('click') + await wrapper.find('[data-testid="accounts-settings-menu"]').trigger('click') + await flushPromises() + await document.querySelector('[data-testid="add-account-menu-item"]')?.dispatchEvent(new Event('click', { bubbles: true })) await flushPromises() // Verify the account type select exists with options @@ -108,7 +113,9 @@ describe('AccountsView', () => { it('shows validation error for empty name (disabled button)', async () => { const { wrapper } = await mountAndSettle(AccountsView, { attachTo: document.body }) - await wrapper.find('[data-testid="add-account-button"]').trigger('click') + await wrapper.find('[data-testid="accounts-settings-menu"]').trigger('click') + await flushPromises() + await document.querySelector('[data-testid="add-account-menu-item"]')?.dispatchEvent(new Event('click', { bubbles: true })) await flushPromises() // Create button should be disabled (no name entered) @@ -124,7 +131,9 @@ describe('AccountsView', () => { it('creates account with icon from picker', async () => { const { wrapper } = await mountAndSettle(AccountsView, { attachTo: document.body }) - await wrapper.find('[data-testid="add-account-button"]').trigger('click') + await wrapper.find('[data-testid="accounts-settings-menu"]').trigger('click') + await flushPromises() + await document.querySelector('[data-testid="add-account-menu-item"]')?.dispatchEvent(new Event('click', { bubbles: true })) await flushPromises() // Icon chips should be visible @@ -142,7 +151,9 @@ describe('AccountsView', () => { it('creates account with custom emoji', async () => { const { wrapper } = await mountAndSettle(AccountsView, { attachTo: document.body }) - await wrapper.find('[data-testid="add-account-button"]').trigger('click') + await wrapper.find('[data-testid="accounts-settings-menu"]').trigger('click') + await flushPromises() + await document.querySelector('[data-testid="add-account-menu-item"]')?.dispatchEvent(new Event('click', { bubbles: true })) await flushPromises() // Custom emoji input should exist diff --git a/frontend/tests/e2e/pages/accounts.page.ts b/frontend/tests/e2e/pages/accounts.page.ts index 7776634..051c8b4 100644 --- a/frontend/tests/e2e/pages/accounts.page.ts +++ b/frontend/tests/e2e/pages/accounts.page.ts @@ -7,8 +7,9 @@ import type { AccountType } from '../fixtures/test-data' */ export class AccountsPage extends BasePage { // Header elements - readonly addAccountButton: Locator - readonly editOrderButton: Locator + readonly settingsMenuButton: Locator + readonly addAccountMenuItem: Locator + readonly editOrderMenuItem: Locator // Account list sections readonly allAccountsRow: Locator @@ -37,8 +38,9 @@ export class AccountsPage extends BasePage { super(page) // Header buttons - this.addAccountButton = page.locator('[data-testid="add-account-button"]') - this.editOrderButton = page.locator('[data-testid="edit-order-button"]') + this.settingsMenuButton = page.locator('[data-testid="accounts-settings-menu"]') + this.addAccountMenuItem = page.locator('[data-testid="add-account-menu-item"]') + this.editOrderMenuItem = page.locator('[data-testid="edit-order-menu-item"]') // Account list sections (scoped to main content to avoid matching sidebar) const mainContent = page.locator('main, [role="main"]') @@ -59,7 +61,7 @@ export class AccountsPage extends BasePage { this.customIconInput = this.createDialog.locator('[data-testid="custom-icon-input"] input') this.useDefaultIconButton = this.createDialog.locator('[data-testid="use-default-icon-button"]') this.descriptionInput = this.createDialog.locator('textarea') - this.startingBalanceInput = this.createDialog.locator('input[type="number"]') + this.startingBalanceInput = this.createDialog.locator('input[inputmode="numeric"]') this.includeInBudgetSwitch = this.createDialog.locator('.v-switch').filter({ hasText: 'Include in budget' }) this.createButton = this.createDialog.getByRole('button', { name: 'Create' }) this.cancelButton = this.createDialog.getByRole('button', { name: 'Cancel' }) @@ -74,7 +76,8 @@ export class AccountsPage extends BasePage { * Open the create account dialog. */ async openCreateDialog() { - await this.addAccountButton.click() + await this.settingsMenuButton.click() + await this.addAccountMenuItem.click() await expect(this.createDialog).toBeVisible() } @@ -270,12 +273,13 @@ export class AccountsPage extends BasePage { * Check if currently in edit mode. */ async isInEditMode(): Promise { - const buttonText = await this.editOrderButton.textContent() - return buttonText?.includes('Done') ?? false + // Check for reorder buttons being visible as the indicator of edit mode + const moveUpButton = this.page.locator('[data-testid="move-up-button"]').first() + return await moveUpButton.isVisible().catch(() => false) } /** - * Enter edit mode by clicking the Edit Order button. + * Enter edit mode via the settings menu. */ async enterEditMode() { if (await this.isInEditMode()) return @@ -285,33 +289,33 @@ export class AccountsPage extends BasePage { .locator('[data-testid="budget-account-item"], [data-testid="tracking-account-item"]') .first() .waitFor({ state: 'visible', timeout: 10000 }) - await this.editOrderButton.click() - await expect(this.editOrderButton).toContainText('Done') + await this.settingsMenuButton.click() + await this.editOrderMenuItem.click() + await expect(this.page.locator('[data-testid="move-up-button"]').first()).toBeVisible() } /** - * Exit edit mode by clicking the Done button. + * Exit edit mode via the settings menu. */ async exitEditMode() { if (!(await this.isInEditMode())) return - await this.editOrderButton.click() - await expect(this.editOrderButton).toContainText('Edit Order') + await this.settingsMenuButton.click() + await this.editOrderMenuItem.click() + await expect(this.page.locator('[data-testid="move-up-button"]').first()).toBeHidden() } /** * Assert we are in edit mode. */ async expectInEditMode() { - await expect(this.editOrderButton).toContainText('Done') - await expect(this.addAccountButton).toBeHidden() + await expect(this.page.locator('[data-testid="move-up-button"]').first()).toBeVisible() } /** * Assert we are not in edit mode. */ async expectNotInEditMode() { - await expect(this.editOrderButton).toContainText('Edit Order') - await expect(this.addAccountButton).toBeVisible() + await expect(this.page.locator('[data-testid="move-up-button"]')).toHaveCount(0) } /**