fix: some smaller pieces still missing
This commit is contained in:
@@ -18,8 +18,8 @@ const UI_DATE_LOCALE: Record<string, string> = {
|
||||
const toTemplateSlug = (value: string) => {
|
||||
const normalized = value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
.replace(/[^a-z0-9]+/g, '_')
|
||||
.replace(/^_+|_+$/g, '');
|
||||
return normalized || 'template';
|
||||
};
|
||||
|
||||
|
||||
@@ -602,7 +602,9 @@ describe('IPC Handlers', () => {
|
||||
mockGitEngine.getChangedScriptFilesBetween.mockResolvedValue([
|
||||
{ status: 'modified', path: 'scripts/transform.py' },
|
||||
]);
|
||||
mockGitEngine.getChangedTemplateFilesBetween.mockResolvedValue([]);
|
||||
mockGitEngine.getChangedTemplateFilesBetween.mockResolvedValue([
|
||||
{ status: 'added', path: 'templates/custom_post.liquid' },
|
||||
]);
|
||||
mockPostEngine.reconcilePublishedPostsFromGitChanges.mockResolvedValue({
|
||||
created: 1,
|
||||
updated: 1,
|
||||
@@ -615,6 +617,12 @@ describe('IPC Handlers', () => {
|
||||
deleted: 0,
|
||||
processedFiles: 1,
|
||||
});
|
||||
mockTemplateEngine.reconcileTemplatesFromGitChanges.mockResolvedValue({
|
||||
created: 1,
|
||||
updated: 0,
|
||||
deleted: 0,
|
||||
processedFiles: 1,
|
||||
});
|
||||
|
||||
const result = await invokeHandler('git:pull', '/repo');
|
||||
|
||||
@@ -623,6 +631,7 @@ describe('IPC Handlers', () => {
|
||||
expect(mockGitEngine.getHeadCommit).toHaveBeenNthCalledWith(2, '/repo');
|
||||
expect(mockGitEngine.getChangedPostFilesBetween).toHaveBeenCalledWith('/repo', 'before-head', 'after-head');
|
||||
expect(mockGitEngine.getChangedScriptFilesBetween).toHaveBeenCalledWith('/repo', 'before-head', 'after-head');
|
||||
expect(mockGitEngine.getChangedTemplateFilesBetween).toHaveBeenCalledWith('/repo', 'before-head', 'after-head');
|
||||
expect(mockPostEngine.reconcilePublishedPostsFromGitChanges).toHaveBeenCalledWith('/repo', [
|
||||
{ status: 'modified', path: 'posts/2026/02/existing.md' },
|
||||
{ status: 'added', path: 'posts/2026/02/new-post.md' },
|
||||
@@ -630,6 +639,9 @@ describe('IPC Handlers', () => {
|
||||
expect(mockScriptEngine.reconcileScriptsFromGitChanges).toHaveBeenCalledWith('/repo', [
|
||||
{ status: 'modified', path: 'scripts/transform.py' },
|
||||
]);
|
||||
expect(mockTemplateEngine.reconcileTemplatesFromGitChanges).toHaveBeenCalledWith('/repo', [
|
||||
{ status: 'added', path: 'templates/custom_post.liquid' },
|
||||
]);
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
@@ -642,8 +654,10 @@ describe('IPC Handlers', () => {
|
||||
expect(mockGitEngine.pull).toHaveBeenCalledWith('/repo');
|
||||
expect(mockGitEngine.getChangedPostFilesBetween).not.toHaveBeenCalled();
|
||||
expect(mockGitEngine.getChangedScriptFilesBetween).not.toHaveBeenCalled();
|
||||
expect(mockGitEngine.getChangedTemplateFilesBetween).not.toHaveBeenCalled();
|
||||
expect(mockPostEngine.reconcilePublishedPostsFromGitChanges).not.toHaveBeenCalled();
|
||||
expect(mockScriptEngine.reconcileScriptsFromGitChanges).not.toHaveBeenCalled();
|
||||
expect(mockTemplateEngine.reconcileTemplatesFromGitChanges).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ success: false, code: 'conflict' });
|
||||
});
|
||||
|
||||
@@ -658,8 +672,10 @@ describe('IPC Handlers', () => {
|
||||
expect(mockGitEngine.pull).toHaveBeenCalledWith('/repo');
|
||||
expect(mockGitEngine.getChangedPostFilesBetween).not.toHaveBeenCalled();
|
||||
expect(mockGitEngine.getChangedScriptFilesBetween).not.toHaveBeenCalled();
|
||||
expect(mockGitEngine.getChangedTemplateFilesBetween).not.toHaveBeenCalled();
|
||||
expect(mockPostEngine.reconcilePublishedPostsFromGitChanges).not.toHaveBeenCalled();
|
||||
expect(mockScriptEngine.reconcileScriptsFromGitChanges).not.toHaveBeenCalled();
|
||||
expect(mockTemplateEngine.reconcileTemplatesFromGitChanges).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -182,6 +182,107 @@ describe('SettingsView Diff Preferences', () => {
|
||||
expect(rebuildScriptsMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('triggers templates rebuild from data maintenance section', async () => {
|
||||
const rebuildTemplatesMock = vi.fn().mockResolvedValue(undefined);
|
||||
(window as any).electronAPI = {
|
||||
...(window as any).electronAPI,
|
||||
templates: {
|
||||
...(window as any).electronAPI?.templates,
|
||||
getEnabledByKind: vi.fn().mockResolvedValue([]),
|
||||
rebuildFromFiles: rebuildTemplatesMock,
|
||||
},
|
||||
};
|
||||
|
||||
render(<SettingsView />);
|
||||
|
||||
const rebuildTemplatesButton = await screen.findByRole('button', { name: /rebuild templates/i });
|
||||
fireEvent.click(rebuildTemplatesButton);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(rebuildTemplatesMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders category template dropdowns populated with enabled templates', async () => {
|
||||
(window as any).electronAPI = {
|
||||
...(window as any).electronAPI,
|
||||
templates: {
|
||||
...(window as any).electronAPI?.templates,
|
||||
getEnabledByKind: vi.fn().mockImplementation((kind: string) => {
|
||||
if (kind === 'post') {
|
||||
return Promise.resolve([
|
||||
{ slug: 'custom_post', title: 'Custom Post' },
|
||||
]);
|
||||
}
|
||||
if (kind === 'list') {
|
||||
return Promise.resolve([
|
||||
{ slug: 'custom_list', title: 'Custom List' },
|
||||
]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
render(<SettingsView />);
|
||||
|
||||
const postTemplateSelect = await screen.findByLabelText(/article post template/i);
|
||||
expect(postTemplateSelect).toBeInTheDocument();
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const options = postTemplateSelect.querySelectorAll('option');
|
||||
const optionTexts = Array.from(options).map((o) => o.textContent);
|
||||
expect(optionTexts).toContain('Custom Post');
|
||||
});
|
||||
|
||||
const listTemplateSelect = screen.getByLabelText(/article list template/i);
|
||||
expect(listTemplateSelect).toBeInTheDocument();
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const options = listTemplateSelect.querySelectorAll('option');
|
||||
const optionTexts = Array.from(options).map((o) => o.textContent);
|
||||
expect(optionTexts).toContain('Custom List');
|
||||
});
|
||||
});
|
||||
|
||||
it('persists category template selection via project metadata update', async () => {
|
||||
(window as any).electronAPI = {
|
||||
...(window as any).electronAPI,
|
||||
templates: {
|
||||
...(window as any).electronAPI?.templates,
|
||||
getEnabledByKind: vi.fn().mockImplementation((kind: string) => {
|
||||
if (kind === 'post') {
|
||||
return Promise.resolve([
|
||||
{ slug: 'custom_post', title: 'Custom Post' },
|
||||
]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
render(<SettingsView />);
|
||||
|
||||
const postTemplateSelect = await screen.findByLabelText(/article post template/i);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const options = postTemplateSelect.querySelectorAll('option');
|
||||
expect(Array.from(options).map((o) => o.textContent)).toContain('Custom Post');
|
||||
});
|
||||
|
||||
fireEvent.change(postTemplateSelect, { target: { value: 'custom_post' } });
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect((window as any).electronAPI.meta.updateProjectMetadata).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
categoryMetadata: expect.objectContaining({
|
||||
article: expect.objectContaining({ postTemplateSlug: 'custom_post' }),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('persists category settings changes via project metadata update', async () => {
|
||||
render(<SettingsView />);
|
||||
|
||||
|
||||
281
tests/renderer/components/SidebarTemplates.test.tsx
Normal file
281
tests/renderer/components/SidebarTemplates.test.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
import React from 'react';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { act, render, screen, fireEvent } from '@testing-library/react';
|
||||
import { Sidebar } from '../../../src/renderer/components/Sidebar/Sidebar';
|
||||
import { useAppStore } from '../../../src/renderer/store';
|
||||
|
||||
describe('Sidebar templates list behavior', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
const listeners = new Map<string, Set<(event: Event) => void>>();
|
||||
(window as any).addEventListener = vi.fn((type: string, listener: (event: Event) => void) => {
|
||||
if (!listeners.has(type)) {
|
||||
listeners.set(type, new Set());
|
||||
}
|
||||
listeners.get(type)?.add(listener);
|
||||
});
|
||||
(window as any).removeEventListener = vi.fn((type: string, listener: (event: Event) => void) => {
|
||||
listeners.get(type)?.delete(listener);
|
||||
});
|
||||
(window as any).dispatchEvent = vi.fn((event: Event) => {
|
||||
listeners.get(event.type)?.forEach((listener) => listener(event));
|
||||
return true;
|
||||
});
|
||||
|
||||
(window as any).electronAPI = {
|
||||
...(window as any).electronAPI,
|
||||
templates: {
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn().mockResolvedValue({ deleted: true }),
|
||||
get: vi.fn(),
|
||||
getAll: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: 'template-1',
|
||||
projectId: 'default',
|
||||
slug: 'custom_post',
|
||||
title: 'Custom Post',
|
||||
kind: 'post',
|
||||
enabled: true,
|
||||
version: 1,
|
||||
filePath: '/tmp/custom_post.liquid',
|
||||
content: '<main>{{ post.title }}</main>',
|
||||
createdAt: '2026-02-22T00:00:00.000Z',
|
||||
updatedAt: '2026-02-22T00:00:00.000Z',
|
||||
},
|
||||
]),
|
||||
getEnabledByKind: vi.fn().mockResolvedValue([]),
|
||||
validate: vi.fn(),
|
||||
rebuildFromFiles: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
useAppStore.setState({
|
||||
activeView: 'templates',
|
||||
sidebarVisible: true,
|
||||
tabs: [],
|
||||
activeTabId: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('opens a transient template tab on single click', async () => {
|
||||
const { container } = render(<Sidebar />);
|
||||
|
||||
const templateRow = await screen.findByRole('button', { name: 'Custom Post' });
|
||||
expect(templateRow).toHaveClass('chat-list-item');
|
||||
expect(container.querySelector('.chat-item-date')).not.toBeNull();
|
||||
fireEvent.click(templateRow);
|
||||
|
||||
expect(useAppStore.getState().tabs).toEqual([
|
||||
{
|
||||
type: 'templates',
|
||||
id: 'template-1',
|
||||
isTransient: true,
|
||||
},
|
||||
]);
|
||||
expect(useAppStore.getState().activeTabId).toBe('template-1');
|
||||
});
|
||||
|
||||
it('renders templates section title and create button', async () => {
|
||||
render(<Sidebar />);
|
||||
|
||||
expect(screen.getByText('TEMPLATES')).toBeInTheDocument();
|
||||
expect(await screen.findByRole('button', { name: 'New Template' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows loading state while templates are being fetched', () => {
|
||||
(window as any).electronAPI.templates.getAll = vi.fn().mockImplementation(
|
||||
() => new Promise(() => {}),
|
||||
);
|
||||
|
||||
render(<Sidebar />);
|
||||
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows empty state with create action when no templates exist', async () => {
|
||||
(window as any).electronAPI.templates.getAll = vi.fn().mockResolvedValue([]);
|
||||
|
||||
render(<Sidebar />);
|
||||
|
||||
expect(await screen.findByText('No templates yet')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Create a template' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('creates a new template from the create button and opens it pinned', async () => {
|
||||
const createMock = vi.fn().mockResolvedValue({
|
||||
id: 'template-new',
|
||||
projectId: 'default',
|
||||
slug: 'new_template',
|
||||
title: 'New Template',
|
||||
kind: 'post',
|
||||
enabled: true,
|
||||
version: 1,
|
||||
filePath: '/tmp/new_template.liquid',
|
||||
content: '',
|
||||
createdAt: '2026-02-22T00:00:00.000Z',
|
||||
updatedAt: '2026-02-22T00:00:00.000Z',
|
||||
});
|
||||
|
||||
(window as any).electronAPI.templates.create = createMock;
|
||||
|
||||
render(<Sidebar />);
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'New Template' }));
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(createMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: 'New Template',
|
||||
kind: 'post',
|
||||
content: '',
|
||||
enabled: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(useAppStore.getState().tabs).toEqual([
|
||||
{
|
||||
type: 'templates',
|
||||
id: 'template-new',
|
||||
isTransient: false,
|
||||
},
|
||||
]);
|
||||
expect(useAppStore.getState().activeTabId).toBe('template-new');
|
||||
});
|
||||
});
|
||||
|
||||
it('opens a pinned template tab on double click', async () => {
|
||||
render(<Sidebar />);
|
||||
|
||||
const templateRow = await screen.findByRole('button', { name: 'Custom Post' });
|
||||
fireEvent.doubleClick(templateRow);
|
||||
|
||||
expect(useAppStore.getState().tabs).toEqual([
|
||||
{
|
||||
type: 'templates',
|
||||
id: 'template-1',
|
||||
isTransient: false,
|
||||
},
|
||||
]);
|
||||
expect(useAppStore.getState().activeTabId).toBe('template-1');
|
||||
});
|
||||
|
||||
it('deletes a template from sidebar action', async () => {
|
||||
const deleteMock = vi.fn().mockResolvedValue({ deleted: true });
|
||||
(window as any).electronAPI.templates.delete = deleteMock;
|
||||
|
||||
useAppStore.setState({
|
||||
tabs: [{ type: 'templates', id: 'template-1', isTransient: false }],
|
||||
activeTabId: 'template-1',
|
||||
});
|
||||
|
||||
render(<Sidebar />);
|
||||
|
||||
const deleteButton = await screen.findByTitle('Delete template');
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(deleteMock).toHaveBeenCalledWith('template-1');
|
||||
expect(useAppStore.getState().tabs).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('refreshes templates list when templates-changed event is emitted', async () => {
|
||||
const getAllMock = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
id: 'template-1',
|
||||
projectId: 'default',
|
||||
slug: 'custom_post',
|
||||
title: 'Custom Post',
|
||||
kind: 'post',
|
||||
enabled: true,
|
||||
version: 1,
|
||||
filePath: '/tmp/custom_post.liquid',
|
||||
content: '<main>{{ post.title }}</main>',
|
||||
createdAt: '2026-02-22T00:00:00.000Z',
|
||||
updatedAt: '2026-02-22T00:00:00.000Z',
|
||||
},
|
||||
])
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
id: 'template-1',
|
||||
projectId: 'default',
|
||||
slug: 'renamed_template',
|
||||
title: 'Renamed Template',
|
||||
kind: 'post',
|
||||
enabled: true,
|
||||
version: 2,
|
||||
filePath: '/tmp/custom_post.liquid',
|
||||
content: '<main>{{ post.title }}</main>',
|
||||
createdAt: '2026-02-22T00:00:00.000Z',
|
||||
updatedAt: '2026-02-22T00:01:00.000Z',
|
||||
},
|
||||
]);
|
||||
|
||||
(window as any).electronAPI.templates.getAll = getAllMock;
|
||||
|
||||
render(<Sidebar />);
|
||||
|
||||
await screen.findByRole('button', { name: 'Custom Post' });
|
||||
window.dispatchEvent(new CustomEvent('bds:templates-changed'));
|
||||
|
||||
expect(await screen.findByRole('button', { name: 'Renamed Template' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('reloads templates when active project context becomes available after mount', async () => {
|
||||
const getAllMock = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
id: 'template-1',
|
||||
projectId: 'project-1',
|
||||
slug: 'custom_post',
|
||||
title: 'Custom Post',
|
||||
kind: 'post',
|
||||
enabled: true,
|
||||
version: 1,
|
||||
filePath: '/tmp/custom_post.liquid',
|
||||
content: '<main>{{ post.title }}</main>',
|
||||
createdAt: '2026-02-22T00:00:00.000Z',
|
||||
updatedAt: '2026-02-22T00:00:00.000Z',
|
||||
},
|
||||
]);
|
||||
|
||||
(window as any).electronAPI.templates.getAll = getAllMock;
|
||||
|
||||
useAppStore.setState({
|
||||
activeProject: null,
|
||||
activeView: 'templates',
|
||||
sidebarVisible: true,
|
||||
tabs: [],
|
||||
activeTabId: null,
|
||||
});
|
||||
|
||||
render(<Sidebar />);
|
||||
|
||||
expect(await screen.findByText('No templates yet')).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
useAppStore.setState({
|
||||
activeProject: {
|
||||
id: 'project-1',
|
||||
name: 'Project 1',
|
||||
slug: 'project-1',
|
||||
dataPath: '/tmp/project-1',
|
||||
isActive: true,
|
||||
createdAt: '2026-02-22T00:00:00.000Z',
|
||||
updatedAt: '2026-02-22T00:00:00.000Z',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(await screen.findByRole('button', { name: 'Custom Post' })).toBeInTheDocument();
|
||||
expect(getAllMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { render, act } from '@testing-library/react';
|
||||
import { render, act, screen, fireEvent } from '@testing-library/react';
|
||||
import { TagsView } from '../../../src/renderer/components/TagsView/TagsView';
|
||||
|
||||
describe('TagsView subscriptions', () => {
|
||||
@@ -61,3 +61,98 @@ describe('TagsView subscriptions', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('TagsView template dropdown', () => {
|
||||
beforeEach(() => {
|
||||
const onMock = vi.fn((_channel: string, _callback: (...args: unknown[]) => void) => vi.fn());
|
||||
|
||||
(window as any).electronAPI = {
|
||||
...(window as any).electronAPI,
|
||||
tags: {
|
||||
getWithCounts: vi.fn().mockResolvedValue([
|
||||
{ name: 'javascript', count: 5 },
|
||||
]),
|
||||
getAll: vi.fn().mockResolvedValue([
|
||||
{ id: 'tag-1', name: 'javascript', color: null, postTemplateSlug: null },
|
||||
]),
|
||||
create: vi.fn(),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn(),
|
||||
rename: vi.fn(),
|
||||
merge: vi.fn(),
|
||||
syncFromPosts: vi.fn(),
|
||||
},
|
||||
templates: {
|
||||
getEnabledByKind: vi.fn().mockResolvedValue([
|
||||
{ slug: 'custom_post', title: 'Custom Post' },
|
||||
]),
|
||||
},
|
||||
on: onMock,
|
||||
};
|
||||
});
|
||||
|
||||
it('loads post templates and shows them in tag edit dropdown', async () => {
|
||||
render(<TagsView />);
|
||||
|
||||
// Wait for tags to load and click the tag to select it
|
||||
const tagButton = await screen.findByText('javascript');
|
||||
fireEvent.click(tagButton);
|
||||
|
||||
// Click the edit button to enter edit mode
|
||||
const editButton = await screen.findByRole('button', { name: /edit/i });
|
||||
fireEvent.click(editButton);
|
||||
|
||||
// Verify template dropdown appears with the loaded template
|
||||
await vi.waitFor(() => {
|
||||
const label = screen.getByText('Post Template');
|
||||
const fieldDiv = label.closest('.tagsview-field')!;
|
||||
const select = fieldDiv.querySelector('select')!;
|
||||
const options = Array.from(select.querySelectorAll('option'));
|
||||
const optionTexts = options.map((o) => o.textContent);
|
||||
expect(optionTexts).toContain('Custom Post');
|
||||
});
|
||||
});
|
||||
|
||||
it('saves tag template selection via update IPC call', async () => {
|
||||
const updateMock = vi.fn().mockResolvedValue(undefined);
|
||||
(window as any).electronAPI.tags.update = updateMock;
|
||||
|
||||
render(<TagsView />);
|
||||
|
||||
// Select the tag
|
||||
const tagButton = await screen.findByText('javascript');
|
||||
fireEvent.click(tagButton);
|
||||
|
||||
// Enter edit mode
|
||||
const editButton = await screen.findByRole('button', { name: /edit/i });
|
||||
fireEvent.click(editButton);
|
||||
|
||||
// Wait for template dropdown to populate
|
||||
await vi.waitFor(() => {
|
||||
const label = screen.getByText('Post Template');
|
||||
const fieldDiv = label.closest('.tagsview-field')!;
|
||||
const select = fieldDiv.querySelector('select')!;
|
||||
const options = Array.from(select.querySelectorAll('option'));
|
||||
expect(options.map((o) => o.textContent)).toContain('Custom Post');
|
||||
});
|
||||
|
||||
// Select a template
|
||||
const label = screen.getByText('Post Template');
|
||||
const fieldDiv = label.closest('.tagsview-field')!;
|
||||
const templateSelect = fieldDiv.querySelector('select')!;
|
||||
fireEvent.change(templateSelect, { target: { value: 'custom_post' } });
|
||||
|
||||
// Save
|
||||
const saveButton = screen.getByRole('button', { name: /save/i });
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(updateMock).toHaveBeenCalledWith(
|
||||
'tag-1',
|
||||
expect.objectContaining({
|
||||
postTemplateSlug: 'custom_post',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,12 +27,12 @@ vi.mock('@monaco-editor/react', () => ({
|
||||
const mockTemplate = {
|
||||
id: 'template-1',
|
||||
projectId: 'default',
|
||||
slug: 'custom-post',
|
||||
slug: 'custom_post',
|
||||
title: 'Custom Post',
|
||||
kind: 'post' as const,
|
||||
enabled: true,
|
||||
version: 1,
|
||||
filePath: '/tmp/custom-post.liquid',
|
||||
filePath: '/tmp/custom_post.liquid',
|
||||
content: '<main>{{ post.title }}</main>',
|
||||
createdAt: '2026-02-22T00:00:00.000Z',
|
||||
updatedAt: '2026-02-22T00:00:00.000Z',
|
||||
@@ -67,7 +67,7 @@ describe('TemplatesView', () => {
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(titleInput.value).toBe('Custom Post');
|
||||
expect(slugInput.value).toBe('custom-post');
|
||||
expect(slugInput.value).toBe('custom_post');
|
||||
});
|
||||
expect(kindSelect.value).toBe('post');
|
||||
expect(enabledInput.checked).toBe(true);
|
||||
@@ -125,7 +125,7 @@ describe('TemplatesView', () => {
|
||||
'template-1',
|
||||
expect.objectContaining({
|
||||
title: 'Custom Post',
|
||||
slug: 'custom-post',
|
||||
slug: 'custom_post',
|
||||
kind: 'list',
|
||||
enabled: false,
|
||||
content: '<main>{{ post.title }}</main>',
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
getGitDiffFileTabSpec,
|
||||
getImportTabSpec,
|
||||
getScriptTabSpec,
|
||||
getTemplateTabSpec,
|
||||
parseGitDiffTabId,
|
||||
openChatTab,
|
||||
getSingletonToolTabSpec,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
openGitDiffFileTab,
|
||||
openImportTab,
|
||||
openScriptTab,
|
||||
openTemplateTab,
|
||||
openSingletonToolTab,
|
||||
} from '../../../src/renderer/navigation/tabPolicy';
|
||||
|
||||
@@ -163,4 +165,33 @@ describe('tabPolicy', () => {
|
||||
{ type: 'git-diff', id: 'git-diff:commit:def456', isTransient: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it('provides canonical template tab spec for preview and pin intents', () => {
|
||||
expect(getTemplateTabSpec('template-1', 'preview')).toEqual({
|
||||
type: 'templates',
|
||||
id: 'template-1',
|
||||
isTransient: true,
|
||||
});
|
||||
|
||||
expect(getTemplateTabSpec('template-1', 'pin')).toEqual({
|
||||
type: 'templates',
|
||||
id: 'template-1',
|
||||
isTransient: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('opens template tabs from shared policy', () => {
|
||||
const opened: Array<{ type: string; id: string; isTransient: boolean }> = [];
|
||||
const openTab = (tab: { type: string; id: string; isTransient: boolean }) => {
|
||||
opened.push(tab);
|
||||
};
|
||||
|
||||
openTemplateTab(openTab, 'template-preview', 'preview');
|
||||
openTemplateTab(openTab, 'template-pin', 'pin');
|
||||
|
||||
expect(opened).toEqual([
|
||||
{ type: 'templates', id: 'template-preview', isTransient: true },
|
||||
{ type: 'templates', id: 'template-pin', isTransient: false },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user