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

224 lines
8 KiB
JavaScript
Raw Permalink Normal View History

2026-10-26 20:12:16 +00:00
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
vi.mock('@inertiajs/vue3', async () => import('../../stubs/inertia.js'));
import Index from '../../../../resources/js/Pages/Prompts/Index.vue';
import { inertiaMock, resetInertiaMock } from '../../stubs/inertia.js';
const promptFixture = (id, name, extra = {}) => ({
id,
name,
description: null,
tags: [{ id: 1, name: 'chat' }],
is_favorite: false,
usage_count: 0,
last_used_at: null,
...extra,
});
const baseProps = () => ({
prompts: {
data: [
promptFixture(1, 'Greeter', {
description: 'Says hi',
is_favorite: true,
usage_count: 5,
last_used_at: '2026-10-01T09:30:00.000000Z',
}),
promptFixture(2, 'Summarizer'),
],
current_page: 1,
last_page: 1,
total: 2,
},
tags: ['chat', 'code', 'writing'],
filters: { search: '', tag: null, favorites: 0 },
sort: 'created_at',
direction: 'desc',
});
const last = () => inertiaMock.routerCalls[inertiaMock.routerCalls.length - 1];
describe('Pages/Prompts/Index', () => {
beforeEach(() => {
resetInertiaMock();
});
it('renders rows with name, description, tags, usage, and last-used date', () => {
const wrapper = mount(Index, { props: baseProps() });
expect(wrapper.text()).toContain('2 prompt(s) in your vault');
const firstRow = wrapper.findAll('tbody tr')[0];
expect(firstRow.find('a[href="/prompts/1"]').text()).toBe('Greeter');
expect(firstRow.text()).toContain('Says hi');
expect(firstRow.text()).toContain('5×');
expect(firstRow.text()).toContain('chat');
expect(firstRow.text()).toMatch(/Oct\s+1,?\s+2026/);
expect(firstRow.find('a[href="/prompts/1/edit"]').exists()).toBe(true);
const secondRow = wrapper.findAll('tbody tr')[1];
expect(secondRow.text()).toContain('Summarizer');
expect(secondRow.text()).not.toContain('Says hi');
expect(secondRow.text()).toContain('—');
});
it('shows the empty state with a create link when there are no rows', () => {
const props = {
...baseProps(),
prompts: { data: [], current_page: 1, last_page: 1, total: 0 },
};
const wrapper = mount(Index, { props });
expect(wrapper.text()).toContain('No prompts match.');
expect(wrapper.find('a[href="/prompts/create"]').exists()).toBe(true);
expect(wrapper.findAll('tbody tr')).toHaveLength(1);
});
it('marks favorite rows with an Unfavorite star and posts the toggle', async () => {
const wrapper = mount(Index, { props: baseProps() });
expect(wrapper.find('button[aria-label="Favorite"]').exists()).toBe(true);
await wrapper.find('button[aria-label="Unfavorite"]').trigger('click');
const call = last();
expect(call.method).toBe('post');
expect(call.url).toBe('/prompts/1/toggle-favorite');
});
it('submits the search form into a GET refresh', async () => {
const wrapper = mount(Index, { props: baseProps() });
await wrapper.find('input[type="search"]').setValue('ada');
await wrapper.find('form').trigger('submit');
const call = last();
expect(call.method).toBe('get');
expect(call.url).toBe('/prompts');
expect(call.data.search).toBe('ada');
expect(call.data.sort).toBe('created_at');
expect(call.data.direction).toBe('desc');
});
it('refreshes with the selected tag', async () => {
const wrapper = mount(Index, { props: baseProps() });
await wrapper.findAll('select')[0].setValue('code');
expect(last().data.tag).toBe('code');
});
it('toggles the favorites filter on and off', async () => {
const wrapper = mount(Index, { props: baseProps() });
const checkbox = wrapper.find('input[type="checkbox"]');
await checkbox.setValue(true);
expect(last().data.favorites).toBe('1');
await checkbox.setValue(false);
expect(last().data.favorites).toBeUndefined();
});
it('refreshes with sort and direction changes', async () => {
const wrapper = mount(Index, { props: baseProps() });
const selects = wrapper.findAll('select');
await selects[1].setValue('usage_count');
expect(last().data.sort).toBe('usage_count');
await selects[2].setValue('asc');
expect(last().data.direction).toBe('asc');
});
it('duplicates a row via router.post', async () => {
const wrapper = mount(Index, { props: baseProps() });
await wrapper.findAll('button').find((b) => b.text() === 'Duplicate').trigger('click');
const call = last();
expect(call.method).toBe('post');
expect(call.url).toBe('/prompts/1/duplicate');
});
it('asks for confirmation, then deletes and refreshes', async () => {
const wrapper = mount(Index, { props: baseProps() });
await wrapper.findAll('button').find((b) => b.text() === 'Delete').trigger('click');
const dialog = wrapper.find('[role="dialog"]');
expect(dialog.text()).toContain('Delete prompt?');
expect(dialog.text()).toContain('Greeter');
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/1');
expect(wrapper.find('[role="dialog"]').exists()).toBe(false);
call.data.onFinish();
expect(last().method).toBe('get');
expect(last().url).toBe('/prompts');
});
it('safely no-ops confirmDelete when no prompt is targeted', () => {
const wrapper = mount(Index, { props: baseProps() });
expect(() => wrapper.vm.confirmDelete()).not.toThrow();
expect(inertiaMock.routerCalls.filter((c) => c.method === 'delete')).toHaveLength(0);
});
it('cancels the delete dialog without deleting', async () => {
const wrapper = mount(Index, { props: baseProps() });
await wrapper.findAll('button').find((b) => b.text() === '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('navigates pages when there is more than one page', async () => {
const props = {
...baseProps(),
prompts: { ...baseProps().prompts, current_page: 2, last_page: 3 },
};
const wrapper = mount(Index, { props });
expect(wrapper.text()).toContain('Page 2 of 3');
const prev = wrapper.findAll('button').find((b) => b.text().includes('Prev'));
const next = wrapper.findAll('button').find((b) => b.text().includes('Next'));
expect(prev.attributes('disabled')).toBeUndefined();
expect(next.attributes('disabled')).toBeUndefined();
await prev.trigger('click');
expect(last().data.page).toBe(1);
await next.trigger('click');
expect(last().data.page).toBe(3);
});
it('disables prev/next at the page boundaries', () => {
const paged = (current_page) => ({
...baseProps(),
prompts: { ...baseProps().prompts, current_page, last_page: 3 },
});
const onFirst = mount(Index, { props: paged(1) });
const onLast = mount(Index, { props: paged(3) });
const prev = onFirst.findAll('button').find((b) => b.text().includes('Prev'));
expect(prev.attributes('disabled')).toBeDefined();
const next = onLast.findAll('button').find((b) => b.text().includes('Next'));
expect(next.attributes('disabled')).toBeDefined();
});
});