diff --git a/web/src/App.vue b/web/src/App.vue index 58b0f21..8f15aa8 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -1,30 +1,39 @@ - - - diff --git a/web/src/views/LabelView.test.ts b/web/src/views/LabelView.test.ts new file mode 100644 index 0000000..35a4c35 --- /dev/null +++ b/web/src/views/LabelView.test.ts @@ -0,0 +1,46 @@ +import { mount } from '@vue/test-utils' +import { createPinia, setActivePinia } from 'pinia' +import LabelView from './LabelView.vue' +import { describe, it, expect, vi, beforeEach } from 'vitest' + +// Mock fetch globally +beforeEach(() => { + setActivePinia(createPinia()) + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ items: [], total: 0 }), + text: async () => '', + })) +}) + +describe('LabelView', () => { + it('shows loading state initially', () => { + const w = mount(LabelView, { + global: { plugins: [createPinia()] }, + }) + // Should show skeleton while loading + expect(w.find('.skeleton-card').exists()).toBe(true) + }) + + it('shows empty state when queue is empty after load', async () => { + const w = mount(LabelView, { + global: { plugins: [createPinia()] }, + }) + // Let all promises resolve + await new Promise(r => setTimeout(r, 0)) + await w.vm.$nextTick() + expect(w.find('.empty-state').exists()).toBe(true) + }) + + it('renders header with action buttons', async () => { + const w = mount(LabelView, { + global: { plugins: [createPinia()] }, + }) + await new Promise(r => setTimeout(r, 0)) + await w.vm.$nextTick() + expect(w.find('.lv-header').exists()).toBe(true) + expect(w.text()).toContain('Undo') + expect(w.text()).toContain('Skip') + expect(w.text()).toContain('Discard') + }) +}) diff --git a/web/src/views/LabelView.vue b/web/src/views/LabelView.vue new file mode 100644 index 0000000..de15f30 --- /dev/null +++ b/web/src/views/LabelView.vue @@ -0,0 +1,318 @@ + + + + +