Files
Seasoned/Seasoned.Frontend/test/GalleryPage.spec.ts
2026-03-19 21:23:18 +00:00

156 lines
4.1 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { nextTick } from 'vue'
import { ref } from 'vue'
import { createVuetify } from 'vuetify'
import { flushPromises } from '@vue/test-utils'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import GalleryPage from "@/pages/gallery.vue"
const vuetify = createVuetify({ components, directives })
global.ResizeObserver = class ResizeObserver {
observe() {} unobserve() {} disconnect() {}
};
global.visualViewport = {
width: 1024,
height: 768,
offsetLeft: 0,
offsetTop: 0,
pageLeft: 0,
pageTop: 0,
scale: 1,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
} as unknown as VisualViewport;
vi.stubGlobal('useRuntimeConfig', () => ({
public: { apiBase: 'http://localhost:5000/' }
}))
const mockFetch = vi.fn()
vi.stubGlobal('$fetch', mockFetch)
const mockNavigate = vi.fn()
vi.stubGlobal('navigateTo', mockNavigate)
vi.stubGlobal('useState', (key, init) => {
const state = ref(init ? init() : null)
return state
})
describe('GalleryPage.vue', () => {
beforeEach(() => {
vi.clearAllMocks()
mockFetch.mockResolvedValue([])
})
const mountOptions = {
global: { plugins: [vuetify] }
}
it('shows loading state initially and then renders recipes', async () => {
mockFetch.mockResolvedValueOnce([
{ id: 1, title: 'Bolognese', createdAt: new Date().toISOString(), ingredients: [], instructions: [] }
])
const wrapper = mount(GalleryPage, mountOptions)
expect(wrapper.text()).toContain('Opening Collection...')
await vi.waitFor(() => {
expect(wrapper.text()).toContain('Bolognese')
})
})
it('triggers semantic search when searchQuery changes', async () => {
vi.useFakeTimers()
mockFetch.mockResolvedValueOnce([])
const wrapper = mount(GalleryPage, mountOptions)
const vm = wrapper.vm as any
vm.searchQuery = 'spicy pasta'
await nextTick()
vi.advanceTimersByTime(600)
mockFetch.mockResolvedValueOnce([{ id: 2, title: 'Spicy Pasta' }])
await flushPromises()
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('api/recipe/search'),
expect.objectContaining({
query: { query: 'spicy pasta' },
credentials: 'include'
})
)
vi.useRealTimers()
})
it('redirects to login if API returns 401', async () => {
mockFetch.mockReset()
mockFetch.mockRejectedValue({ status: 401 })
mount(GalleryPage, mountOptions)
await flushPromises()
await vi.waitFor(() => {
expect(mockNavigate).toHaveBeenCalledWith('/login')
}, { timeout: 1000 })
})
it('enters editing mode and formats arrays into strings', async () => {
mockFetch.mockResolvedValueOnce([
{
id: 1,
title: 'Muffins',
ingredients: ['Flour', 'Sugar'],
instructions: ['Mix', 'Bake'],
createdAt: new Date().toISOString()
}
])
const wrapper = mount(GalleryPage, mountOptions)
await vi.waitFor(() => expect(wrapper.vm.recipes.length).toBe(1))
const vm = wrapper.vm as any
vm.editRecipe(vm.recipes[0])
expect(vm.isEditing).toBe(true)
expect(vm.selectedRecipe.ingredients).toBe('Flour\nSugar')
})
it('shoves updated recipe back to .NET API on saveChanges', async () => {
const mockRecipe = { id: 1, title: 'Old Title', ingredients: 'Water', instructions: 'Boil', createdAt: new Date().toISOString() }
mockFetch.mockResolvedValueOnce([mockRecipe])
const wrapper = mount(GalleryPage, mountOptions)
await vi.waitFor(() => expect(wrapper.vm.recipes.length).toBe(1))
const vm = wrapper.vm as any
vm.selectedRecipe = { ...mockRecipe, title: 'New Title' }
vm.isEditing = true
mockFetch.mockResolvedValueOnce({ success: true })
await vm.saveChanges()
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('api/recipe/update/1'),
expect.objectContaining({
method: 'PUT',
credentials: 'include'
})
)
expect(vm.recipes[0].title).toBe('New Title')
})
})