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

224 lines
8 KiB
JavaScript
Raw 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 { 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();
});
});