import { beforeEach, describe, expect, it, vi } from 'vitest'; import { mount } from '@vue/test-utils'; vi.mock('@inertiajs/vue3', async () => import('../stubs/inertia.js')); import PromptForm from '../../../resources/js/Components/PromptForm.vue'; import { inertiaMock, resetInertiaMock, resolvePendingSubmissions } from '../stubs/inertia.js'; const baseProps = () => ({ initial: { name: 'Ada', description: 'Greeter', body: 'Hi {{ name }}', tags: ['greeting'] }, suggestedTags: ['greeting', 'chat', 'code'], method: 'post', action: '/prompts', submitLabel: 'Create prompt', cancelHref: '/prompts', }); const removeButtons = (wrapper) => wrapper.findAll('[aria-label^="Remove tag"]'); describe('PromptForm', () => { beforeEach(() => { resetInertiaMock(); }); it('pre-fills name, description, body, and tags from initial', () => { const wrapper = mount(PromptForm, { props: baseProps() }); expect(wrapper.find('#prompt-name').element.value).toBe('Ada'); expect(wrapper.find('#prompt-description').element.value).toBe('Greeter'); expect(wrapper.find('#prompt-body').element.value).toBe('Hi {{ name }}'); const buttons = removeButtons(wrapper); expect(buttons).toHaveLength(1); expect(buttons[0].attributes('aria-label')).toBe('Remove tag greeting'); }); it('treats missing initial values as empty', () => { const wrapper = mount(PromptForm, { props: { ...baseProps(), initial: {} } }); expect(wrapper.find('#prompt-name').element.value).toBe(''); expect(wrapper.find('#prompt-description').element.value).toBe(''); expect(removeButtons(wrapper)).toHaveLength(0); }); it('suggests only tags not already added', () => { const wrapper = mount(PromptForm, { props: baseProps() }); const suggestions = wrapper .findAll('button') .filter((button) => button.text().startsWith('+ ')) .map((button) => button.text()); expect(suggestions).toEqual(['+ chat', '+ code']); }); it('adds a trimmed, lowercase tag on Enter and clears the input', async () => { const wrapper = mount(PromptForm, { props: baseProps() }); const input = wrapper.find('#prompt-tags'); await input.setValue(' Code Review '); await input.trigger('keydown', { key: 'Enter' }); expect(input.element.value).toBe(''); expect(removeButtons(wrapper).map((b) => b.attributes('aria-label'))).toContain( 'Remove tag code review' ); }); it('adds a tag on comma and commits a pending tag on blur', async () => { const wrapper = mount(PromptForm, { props: baseProps() }); const input = wrapper.find('#prompt-tags'); await input.setValue('notes'); await input.trigger('keydown', { key: ',' }); expect(input.element.value).toBe(''); expect(removeButtons(wrapper)).toHaveLength(2); await input.setValue('solo'); await input.trigger('blur'); expect(removeButtons(wrapper).map((b) => b.attributes('aria-label'))).toContain('Remove tag solo'); }); it('ignores empty and duplicate tags', async () => { const wrapper = mount(PromptForm, { props: baseProps() }); const input = wrapper.find('#prompt-tags'); await input.setValue(' '); await input.trigger('keydown', { key: 'Enter' }); expect(removeButtons(wrapper)).toHaveLength(1); const suggestion = wrapper .findAll('button') .find((button) => button.text() === '+ chat'); await suggestion.trigger('click'); expect(removeButtons(wrapper)).toHaveLength(2); await suggestion.trigger('click'); expect(removeButtons(wrapper)).toHaveLength(2); }); it('removes the last tag on Backspace with an empty input', async () => { const wrapper = mount(PromptForm, { props: baseProps() }); const input = wrapper.find('#prompt-tags'); await input.setValue('draft'); await input.trigger('keydown', { key: 'Backspace' }); expect(removeButtons(wrapper)).toHaveLength(1); await input.setValue(''); await input.trigger('keydown', { key: 'Backspace' }); expect(removeButtons(wrapper)).toHaveLength(0); }); it('removes a tag via its remove button', async () => { const wrapper = mount(PromptForm, { props: baseProps() }); await removeButtons(wrapper)[0].trigger('click'); expect(removeButtons(wrapper)).toHaveLength(0); }); it('shows server errors from the Inertia page (array and string shapes)', async () => { const wrapper = mount(PromptForm, { props: baseProps() }); inertiaMock.page.props.errors = { name: 'The name is required.', description: 'Too long.', body: 'The body is required.', tags: ['Each tag may be up to 40 characters.'], }; await wrapper.vm.$nextTick(); expect(wrapper.text()).toContain('The name is required.'); expect(wrapper.text()).toContain('Too long.'); expect(wrapper.text()).toContain('The body is required.'); expect(wrapper.text()).toContain('Each tag may be up to 40 characters.'); inertiaMock.page.props.errors = { tags: 'Tags must be strings.' }; await wrapper.vm.$nextTick(); expect(wrapper.text()).toContain('Tags must be strings.'); }); it('submits via POST with the transformed payload (trimmed/null description, pending tag)', async () => { const wrapper = mount(PromptForm, { props: baseProps() }); await wrapper.find('#prompt-description').setValue(' '); await wrapper.find('#prompt-tags').setValue('new one'); await wrapper.find('form').trigger('submit'); const submission = inertiaMock.formSubmissions[0]; expect(submission.method).toBe('post'); expect(submission.url).toBe('/prompts'); expect(submission.data).toEqual({ name: 'Ada', description: null, body: 'Hi {{ name }}', tags: ['greeting', 'new one'], }); }); it('keeps a non-empty description trimmed when submitting', async () => { const wrapper = mount(PromptForm, { props: baseProps() }); await wrapper.find('#prompt-description').setValue(' Trimmed '); await wrapper.find('form').trigger('submit'); expect(inertiaMock.formSubmissions[0].data.description).toBe('Trimmed'); }); it('submits via PUT when method is "put"', async () => { const wrapper = mount(PromptForm, { props: { ...baseProps(), method: 'put', action: '/prompts/7', submitLabel: 'Save changes', cancelHref: '/prompts/7', }, }); await wrapper.find('form').trigger('submit'); const submission = inertiaMock.formSubmissions[0]; expect(submission.method).toBe('put'); expect(submission.url).toBe('/prompts/7'); }); it('shows "Saving…" and disables the button while processing', async () => { inertiaMock.deferSubmissions = true; const wrapper = mount(PromptForm, { props: baseProps() }); const button = () => wrapper.find('button[type="submit"]'); await wrapper.find('form').trigger('submit'); expect(inertiaMock.forms[0].processing).toBe(true); await wrapper.vm.$nextTick(); expect(button().text()).toBe('Saving…'); expect(button().attributes('disabled')).toBeDefined(); resolvePendingSubmissions(); await wrapper.vm.$nextTick(); expect(inertiaMock.forms[0].processing).toBe(false); expect(button().text()).toBe('Create prompt'); expect(button().attributes('disabled')).toBeUndefined(); expect(inertiaMock.formSubmissions).toHaveLength(1); }); it('clears form errors via onError when the server rejects the submission', async () => { inertiaMock.failNext = true; const wrapper = mount(PromptForm, { props: baseProps() }); await wrapper.find('form').trigger('submit'); const form = inertiaMock.forms[0]; expect(form.errors).toEqual({}); // stub seeded errors; onError cleared them expect(form.processing).toBe(false); expect(inertiaMock.page.props.errors.name).toEqual(['The server rejected the submission.']); expect(wrapper.text()).toContain('The server rejected the submission.'); }); it('renders a cancel link to cancelHref', () => { const wrapper = mount(PromptForm, { props: baseProps() }); const link = wrapper.find('a[data-inertia]'); expect(link.attributes('href')).toBe('/prompts'); expect(link.text()).toBe('Cancel'); }); });