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