import { mount } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { Listing, TrustScore, Seller } from '../stores/search' import { useSearchStore } from '../stores/search' // ── Mock vue-router — ListingView reads route.params.id ────────────────────── const mockRouteId = { value: 'test-listing-id' } vi.mock('vue-router', () => ({ useRoute: () => ({ params: { id: mockRouteId.value } }), RouterLink: { template: '' }, })) // ── Helpers ────────────────────────────────────────────────────────────────── function makeListing(id: string, overrides: Partial = {}): Listing { return { id: null, platform: 'ebay', platform_listing_id: id, title: 'NVIDIA RTX 4090 24GB — Used Excellent', price: 849.99, currency: 'USD', condition: 'used_excellent', seller_platform_id: 'seller1', url: 'https://ebay.com/itm/test', photo_urls: ['https://example.com/img.jpg'], listing_age_days: 3, buying_format: 'fixed_price', ends_at: null, fetched_at: null, trust_score_id: null, ...overrides, } } function makeTrust(score: number, flags: string[] = [], partial = false): TrustScore { return { id: null, listing_id: 1, composite_score: score, account_age_score: 18, feedback_count_score: 20, feedback_ratio_score: 20, price_vs_market_score: 15, category_history_score: 14, photo_hash_duplicate: false, photo_analysis_json: null, red_flags_json: JSON.stringify(flags), score_is_partial: partial, scored_at: null, } } function makeSeller(overrides: Partial = {}): Seller { return { id: null, platform: 'ebay', platform_seller_id: 'seller1', username: 'techdeals_rog', account_age_days: 720, feedback_count: 4711, feedback_ratio: 0.997, category_history_json: '{}', fetched_at: null, ...overrides, } } async function mountView(storeSetup?: (store: ReturnType) => void) { setActivePinia(createPinia()) const store = useSearchStore() if (storeSetup) storeSetup(store) const { default: ListingView } = await import('../views/ListingView.vue') return mount(ListingView, { global: { plugins: [] }, }) } // ── Tests ──────────────────────────────────────────────────────────────────── describe('ListingView — not found', () => { beforeEach(() => { mockRouteId.value = 'missing-id' sessionStorage.clear() }) it('shows not-found state when listing is absent from store', async () => { const wrapper = await mountView() expect(wrapper.text()).toContain('Listing not found') expect(wrapper.text()).toContain('Return to search') }) it('does not render the trust section when listing is absent', async () => { const wrapper = await mountView() expect(wrapper.find('.lv-trust').exists()).toBe(false) }) }) describe('ListingView — listing present', () => { const ID = 'test-listing-id' beforeEach(() => { mockRouteId.value = ID sessionStorage.clear() }) it('renders the listing title', async () => { const wrapper = await mountView(store => { store.results.push(makeListing(ID)) store.trustScores.set(ID, makeTrust(85)) store.sellers.set('seller1', makeSeller()) }) expect(wrapper.text()).toContain('NVIDIA RTX 4090 24GB') }) it('renders the formatted price', async () => { const wrapper = await mountView(store => { store.results.push(makeListing(ID)) store.trustScores.set(ID, makeTrust(85)) }) expect(wrapper.text()).toContain('$849.99') }) it('shows the composite trust score in the ring', async () => { const wrapper = await mountView(store => { store.results.push(makeListing(ID)) store.trustScores.set(ID, makeTrust(72)) }) expect(wrapper.find('.lv-ring__score').text()).toBe('72') }) it('renders all five signal rows in the table', async () => { const wrapper = await mountView(store => { store.results.push(makeListing(ID)) store.trustScores.set(ID, makeTrust(80)) store.sellers.set('seller1', makeSeller()) }) const rows = wrapper.findAll('.lv-signals__row') expect(rows).toHaveLength(5) }) it('shows score values in signal table', async () => { const wrapper = await mountView(store => { store.results.push(makeListing(ID)) store.trustScores.set(ID, makeTrust(80)) store.sellers.set('seller1', makeSeller()) }) // feedback_count_score = 20 expect(wrapper.text()).toContain('20 / 20') }) it('shows seller username', async () => { const wrapper = await mountView(store => { store.results.push(makeListing(ID)) store.trustScores.set(ID, makeTrust(80)) store.sellers.set('seller1', makeSeller({ username: 'gpu_warehouse' })) }) expect(wrapper.text()).toContain('gpu_warehouse') }) }) describe('ListingView — red flags', () => { const ID = 'test-listing-id' beforeEach(() => { mockRouteId.value = ID sessionStorage.clear() }) it('renders hard flag badge for new_account', async () => { const wrapper = await mountView(store => { store.results.push(makeListing(ID)) store.trustScores.set(ID, makeTrust(40, ['new_account'])) }) const flags = wrapper.findAll('.lv-flag--hard') expect(flags.length).toBeGreaterThan(0) expect(wrapper.text()).toContain('New account') }) it('renders soft flag badge for scratch_dent_mentioned', async () => { const wrapper = await mountView(store => { store.results.push(makeListing(ID)) store.trustScores.set(ID, makeTrust(55, ['scratch_dent_mentioned'])) }) const flags = wrapper.findAll('.lv-flag--soft') expect(flags.length).toBeGreaterThan(0) expect(wrapper.text()).toContain('Damage mentioned') }) it('shows no flag badges when red_flags_json is empty', async () => { const wrapper = await mountView(store => { store.results.push(makeListing(ID)) store.trustScores.set(ID, makeTrust(90, [])) }) expect(wrapper.find('.lv-flag').exists()).toBe(false) }) it('applies triple-red class when account + price + photo flags all present', async () => { const wrapper = await mountView(store => { store.results.push(makeListing(ID)) store.trustScores.set(ID, makeTrust(12, [ 'new_account', 'suspicious_price', 'duplicate_photo', ])) }) expect(wrapper.find('.lv-layout--triple-red').exists()).toBe(true) }) it('does not apply triple-red class when only two flag categories present', async () => { const wrapper = await mountView(store => { store.results.push(makeListing(ID)) store.trustScores.set(ID, makeTrust(30, ['new_account', 'suspicious_price'])) }) expect(wrapper.find('.lv-layout--triple-red').exists()).toBe(false) }) }) describe('ListingView — partial/pending signals', () => { const ID = 'test-listing-id' beforeEach(() => { mockRouteId.value = ID sessionStorage.clear() }) it('shows pending for account age when seller.account_age_days is null', async () => { const wrapper = await mountView(store => { store.results.push(makeListing(ID)) store.trustScores.set(ID, makeTrust(60, [], true)) store.sellers.set('seller1', makeSeller({ account_age_days: null })) }) expect(wrapper.text()).toContain('pending') }) it('shows partial warning text when score_is_partial is true', async () => { const wrapper = await mountView(store => { store.results.push(makeListing(ID)) store.trustScores.set(ID, makeTrust(60, [], true)) store.sellers.set('seller1', makeSeller({ account_age_days: null })) }) expect(wrapper.find('.lv-verdict__partial').exists()).toBe(true) }) }) describe('ListingView — ring colour class', () => { const ID = 'test-listing-id' beforeEach(() => { mockRouteId.value = ID sessionStorage.clear() }) it('applies lv-ring--high for score >= 80', async () => { const wrapper = await mountView(store => { store.results.push(makeListing(ID)) store.trustScores.set(ID, makeTrust(82)) }) expect(wrapper.find('.lv-ring--high').exists()).toBe(true) }) it('applies lv-ring--mid for score 50–79', async () => { const wrapper = await mountView(store => { store.results.push(makeListing(ID)) store.trustScores.set(ID, makeTrust(63)) }) expect(wrapper.find('.lv-ring--mid').exists()).toBe(true) }) it('applies lv-ring--low for score < 50', async () => { const wrapper = await mountView(store => { store.results.push(makeListing(ID)) store.trustScores.set(ID, makeTrust(22)) }) expect(wrapper.find('.lv-ring--low').exists()).toBe(true) }) })