211 lines
7.6 KiB
JavaScript
211 lines
7.6 KiB
JavaScript
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.');
|
||
});
|
||
});
|