Newer
Older
navi-1 / webclient / tests / unit / components / ui / Form.test.js
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import Form from '@/components/ui/Form.vue'

const samplePayload = {
  form_id: 'form_abc',
  title: 'Preferences',
  description: 'Tell us what you need.',
  fields: [
    { name: 'email', label: 'Email', type: 'email', required: true },
    { name: 'budget', label: 'Budget', type: 'number', required: true, min: 0, max: 1000000 },
    { name: 'notes', label: 'Notes', type: 'textarea' },
  ],
  submit_label: 'Send',
}

function mountWithProvide(payload = samplePayload, sendFn = vi.fn()) {
  return mount(Form, {
    props: { data: payload },
    global: {
      provide: {
        wsSend: sendFn,
      },
      mocks: {
        // localStorage is not available in jsdom by default; provide stubs.
        localStorage: {
          getItem: vi.fn(() => '[]'),
          setItem: vi.fn(),
        },
      },
    },
  })
}

vi.mock('@/stores/sessions.js', () => ({
  useSessionsStore: () => ({
    updatePreview: vi.fn(),
    updateName: vi.fn(),
    sessions: [],
  }),
}))

vi.mock('@/api/index.js', () => ({
  getSession: vi.fn(),
  getFeedback: vi.fn().mockResolvedValue({ feedback: [] }),
  setFeedback: vi.fn(),
  generateSessionName: vi.fn(),
}))

describe('Form', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
    vi.stubGlobal('localStorage', {
      getItem: vi.fn(() => '[]'),
      setItem: vi.fn(),
    })
  })

  it('renders title, description and fields', () => {
    const wrapper = mountWithProvide()
    expect(wrapper.text()).toContain('Preferences')
    expect(wrapper.text()).toContain('Tell us what you need.')
    expect(wrapper.find('input[type="email"]').exists()).toBe(true)
    expect(wrapper.find('input[type="number"]').exists()).toBe(true)
    expect(wrapper.find('textarea').exists()).toBe(true)
  })

  it('submit button is disabled while form is invalid', () => {
    const wrapper = mountWithProvide()
    const btn = wrapper.find('button[type="submit"]')
    expect(btn.attributes('disabled')).toBeDefined()
  })

  it('enables submit button when required fields are valid', async () => {
    const wrapper = mountWithProvide()
    const email = wrapper.find('input[type="email"]')
    const budget = wrapper.find('input[type="number"]')

    await email.setValue('test@example.com')
    await email.trigger('input')
    await budget.setValue('50000')
    await budget.trigger('input')
    await flushPromises()

    const btn = wrapper.find('button[type="submit"]')
    expect(btn.attributes('disabled')).toBeUndefined()
  })

  it('shows validation error for invalid email in real time', async () => {
    const wrapper = mountWithProvide()
    const email = wrapper.find('input[type="email"]')

    await email.setValue('bad-email')
    await email.trigger('input')
    await email.trigger('blur')
    await flushPromises()

    expect(wrapper.text()).toContain('valid email')
  })

  it('sends form_submit payload and switches to summary on submit', async () => {
    const sendFn = vi.fn()
    const wrapper = mountWithProvide(samplePayload, sendFn)

    const email = wrapper.find('input[type="email"]')
    const budget = wrapper.find('input[type="number"]')

    await email.setValue('test@example.com')
    await email.trigger('input')
    await budget.setValue('50000')
    await budget.trigger('input')
    await flushPromises()

    await wrapper.find('form').trigger('submit')
    await flushPromises()

    expect(sendFn).toHaveBeenCalledTimes(1)
    const payload = sendFn.mock.calls[0][0]
    expect(payload.type).toBe('form_submit')
    expect(payload.form_id).toBe('form_abc')
    expect(payload.values.email).toBe('test@example.com')
    expect(payload.values.budget).toBe(50000)

    expect(wrapper.text()).toContain('Submitted')
    expect(wrapper.text()).toContain('test@example.com')
    expect(wrapper.text()).toContain('50000')
  })

  it('prevents double submission', async () => {
    const sendFn = vi.fn()
    const wrapper = mountWithProvide(samplePayload, sendFn)

    const email = wrapper.find('input[type="email"]')
    const budget = wrapper.find('input[type="number"]')
    await email.setValue('test@example.com')
    await email.trigger('input')
    await budget.setValue('50000')
    await budget.trigger('input')
    await flushPromises()

    await wrapper.find('form').trigger('submit')
    await flushPromises()
    // After submit the form DOM is replaced by the read-only summary, so a
    // second submit attempt is impossible.
    expect(wrapper.find('form').exists()).toBe(false)
    expect(sendFn).toHaveBeenCalledTimes(1)
  })
})