diff --git a/frontend/src/components/ui/__tests__/ToastContainer.test.js b/frontend/src/components/ui/__tests__/ToastContainer.test.js new file mode 100644 index 0000000..65fb5db --- /dev/null +++ b/frontend/src/components/ui/__tests__/ToastContainer.test.js @@ -0,0 +1,57 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { mount } from '@vue/test-utils' +import { setActivePinia, createPinia } from 'pinia' +import { useToastStore } from '../../../stores/toast.js' +import ToastContainer from '../ToastContainer.vue' + +beforeEach(() => { + setActivePinia(createPinia()) +}) + +describe('ToastContainer', () => { + it('renders nothing when store.toasts is empty', () => { + const wrapper = mount(ToastContainer, { + global: { + stubs: { AppIcon: true, Teleport: true }, + }, + }) + expect(wrapper.find('[data-test="toast"]').exists()).toBe(false) + }) + + it('renders one element per toast', () => { + const store = useToastStore() + store.show('First', 'success', 0) + store.show('Second', 'error', 0) + const wrapper = mount(ToastContainer, { + global: { + stubs: { AppIcon: true, Teleport: true }, + }, + }) + expect(wrapper.findAll('[data-test="toast"]').length).toBe(2) + }) + + it('clicking a toast calls dismiss(id)', async () => { + const store = useToastStore() + store.show('Click me', 'info', 0) + const wrapper = mount(ToastContainer, { + global: { + stubs: { AppIcon: true, Teleport: true }, + }, + }) + await wrapper.find('[data-test="toast"]').trigger('click') + expect(store.toasts.length).toBe(0) + }) + + it('accent class matches type (success -> bg-green-500)', () => { + const store = useToastStore() + store.show('Success toast', 'success', 0) + const wrapper = mount(ToastContainer, { + global: { + stubs: { AppIcon: true, Teleport: true }, + }, + }) + const toast = wrapper.find('[data-test="toast"]') + const accentBar = toast.find('.bg-green-500') + expect(accentBar.exists()).toBe(true) + }) +}) diff --git a/frontend/src/stores/__tests__/toast.test.js b/frontend/src/stores/__tests__/toast.test.js new file mode 100644 index 0000000..e661692 --- /dev/null +++ b/frontend/src/stores/__tests__/toast.test.js @@ -0,0 +1,64 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { setActivePinia, createPinia } from 'pinia' +import { useToastStore } from '../toast.js' + +beforeEach(() => { + setActivePinia(createPinia()) +}) + +afterEach(() => { + vi.useRealTimers() +}) + +describe('useToastStore', () => { + it('show(msg, type, duration) appends a toast with the given fields', () => { + const store = useToastStore() + store.show('Hi', 'success', 4000) + expect(store.toasts.length).toBe(1) + expect(store.toasts[0].message).toBe('Hi') + expect(store.toasts[0].type).toBe('success') + expect(store.toasts[0].duration).toBe(4000) + }) + + it('show defaults type to success and duration to 4000', () => { + const store = useToastStore() + store.show('Hi') + expect(store.toasts[0].type).toBe('success') + expect(store.toasts[0].duration).toBe(4000) + }) + + it('auto-dismisses after duration using fake timers', () => { + vi.useFakeTimers() + const store = useToastStore() + store.show('Hi', 'success', 4000) + expect(store.toasts.length).toBe(1) + vi.advanceTimersByTime(4000) + expect(store.toasts.length).toBe(0) + }) + + it('dismiss(id) removes the toast immediately', () => { + const store = useToastStore() + store.show('Hi', 'success', 0) + const id = store.toasts[0].id + store.dismiss(id) + expect(store.toasts.length).toBe(0) + }) + + it('duration=0 disables auto-dismiss', () => { + vi.useFakeTimers() + const store = useToastStore() + store.show('Hi', 'success', 0) + vi.advanceTimersByTime(10000) + expect(store.toasts.length).toBe(1) + }) + + it('multiple toasts stack with unique ids', () => { + const store = useToastStore() + store.show('A') + store.show('B') + store.show('C') + expect(store.toasts.length).toBe(3) + const ids = store.toasts.map(t => t.id) + expect(new Set(ids).size).toBe(3) + }) +})