PromptVault/tests/js/Pages/Prompts/Show.test.js
2026-10-26 15:12:16 -05:00

211 lines
7.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.');
});
});