import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { flushPromises, mount } from '@vue/test-utils'; vi.mock('@inertiajs/vue3', async () => import('../../stubs/inertia.js')); import Show from '../../../../resources/js/Pages/Prompts/Show.vue'; import { inertiaMock, resetInertiaMock } from '../../stubs/inertia.js'; const promptFixture = { id: 7, name: 'Greeter', description: 'Says hi.', body: 'Hi {{ name }}!', is_favorite: true, usage_count: 4, last_used_at: '2026-10-01T09:30:00.000000Z', created_at: '2026-09-01T08:00:00.000000Z', updated_at: '2026-10-20T10:00:00.000000Z', tags: [{ id: 1, name: 'chat' }], }; const baseProps = (overrides = {}) => ({ prompt: { ...promptFixture, ...overrides }, variables: ['name'], }); const last = () => inertiaMock.routerCalls[inertiaMock.routerCalls.length - 1]; const installClipboard = (fail = false) => { const writeText = vi.fn( fail ? () => Promise.reject(new Error('Permission denied')) : () => Promise.resolve() ); vi.stubGlobal('navigator', { clipboard: { writeText } }); return writeText; }; const buttonWithText = (wrapper, text) => wrapper.findAll('button').find((button) => button.text() === text); describe('Pages/Prompts/Show', () => { beforeEach(() => { resetInertiaMock(); }); afterEach(() => { vi.unstubAllGlobals(); }); it('renders the prompt metadata, tag links, and body', () => { const wrapper = mount(Show, { props: baseProps() }); expect(inertiaMock.headCalls.at(-1).title).toBe('Greeter'); expect(wrapper.find('h1').text()).toBe('Greeter'); expect(wrapper.text()).toContain('Says hi.'); expect(wrapper.find('a[href="/prompts"]').text()).toContain('All prompts'); expect(wrapper.find('a[href="/prompts/7/edit"]').text()).toContain('Edit'); expect(wrapper.find('a[href="/prompts?tag=chat"]').text()).toBe('chat'); expect(wrapper.text()).toContain('4×'); expect(wrapper.find('pre').text()).toBe('Hi {{ name }}!'); expect(wrapper.text()).toMatch(/Oct\s+1,?\s+2026/); expect(wrapper.text()).toMatch(/Sep\s+1,?\s+2026/); }); it('renders the star as Favorite and the last-used date as an em dash when unset', () => { const wrapper = mount(Show, { props: baseProps({ is_favorite: false, description: null, last_used_at: null }), }); expect(wrapper.find('button[aria-label="Favorite"]').exists()).toBe(true); expect(wrapper.text()).not.toContain('Says hi.'); expect(wrapper.text()).toContain('Last used:'); expect(wrapper.text()).toContain('—'); }); it('toggles favorite via router.post', async () => { const wrapper = mount(Show, { props: baseProps() }); await wrapper.find('button[aria-label="Unfavorite"]').trigger('click'); const call = last(); expect(call.method).toBe('post'); expect(call.url).toBe('/prompts/7/toggle-favorite'); }); it('duplicates via router.post', async () => { const wrapper = mount(Show, { props: baseProps() }); await buttonWithText(wrapper, 'Duplicate').trigger('click'); const call = last(); expect(call.method).toBe('post'); expect(call.url).toBe('/prompts/7/duplicate'); }); it('deletes after confirmation', async () => { const wrapper = mount(Show, { props: baseProps() }); await buttonWithText(wrapper, 'Delete').trigger('click'); const dialog = wrapper.find('[role="dialog"]'); expect(dialog.text()).toContain('will be permanently deleted'); await dialog.findAll('button').find((b) => b.text() === 'Delete').trigger('click'); const call = last(); expect(call.method).toBe('delete'); expect(call.url).toBe('/prompts/7'); }); it('cancels the delete dialog without deleting', async () => { const wrapper = mount(Show, { props: baseProps() }); await buttonWithText(wrapper, 'Delete').trigger('click'); await wrapper .find('[role="dialog"]') .findAll('button') .find((b) => b.text() === 'Cancel') .trigger('click'); expect(wrapper.find('[role="dialog"]').exists()).toBe(false); expect(inertiaMock.routerCalls.filter((c) => c.method === 'delete')).toHaveLength(0); }); it('copies the template body and shows a transient confirmation', async () => { const writeText = installClipboard(); const wrapper = mount(Show, { props: baseProps() }); const copyButton = buttonWithText(wrapper, 'Copy template'); await copyButton.trigger('click'); await flushPromises(); expect(writeText).toHaveBeenCalledWith('Hi {{ name }}!'); expect(copyButton.text()).toBe('✓ Copied'); }); it('ignores clipboard failures when copying the template', async () => { installClipboard(true); const wrapper = mount(Show, { props: baseProps() }); const copyButton = buttonWithText(wrapper, 'Copy template'); await copyButton.trigger('click'); await flushPromises(); expect(copyButton.text()).toBe('Copy template'); }); it('renders the prompt via /use on success and reloads', async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ rendered: 'Hi Ada!' }), }); vi.stubGlobal('fetch', fetchMock); const wrapper = mount(Show, { props: baseProps() }); await wrapper.findAll('textarea')[0].setValue('Ada'); await buttonWithText(wrapper, 'Use prompt (counts usage)').trigger('click'); await flushPromises(); expect(fetchMock).toHaveBeenCalledWith( '/prompts/7/use', expect.objectContaining({ method: 'POST', body: JSON.stringify({ values: { name: 'Ada' } }), }) ); expect(wrapper.findAll('pre').at(-1).text()).toBe('Hi Ada!'); expect(inertiaMock.routerCalls.some((c) => c.method === 'reload')).toBe(true); }); it('shows server error details when /use fails with missing variables', async () => { vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, json: async () => ({ message: 'Missing variables', missing: ['name'] }), })); const wrapper = mount(Show, { props: baseProps() }); await buttonWithText(wrapper, 'Use prompt (counts usage)').trigger('click'); await flushPromises(); expect(wrapper.text()).toContain('Missing variables'); expect(wrapper.text()).toContain('(name)'); expect(inertiaMock.routerCalls.some((c) => c.method === 'reload')).toBe(false); }); it('falls back to a generic message when the error body is unreadable', async () => { vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, json: async () => { throw new Error('not json'); }, })); const wrapper = mount(Show, { props: baseProps() }); await buttonWithText(wrapper, 'Use prompt (counts usage)').trigger('click'); await flushPromises(); expect(wrapper.text()).toContain('Could not render the prompt.'); }); it('shows a generic message when the request fails', async () => { vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network down'))); const wrapper = mount(Show, { props: baseProps() }); await buttonWithText(wrapper, 'Use prompt (counts usage)').trigger('click'); await flushPromises(); expect(wrapper.text()).toContain('Request failed.'); }); });