PromptVault/tests/js/Pages/Prompts/Show.test.js

211 lines
7.6 KiB
JavaScript
Raw Permalink Normal View History

2026-10-26 20:12:16 +00:00
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.');
});
});