feat(defects): file a defect via a board-section modal (T5)
POST /api/defects validates the body with arktype (known section, uuid project, non-empty trimmed verbatim) and resolves the reporter through a dev seam (getReporter, ADR 0002) until OAuth lands. DefectForm is a DaisyUI modal opened by a section click — pick a project, describe the problem, submit.
This commit is contained in:
80
tests/defect-filing.spec.ts
Normal file
80
tests/defect-filing.spec.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'vitest'
|
||||
import { type } from 'arktype'
|
||||
import { setupTestDb, truncateAll, type TestDb } from './helpers/db'
|
||||
import { createProject } from '../server/db/repositories/projects'
|
||||
import { createDefect, listRecentDefects } from '../server/db/repositories/defects'
|
||||
import { DefectInput } from '../server/utils/defectInput'
|
||||
|
||||
const UUID = '3f0a27a7-81c0-43fa-9b42-c2864ddc569b'
|
||||
|
||||
describe('defect input validation', () => {
|
||||
it('accepts a valid defect and trims the verbatim', () => {
|
||||
const result = DefectInput({
|
||||
sectionId: 'macroplan',
|
||||
projectId: UUID,
|
||||
verbatim: ' builds are flaky ',
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
sectionId: 'macroplan',
|
||||
projectId: UUID,
|
||||
verbatim: 'builds are flaky',
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects an unknown section id', () => {
|
||||
const result = DefectInput({ sectionId: 'not-a-section', projectId: UUID, verbatim: 'x' })
|
||||
expect(result instanceof type.errors).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects empty / whitespace-only verbatim', () => {
|
||||
const result = DefectInput({ sectionId: 'macroplan', projectId: UUID, verbatim: ' ' })
|
||||
expect(result instanceof type.errors).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects a non-uuid project id', () => {
|
||||
const result = DefectInput({ sectionId: 'macroplan', projectId: 'nope', verbatim: 'x' })
|
||||
expect(result instanceof type.errors).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('defects repository', () => {
|
||||
let db: TestDb
|
||||
let pool: { end: () => Promise<void> }
|
||||
|
||||
beforeAll(async () => {
|
||||
const setup = await setupTestDb()
|
||||
db = setup.db
|
||||
pool = setup.pool
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await pool.end()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await truncateAll(db)
|
||||
})
|
||||
|
||||
it('persists a filed defect with section, project, verbatim and reporter', async () => {
|
||||
const project = await createProject(db, 'Acme')
|
||||
const row = await createDefect(db, {
|
||||
sectionId: 'macroplan',
|
||||
projectId: project.id,
|
||||
verbatim: 'login is flaky',
|
||||
reporterEmail: 'dev@theodo.com',
|
||||
})
|
||||
|
||||
expect(row).toMatchObject({
|
||||
sectionId: 'macroplan',
|
||||
projectId: project.id,
|
||||
verbatim: 'login is flaky',
|
||||
reporterEmail: 'dev@theodo.com',
|
||||
})
|
||||
expect(row.id).toBeDefined()
|
||||
expect(row.createdAt).toBeInstanceOf(Date)
|
||||
|
||||
const recent = await listRecentDefects(db)
|
||||
expect(recent).toHaveLength(1)
|
||||
expect(recent[0]!.id).toBe(row.id)
|
||||
})
|
||||
})
|
||||
73
tests/defect-form.nuxt.spec.ts
Normal file
73
tests/defect-form.nuxt.spec.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// @vitest-environment nuxt
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime'
|
||||
import { flushPromises } from '@vue/test-utils'
|
||||
import { defineEventHandler, readBody } from 'h3'
|
||||
import DefectForm from '../app/components/DefectForm.vue'
|
||||
|
||||
let filed: { sectionId: string; projectId: string; verbatim: string } | null
|
||||
|
||||
beforeEach(() => {
|
||||
filed = null
|
||||
registerEndpoint(
|
||||
'/api/projects',
|
||||
defineEventHandler(() => [{ id: 'p1', name: 'Acme' }]),
|
||||
)
|
||||
registerEndpoint(
|
||||
'/api/defects',
|
||||
defineEventHandler(async (event) => {
|
||||
filed = await readBody(event)
|
||||
return { id: 'd1', ...filed, reporterEmail: 'dev@theodo.com' }
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
async function pickProject(wrapper: Awaited<ReturnType<typeof mountSuspended>>) {
|
||||
await wrapper.find('[data-test="project-input"]').trigger('focus')
|
||||
await wrapper.find('[data-test="project-option"]').trigger('click')
|
||||
}
|
||||
|
||||
describe('DefectForm', () => {
|
||||
it('opens for the clicked section and shows its name', async () => {
|
||||
const wrapper = await mountSuspended(DefectForm, { props: { sectionId: 'macroplan' } })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('dialog').classes()).toContain('modal-open')
|
||||
expect(wrapper.text()).toContain('Macroplan')
|
||||
})
|
||||
|
||||
it('is closed when no section is selected', async () => {
|
||||
const wrapper = await mountSuspended(DefectForm, { props: { sectionId: null } })
|
||||
expect(wrapper.find('dialog').classes()).not.toContain('modal-open')
|
||||
})
|
||||
|
||||
it('cannot submit until both a project and verbatim are provided', async () => {
|
||||
const wrapper = await mountSuspended(DefectForm, { props: { sectionId: 'macroplan' } })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('[data-test="submit"]').attributes('disabled')).toBeDefined()
|
||||
|
||||
await pickProject(wrapper)
|
||||
// project but no verbatim → still blocked
|
||||
expect(wrapper.find('[data-test="submit"]').attributes('disabled')).toBeDefined()
|
||||
|
||||
await wrapper.find('[data-test="verbatim"]').setValue('builds are flaky')
|
||||
expect(wrapper.find('[data-test="submit"]').attributes('disabled')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('files the defect with the section, chosen project and verbatim, then emits filed', async () => {
|
||||
const wrapper = await mountSuspended(DefectForm, { props: { sectionId: 'macroplan' } })
|
||||
await flushPromises()
|
||||
|
||||
await pickProject(wrapper)
|
||||
await wrapper.find('[data-test="verbatim"]').setValue('builds are flaky')
|
||||
await wrapper.find('form').trigger('submit')
|
||||
|
||||
await vi.waitFor(() => expect(wrapper.emitted('filed')).toBeTruthy())
|
||||
expect(filed).toEqual({
|
||||
sectionId: 'macroplan',
|
||||
projectId: 'p1',
|
||||
verbatim: 'builds are flaky',
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user