Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 36 additions & 22 deletions frontend/src/views/accounts/AccountsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -131,29 +131,43 @@ async function handleCreate(data: FormData) {
<h1 class="text-h4">
Accounts
</h1>
<!-- Settings menu (gear icon) -->
<v-menu>
<template #activator="{ props }">
<v-btn
icon
variant="text"
size="small"
v-bind="props"
data-testid="accounts-settings-menu"
>
<v-icon>mdi-cog</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item
data-testid="add-account-menu-item"
@click="showCreateDialog = true"
>
<template #prepend>
<v-icon>mdi-plus</v-icon>
</template>
<v-list-item-title>Add Account</v-list-item-title>
</v-list-item>
<v-divider />
<v-list-item
:disabled="accountsStore.reorderLoading"
data-testid="edit-order-menu-item"
@click="handleToggleEditMode"
>
<template #prepend>
<v-icon>{{ accountsStore.isEditMode ? 'mdi-check' : 'mdi-pencil' }}</v-icon>
</template>
<v-list-item-title>{{ accountsStore.isEditMode ? 'Done Editing' : 'Edit Order' }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-spacer />
<v-btn
:variant="accountsStore.isEditMode ? 'tonal' : 'text'"
:color="accountsStore.isEditMode ? 'primary' : undefined"
:loading="accountsStore.reorderLoading"
class="mr-2"
data-testid="edit-order-button"
@click="handleToggleEditMode"
>
<v-icon start>
{{ accountsStore.isEditMode ? 'mdi-check' : 'mdi-pencil' }}
</v-icon>
{{ accountsStore.isEditMode ? 'Done' : 'Edit Order' }}
</v-btn>
<v-btn
v-if="!accountsStore.isEditMode"
color="primary"
prepend-icon="mdi-plus"
data-testid="add-account-button"
@click="showCreateDialog = true"
>
Add Account
</v-btn>
</div>

<!-- Loading State -->
Expand Down
10 changes: 6 additions & 4 deletions frontend/tests/component/accounts/AccountForm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})

Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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()

Expand Down
38 changes: 16 additions & 22 deletions frontend/tests/component/accounts/AccountsReorder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand All @@ -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)
})
})

Expand Down
31 changes: 21 additions & 10 deletions frontend/tests/component/accounts/AccountsView.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,11 @@ async function mountAndSettle<T extends Parameters<typeof mountView>[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 () => {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down
40 changes: 22 additions & 18 deletions frontend/tests/e2e/pages/accounts.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"]')
Expand All @@ -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' })
Expand All @@ -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()
}

Expand Down Expand Up @@ -270,12 +273,13 @@ export class AccountsPage extends BasePage {
* Check if currently in edit mode.
*/
async isInEditMode(): Promise<boolean> {
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
Expand All @@ -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)
}

/**
Expand Down
Loading