profileShare

rasmusjy / splitapp-frontend-vue

Read-only snapshot

No repository description.

main default branch 96 files Expires Sep 13, 2026, 9:06 AM
SplitMethodSelector.spec.ts 6,798 bytes
1 import { describe, it, expect } from 'vitest'
2 import { mount } from '@vue/test-utils'
3 import SplitMethodSelector from '@/components/SplitMethodSelector.vue'
4 import type { ITripParticipant } from '@/types/ITrip'
5 import type { IExpenseSplitCreate } from '@/types/IExpense'
6
7 function participant(id: string, name: string): ITripParticipant {
8 return {
9 id: `tp-${id}`,
10 tripId: 'trip-1',
11 userId: id,
12 userName: name,
13 userEmail: `${name.toLowerCase()}@t.ee`,
14 role: 'Participant',
15 nickname: null,
16 joinedAt: '2026-01-01',
17 isActive: true,
18 }
19 }
20
21 const PARTICIPANTS: ITripParticipant[] = [
22 participant('u1', 'Alice'),
23 participant('u2', 'Bob'),
24 participant('u3', 'Charlie'),
25 ]
26
27 // Grab the latest payload emitted for a given event, or null if never emitted.
28 function latestEmit<T>(wrapper: ReturnType<typeof mount>, event: string): T | null {
29 const calls = wrapper.emitted<[T]>(event)
30 if (!calls || calls.length === 0) return null
31 return calls[calls.length - 1]![0]
32 }
33
34 describe('SplitMethodSelector — EqualAll', () => {
35 it('splits equally among every participant', () => {
36 const wrapper = mount(SplitMethodSelector, {
37 props: {
38 participants: PARTICIPANTS,
39 totalAmount: 90,
40 splitMethod: 'EqualAll',
41 },
42 })
43
44 const splits = latestEmit<IExpenseSplitCreate[]>(wrapper, 'update:splits')
45 expect(splits).toHaveLength(3)
46 for (const s of splits!) {
47 expect(s.amount).toBe(30)
48 expect(s.percentage).toBeNull()
49 }
50
51 expect(latestEmit<boolean>(wrapper, 'update:valid')).toBe(true)
52 })
53
54 it('is invalid when there are no participants', () => {
55 const wrapper = mount(SplitMethodSelector, {
56 props: {
57 participants: [],
58 totalAmount: 50,
59 splitMethod: 'EqualAll',
60 },
61 })
62 expect(latestEmit<boolean>(wrapper, 'update:valid')).toBe(false)
63 expect(latestEmit<IExpenseSplitCreate[]>(wrapper, 'update:splits')).toEqual([])
64 })
65 })
66
67 describe('SplitMethodSelector — EqualSubset', () => {
68 it('auto-selects every participant on first switch to EqualSubset', () => {
69 const wrapper = mount(SplitMethodSelector, {
70 props: {
71 participants: PARTICIPANTS,
72 totalAmount: 60,
73 splitMethod: 'EqualSubset',
74 },
75 })
76
77 const splits = latestEmit<IExpenseSplitCreate[]>(wrapper, 'update:splits')
78 expect(splits).toHaveLength(3)
79 expect(splits!.map((s) => s.amount)).toEqual([20, 20, 20])
80 expect(latestEmit<boolean>(wrapper, 'update:valid')).toBe(true)
81 })
82
83 it('toggling a checkbox off removes that user and redistributes', async () => {
84 const wrapper = mount(SplitMethodSelector, {
85 props: {
86 participants: PARTICIPANTS,
87 totalAmount: 60,
88 splitMethod: 'EqualSubset',
89 },
90 })
91
92 // Deselect Bob by firing a change event on his hidden checkbox.
93 // (jsdom does not always propagate label clicks to the wrapped checkbox,
94 // so we dispatch on the input directly.)
95 const checkboxes = wrapper.findAll('input[type="checkbox"]')
96 await checkboxes[1]!.trigger('change')
97
98 const splits = latestEmit<IExpenseSplitCreate[]>(wrapper, 'update:splits')
99 expect(splits).toHaveLength(2)
100 expect(splits!.map((s) => s.userId).sort()).toEqual(['u1', 'u3'])
101 expect(splits!.every((s) => s.amount === 30)).toBe(true)
102 })
103 })
104
105 describe('SplitMethodSelector — ExactAmounts', () => {
106 it('is invalid until entered amounts match the total', async () => {
107 const wrapper = mount(SplitMethodSelector, {
108 props: {
109 participants: PARTICIPANTS,
110 totalAmount: 100,
111 splitMethod: 'ExactAmounts',
112 },
113 })
114
115 // Initially all participants have amount 0 → not valid (0 ≠ 100).
116 expect(latestEmit<boolean>(wrapper, 'update:valid')).toBe(false)
117
118 // Fill inputs so they sum to exactly 100.
119 const inputs = wrapper.findAll('input[type="number"]')
120 await inputs[0]!.setValue('50')
121 await inputs[1]!.setValue('30')
122 await inputs[2]!.setValue('20')
123
124 expect(latestEmit<boolean>(wrapper, 'update:valid')).toBe(true)
125 const splits = latestEmit<IExpenseSplitCreate[]>(wrapper, 'update:splits')
126 expect(splits!.map((s) => s.amount)).toEqual([50, 30, 20])
127 })
128
129 it('stays invalid when the sum differs from total', async () => {
130 const wrapper = mount(SplitMethodSelector, {
131 props: {
132 participants: PARTICIPANTS,
133 totalAmount: 100,
134 splitMethod: 'ExactAmounts',
135 },
136 })
137 const inputs = wrapper.findAll('input[type="number"]')
138 await inputs[0]!.setValue('50')
139 await inputs[1]!.setValue('30')
140 // Missing 20 → 80 ≠ 100
141 expect(latestEmit<boolean>(wrapper, 'update:valid')).toBe(false)
142 })
143 })
144
145 describe('SplitMethodSelector — Percentages', () => {
146 it('is valid only when percentages add up to 100', async () => {
147 const wrapper = mount(SplitMethodSelector, {
148 props: {
149 participants: PARTICIPANTS,
150 totalAmount: 200,
151 splitMethod: 'Percentages',
152 },
153 })
154
155 expect(latestEmit<boolean>(wrapper, 'update:valid')).toBe(false)
156
157 const inputs = wrapper.findAll('input[type="number"]')
158 await inputs[0]!.setValue('50')
159 await inputs[1]!.setValue('30')
160 await inputs[2]!.setValue('20')
161
162 expect(latestEmit<boolean>(wrapper, 'update:valid')).toBe(true)
163
164 const splits = latestEmit<IExpenseSplitCreate[]>(wrapper, 'update:splits')
165 expect(splits!.map((s) => s.amount)).toEqual([100, 60, 40])
166 expect(splits!.map((s) => s.percentage)).toEqual([50, 30, 20])
167 })
168
169 it('90% total is rejected', async () => {
170 const wrapper = mount(SplitMethodSelector, {
171 props: {
172 participants: PARTICIPANTS,
173 totalAmount: 100,
174 splitMethod: 'Percentages',
175 },
176 })
177 const inputs = wrapper.findAll('input[type="number"]')
178 await inputs[0]!.setValue('30')
179 await inputs[1]!.setValue('30')
180 await inputs[2]!.setValue('30')
181 expect(latestEmit<boolean>(wrapper, 'update:valid')).toBe(false)
182 })
183 })
184
185 describe('SplitMethodSelector — existingSplits (edit mode)', () => {
186 it('pre-selects users from existingSplits for EqualSubset', () => {
187 const wrapper = mount(SplitMethodSelector, {
188 props: {
189 participants: PARTICIPANTS,
190 totalAmount: 80,
191 splitMethod: 'EqualSubset',
192 existingSplits: [
193 { id: 's1', userId: 'u1', userName: 'Alice', amount: 40, percentage: null },
194 { id: 's2', userId: 'u3', userName: 'Charlie', amount: 40, percentage: null },
195 ],
196 },
197 })
198
199 const splits = latestEmit<IExpenseSplitCreate[]>(wrapper, 'update:splits')
200 expect(splits).toHaveLength(2)
201 expect(splits!.map((s) => s.userId).sort()).toEqual(['u1', 'u3'])
202 expect(splits!.every((s) => s.amount === 40)).toBe(true)
203 })
204 })
205