W3: add 3 new vitest tests (wizard routing, nudge badge render, red flag tooltip); update existing test mocks for new API functions
This commit is contained in:
parent
f43ede9e07
commit
1e55c11dc9
6 changed files with 214 additions and 12 deletions
|
|
@ -1,33 +1,51 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import App from '@/App.vue'
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
getProfile: vi.fn().mockResolvedValue({ id: 'p1', full_name: 'Test User', email: '', phone: '', location: '', headline: '', summary: '', languages: [], hard_rules: {} }),
|
||||
HttpError: class HttpError extends Error {
|
||||
status: number
|
||||
body: unknown
|
||||
constructor(status: number, body: unknown, msg?: string) {
|
||||
super(msg ?? `HTTP ${status}`)
|
||||
this.status = status
|
||||
this.body = body
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
function makeRouter() {
|
||||
return createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', redirect: '/cv' },
|
||||
{ path: '/', redirect: '/today' },
|
||||
{ path: '/today', name: 'today', component: { template: '<div>Today</div>' } },
|
||||
{ path: '/cv', name: 'cv', component: { template: '<div>CV</div>' } },
|
||||
{ path: '/research', name: 'research', component: { template: '<div>Research</div>' } },
|
||||
{ path: '/applications', name: 'applications', component: { template: '<div>Applications</div>' } },
|
||||
{ path: '/applications/:id', name: 'application-detail', component: { template: '<div>Detail</div>' } }
|
||||
{ path: '/applications/:id', name: 'application-detail', component: { template: '<div>Detail</div>' } },
|
||||
{ path: '/welcome', name: 'welcome', component: { template: '<div>Welcome</div>' } }
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
describe('Router tabs', () => {
|
||||
it('renders all three tab links', async () => {
|
||||
it('renders all four tab links', async () => {
|
||||
setActivePinia(createPinia())
|
||||
const router = makeRouter()
|
||||
await router.push('/cv')
|
||||
await router.push('/today')
|
||||
await router.isReady()
|
||||
const wrapper = mount(App, { global: { plugins: [router] } })
|
||||
await vi.waitFor(() => {
|
||||
const links = wrapper.findAll('nav a')
|
||||
expect(links).toHaveLength(3)
|
||||
expect(links[0].text()).toBe('CV')
|
||||
expect(links[1].text()).toBe('Research')
|
||||
expect(links[2].text()).toBe('Applications')
|
||||
expect(links).toHaveLength(4)
|
||||
expect(links[0].text()).toBe('Today')
|
||||
expect(links[1].text()).toBe('CV')
|
||||
expect(links[2].text()).toBe('Research')
|
||||
expect(links[3].text()).toBe('Applications')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -12,6 +12,7 @@ vi.mock('@/api', () => ({
|
|||
confirmApproval: vi.fn(),
|
||||
rejectApproval: vi.fn(),
|
||||
outboxSend: vi.fn(),
|
||||
interviewPrep: vi.fn(),
|
||||
HttpError: class HttpError extends Error {
|
||||
status: number
|
||||
body: unknown
|
||||
|
|
|
|||
112
apps/web/src/views/Applications.badges.test.ts
Normal file
112
apps/web/src/views/Applications.badges.test.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import Applications from '@/views/Applications.vue'
|
||||
import type { Application } from '@/types'
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
getApplications: vi.fn(),
|
||||
transitionApplication: vi.fn(),
|
||||
batchScore: vi.fn(),
|
||||
getToday: vi.fn(),
|
||||
HttpError: class HttpError extends Error {
|
||||
status: number
|
||||
body: unknown
|
||||
constructor(status: number, body: unknown, msg?: string) {
|
||||
super(msg ?? `HTTP ${status}`)
|
||||
this.status = status
|
||||
this.body = body
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
function makeApp(id: string, state: string, company: string): Application {
|
||||
return {
|
||||
id,
|
||||
job_posting_id: 'j-' + id,
|
||||
state: state as Application['state'],
|
||||
score: 80,
|
||||
score_rationale: null,
|
||||
notes: '',
|
||||
state_changed_at: '2026-01-01T00:00:00Z',
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
posting: {
|
||||
id: 'j-' + id, source: 'manual_url', external_id: null, url: 'http://x',
|
||||
company, title: 'Engineer', location: 'Remote', description: '',
|
||||
raw: {}, fetched_at: '2026-01-01T00:00:00Z'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('Applications kanban badges', () => {
|
||||
it('renders nudge dot on cards that have a follow-up nudge', async () => {
|
||||
setActivePinia(createPinia())
|
||||
const api = await import('@/api')
|
||||
|
||||
const apps = [makeApp('app-1', 'sent', 'NudgeCorp'), makeApp('app-2', 'discovered', 'NoNudge')]
|
||||
;(api.getApplications as ReturnType<typeof vi.fn>).mockResolvedValue(apps)
|
||||
;(api.batchScore as ReturnType<typeof vi.fn>).mockResolvedValue({ results: [] })
|
||||
;(api.getToday as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
digest: [],
|
||||
nudges: [
|
||||
{ application_id: 'app-1', days_since_sent: 9, suggestion: 'Send a follow-up email' }
|
||||
],
|
||||
pending_approvals: 0
|
||||
})
|
||||
|
||||
const wrapper = mount(Applications)
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain('NudgeCorp')
|
||||
})
|
||||
|
||||
// The nudge dot should be rendered as an orange dot (span with bg-orange-500)
|
||||
const dots = wrapper.findAll('.bg-orange-500')
|
||||
expect(dots.length).toBeGreaterThanOrEqual(1)
|
||||
|
||||
// Verify the dot is in the card for NudgeCorp (the sent column)
|
||||
const sentCol = wrapper.findAll('.font-semibold').find((el) => el.text() === 'sent')
|
||||
expect(sentCol).toBeTruthy()
|
||||
const sentColumn = sentCol!.element.parentElement!
|
||||
expect(sentColumn.textContent).toContain('NudgeCorp')
|
||||
// The dot should be inside this column
|
||||
expect(sentColumn.querySelector('.bg-orange-500')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders red flag badge with tooltip text from batch scoring results', async () => {
|
||||
setActivePinia(createPinia())
|
||||
const api = await import('@/api')
|
||||
|
||||
const apps = [makeApp('app-1', 'discovered', 'ScamCorp')]
|
||||
;(api.getApplications as ReturnType<typeof vi.fn>).mockResolvedValue(apps)
|
||||
;(api.batchScore as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
results: [
|
||||
{
|
||||
application_id: 'app-1',
|
||||
score: 30,
|
||||
rationale: {},
|
||||
red_flags: ['Unpaid trial period mentioned', 'Asks for bank details upfront']
|
||||
}
|
||||
]
|
||||
})
|
||||
;(api.getToday as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
digest: [],
|
||||
nudges: [],
|
||||
pending_approvals: 0
|
||||
})
|
||||
|
||||
const wrapper = mount(Applications)
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).toContain('ScamCorp')
|
||||
})
|
||||
|
||||
// The warning symbol should be rendered in the card
|
||||
const warningEl = wrapper.find('.text-red-600.font-bold')
|
||||
expect(warningEl.exists()).toBe(true)
|
||||
|
||||
// The title attribute should contain the red flag text
|
||||
const title = warningEl.attributes('title')
|
||||
expect(title).toBeTruthy()
|
||||
expect(title).toContain('Unpaid trial period mentioned')
|
||||
expect(title).toContain('Asks for bank details upfront')
|
||||
})
|
||||
})
|
||||
|
|
@ -7,6 +7,8 @@ import type { Application } from '@/types'
|
|||
vi.mock('@/api', () => ({
|
||||
getApplications: vi.fn(),
|
||||
transitionApplication: vi.fn(),
|
||||
batchScore: vi.fn().mockResolvedValue({ results: [] }),
|
||||
getToday: vi.fn().mockResolvedValue({ digest: [], nudges: [], pending_approvals: 0 }),
|
||||
HttpError: class HttpError extends Error {
|
||||
status: number
|
||||
body: unknown
|
||||
|
|
|
|||
70
apps/web/src/views/Welcome.test.ts
Normal file
70
apps/web/src/views/Welcome.test.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import Welcome from '@/views/Welcome.vue'
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
importCv: vi.fn(),
|
||||
confirmCvImport: vi.fn(),
|
||||
fetchPostings: vi.fn(),
|
||||
HttpError: class HttpError extends Error {
|
||||
status: number
|
||||
body: unknown
|
||||
constructor(status: number, body: unknown, msg?: string) {
|
||||
super(msg ?? `HTTP ${status}`)
|
||||
this.status = status
|
||||
this.body = body
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
function makeRouter() {
|
||||
return createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/welcome', name: 'welcome', component: Welcome },
|
||||
{ path: '/today', name: 'today', component: { template: '<div>Today</div>' } }
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
describe('Onboarding wizard', () => {
|
||||
it('shows welcome step and advances through steps to finish', async () => {
|
||||
setActivePinia(createPinia())
|
||||
const router = makeRouter()
|
||||
await router.push('/welcome')
|
||||
await router.isReady()
|
||||
|
||||
const wrapper = mount(Welcome, { global: { plugins: [router] } })
|
||||
|
||||
// Step 0: Welcome
|
||||
expect(wrapper.text()).toContain('Welcome to Jobhunt')
|
||||
expect(wrapper.text()).toContain('Get Started')
|
||||
|
||||
// Advance to step 1 (Import CV)
|
||||
const getStartedBtn = wrapper.find('button')
|
||||
await getStartedBtn.trigger('click')
|
||||
expect(wrapper.text()).toContain('Import Your CV')
|
||||
|
||||
// Skip import -> step 2 (Fetch Postings)
|
||||
const skipLink = wrapper.findAll('button').find((b) => b.text().includes('Skip for now'))
|
||||
expect(skipLink).toBeTruthy()
|
||||
await skipLink!.trigger('click')
|
||||
expect(wrapper.text()).toContain('Fetch Job Postings')
|
||||
|
||||
// Continue -> step 3 (Done)
|
||||
const continueBtn = wrapper.findAll('button').find((b) => b.text().includes('Continue'))
|
||||
expect(continueBtn).toBeTruthy()
|
||||
await continueBtn!.trigger('click')
|
||||
expect(wrapper.text()).toContain('You are all set')
|
||||
|
||||
// Finish -> navigates to /today
|
||||
const finishBtn = wrapper.findAll('button').find((b) => b.text().includes('Go to Today'))
|
||||
expect(finishBtn).toBeTruthy()
|
||||
await finishBtn!.trigger('click')
|
||||
await vi.waitFor(() => {
|
||||
expect(router.currentRoute.value.path).toBe('/today')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useToastStore } from '@/stores/toast'
|
||||
import * as api from '@/api'
|
||||
|
|
@ -11,7 +11,6 @@ const router = useRouter()
|
|||
|
||||
const step = ref(0)
|
||||
const steps = ['Welcome', 'Import CV', 'Fetch Postings', 'Done']
|
||||
const stepTitle = computed(() => steps[step.value])
|
||||
|
||||
// Step 1: Import CV
|
||||
const selectedFile = ref<File | null>(null)
|
||||
|
|
|
|||
Loading…
Reference in a new issue