feat: rework templates
This commit is contained in:
@@ -91,6 +91,7 @@ describe('main bootstrap preview behavior', () => {
|
||||
|
||||
vi.doMock('../../src/main/ipc', () => ({
|
||||
registerIpcHandlers: vi.fn(),
|
||||
registerEventForwarding: vi.fn(),
|
||||
registerChatHandlers: vi.fn(),
|
||||
initializeChatHandlers: vi.fn(),
|
||||
cleanupChatHandlers: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -209,6 +210,7 @@ describe('main bootstrap preview behavior', () => {
|
||||
|
||||
vi.doMock('../../src/main/ipc', () => ({
|
||||
registerIpcHandlers: vi.fn(),
|
||||
registerEventForwarding: vi.fn(),
|
||||
registerChatHandlers: vi.fn(),
|
||||
initializeChatHandlers: vi.fn(),
|
||||
cleanupChatHandlers: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -356,6 +358,7 @@ describe('main bootstrap preview behavior', () => {
|
||||
|
||||
vi.doMock('../../src/main/ipc', () => ({
|
||||
registerIpcHandlers: vi.fn(),
|
||||
registerEventForwarding: vi.fn(),
|
||||
registerChatHandlers: vi.fn(),
|
||||
initializeChatHandlers: vi.fn(),
|
||||
cleanupChatHandlers: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -498,6 +501,7 @@ describe('main bootstrap preview behavior', () => {
|
||||
|
||||
vi.doMock('../../src/main/ipc', () => ({
|
||||
registerIpcHandlers: vi.fn(),
|
||||
registerEventForwarding: vi.fn(),
|
||||
registerChatHandlers: vi.fn(),
|
||||
initializeChatHandlers: vi.fn(),
|
||||
cleanupChatHandlers: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -628,6 +632,7 @@ describe('main bootstrap preview behavior', () => {
|
||||
|
||||
vi.doMock('../../src/main/ipc', () => ({
|
||||
registerIpcHandlers: vi.fn(),
|
||||
registerEventForwarding: vi.fn(),
|
||||
registerChatHandlers: vi.fn(),
|
||||
initializeChatHandlers: vi.fn(),
|
||||
cleanupChatHandlers: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -780,6 +785,7 @@ describe('main bootstrap preview behavior', () => {
|
||||
|
||||
vi.doMock('../../src/main/ipc', () => ({
|
||||
registerIpcHandlers: vi.fn(),
|
||||
registerEventForwarding: vi.fn(),
|
||||
registerChatHandlers: vi.fn(),
|
||||
initializeChatHandlers: vi.fn(),
|
||||
cleanupChatHandlers: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -961,6 +967,7 @@ describe('main bootstrap preview behavior', () => {
|
||||
|
||||
vi.doMock('../../src/main/ipc', () => ({
|
||||
registerIpcHandlers: vi.fn(),
|
||||
registerEventForwarding: vi.fn(),
|
||||
registerChatHandlers: vi.fn(),
|
||||
initializeChatHandlers: vi.fn(),
|
||||
cleanupChatHandlers: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -1145,6 +1152,7 @@ describe('main bootstrap preview behavior', () => {
|
||||
|
||||
vi.doMock('../../src/main/ipc', () => ({
|
||||
registerIpcHandlers: vi.fn(),
|
||||
registerEventForwarding: vi.fn(),
|
||||
registerChatHandlers: vi.fn(),
|
||||
initializeChatHandlers: vi.fn(),
|
||||
cleanupChatHandlers: vi.fn().mockResolvedValue(undefined),
|
||||
|
||||
129
tests/engine/mcp-view-builder.test.ts
Normal file
129
tests/engine/mcp-view-builder.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { buildMcpView, type McpViewConfig } from '../../src/main/engine/mcp-view-builder';
|
||||
|
||||
describe('mcp-view-builder', () => {
|
||||
describe('buildMcpView', () => {
|
||||
const minimalConfig: McpViewConfig = {
|
||||
title: 'Test View',
|
||||
waitingMessage: 'Waiting for data...',
|
||||
renderBody: `
|
||||
document.getElementById("review").innerHTML = "<h1>Hello</h1>";
|
||||
`,
|
||||
acceptLabel: 'Accept',
|
||||
discardLabel: 'Discard',
|
||||
};
|
||||
|
||||
it('returns a valid HTML document', () => {
|
||||
const html = buildMcpView(minimalConfig);
|
||||
expect(html).toContain('<!DOCTYPE html>');
|
||||
expect(html).toContain('</html>');
|
||||
});
|
||||
|
||||
it('sets the page title', () => {
|
||||
const html = buildMcpView(minimalConfig);
|
||||
expect(html).toContain('<title>Test View</title>');
|
||||
});
|
||||
|
||||
it('contains the waiting message', () => {
|
||||
const html = buildMcpView(minimalConfig);
|
||||
expect(html).toContain('Waiting for data...');
|
||||
});
|
||||
|
||||
it('contains the App import from ext-apps', () => {
|
||||
const html = buildMcpView(minimalConfig);
|
||||
expect(html).toContain('@modelcontextprotocol/ext-apps/app-with-deps');
|
||||
expect(html).toContain('new App(');
|
||||
});
|
||||
|
||||
it('contains accept and discard proposal handlers', () => {
|
||||
const html = buildMcpView(minimalConfig);
|
||||
expect(html).toContain('window.acceptProposal');
|
||||
expect(html).toContain('window.discardProposal');
|
||||
});
|
||||
|
||||
it('uses custom button labels in renderBody', () => {
|
||||
const config: McpViewConfig = {
|
||||
...minimalConfig,
|
||||
renderBody: `
|
||||
document.getElementById("review").innerHTML = \`
|
||||
<div class="actions">
|
||||
<button class="btn btn-accept" onclick="acceptProposal()">Accept</button>
|
||||
<button class="btn btn-discard" onclick="discardProposal()">Discard</button>
|
||||
</div>
|
||||
\`;`,
|
||||
};
|
||||
const html = buildMcpView(config);
|
||||
expect(html).toContain('onclick="acceptProposal()"');
|
||||
expect(html).toContain('onclick="discardProposal()"');
|
||||
});
|
||||
|
||||
it('calls accept_proposal and discard_proposal tools via app bridge', () => {
|
||||
const html = buildMcpView(minimalConfig);
|
||||
expect(html).toContain('app.callServerTool');
|
||||
expect(html).toContain('"accept_proposal"');
|
||||
expect(html).toContain('"discard_proposal"');
|
||||
});
|
||||
|
||||
it('contains the custom renderBody code', () => {
|
||||
const html = buildMcpView(minimalConfig);
|
||||
expect(html).toContain('document.getElementById("review").innerHTML = "<h1>Hello</h1>"');
|
||||
});
|
||||
|
||||
it('uses module script type', () => {
|
||||
const html = buildMcpView(minimalConfig);
|
||||
expect(html).toContain('type="module"');
|
||||
});
|
||||
|
||||
it('connects the App on load', () => {
|
||||
const html = buildMcpView(minimalConfig);
|
||||
expect(html).toContain('app.connect()');
|
||||
});
|
||||
|
||||
it('has a status display element', () => {
|
||||
const html = buildMcpView(minimalConfig);
|
||||
expect(html).toContain('id="status"');
|
||||
expect(html).toContain('showStatus');
|
||||
});
|
||||
|
||||
it('disables buttons during action', () => {
|
||||
const html = buildMcpView(minimalConfig);
|
||||
expect(html).toContain('setButtonsDisabled(true)');
|
||||
});
|
||||
|
||||
it('uses XSS-safe escaping function', () => {
|
||||
const html = buildMcpView(minimalConfig);
|
||||
expect(html).toContain('function esc(');
|
||||
expect(html).toContain('document.createElement("div")');
|
||||
});
|
||||
|
||||
it('renders tool result data via ontoolresult handler', () => {
|
||||
const html = buildMcpView(minimalConfig);
|
||||
expect(html).toContain('app.ontoolresult');
|
||||
expect(html).toContain('renderReview');
|
||||
});
|
||||
|
||||
it('includes extra CSS when provided', () => {
|
||||
const config: McpViewConfig = {
|
||||
...minimalConfig,
|
||||
extraCss: '.custom-class { color: red; }',
|
||||
};
|
||||
const html = buildMcpView(config);
|
||||
expect(html).toContain('.custom-class { color: red; }');
|
||||
});
|
||||
|
||||
it('includes extra JS helpers when provided', () => {
|
||||
const config: McpViewConfig = {
|
||||
...minimalConfig,
|
||||
extraJsHelpers: 'function fmt(v) { return String(v); }',
|
||||
};
|
||||
const html = buildMcpView(config);
|
||||
expect(html).toContain('function fmt(v) { return String(v); }');
|
||||
});
|
||||
|
||||
it('does not include extra CSS/JS when not provided', () => {
|
||||
const html = buildMcpView(minimalConfig);
|
||||
// The shared CSS is always present; just ensure no extra markers
|
||||
expect(html).not.toContain('function fmt(');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,68 +1,40 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import path from 'path';
|
||||
import {
|
||||
reviewPostHtml,
|
||||
reviewScriptHtml,
|
||||
reviewTemplateHtml,
|
||||
reviewMetadataHtml,
|
||||
resolveMcpViewsDirs,
|
||||
loadViewHtml,
|
||||
} from '../../src/main/engine/mcp-views';
|
||||
|
||||
const viewOpts = {
|
||||
moduleDir: path.resolve(__dirname, '../../src/main/engine'),
|
||||
};
|
||||
|
||||
describe('mcp-views', () => {
|
||||
describe('resolveMcpViewsDirs', () => {
|
||||
it('returns candidate directories', () => {
|
||||
const dirs = resolveMcpViewsDirs(viewOpts);
|
||||
expect(dirs.length).toBeGreaterThanOrEqual(2);
|
||||
expect(dirs.some(d => d.includes('mcp-views'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadViewHtml', () => {
|
||||
it('loads an existing view file', () => {
|
||||
const html = loadViewHtml('review-post.html', viewOpts);
|
||||
expect(html).toContain('<!DOCTYPE html>');
|
||||
});
|
||||
|
||||
it('throws for a non-existent view', () => {
|
||||
expect(() => loadViewHtml('does-not-exist.html', viewOpts)).toThrow(
|
||||
/not found/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reviewPostHtml', () => {
|
||||
it('returns valid HTML document', () => {
|
||||
const html = reviewPostHtml(viewOpts);
|
||||
const html = reviewPostHtml();
|
||||
expect(html).toContain('<!DOCTYPE html>');
|
||||
expect(html).toContain('</html>');
|
||||
});
|
||||
|
||||
it('contains App import from ext-apps', () => {
|
||||
const html = reviewPostHtml(viewOpts);
|
||||
const html = reviewPostHtml();
|
||||
expect(html).toContain('@modelcontextprotocol/ext-apps/app-with-deps');
|
||||
expect(html).toContain('new App(');
|
||||
});
|
||||
|
||||
it('contains accept and discard buttons', () => {
|
||||
const html = reviewPostHtml(viewOpts);
|
||||
const html = reviewPostHtml();
|
||||
expect(html).toContain('acceptProposal()');
|
||||
expect(html).toContain('discardProposal()');
|
||||
});
|
||||
|
||||
it('calls accept_proposal and discard_proposal tools via app bridge', () => {
|
||||
const html = reviewPostHtml(viewOpts);
|
||||
const html = reviewPostHtml();
|
||||
expect(html).toContain('app.callServerTool');
|
||||
expect(html).toContain('"accept_proposal"');
|
||||
expect(html).toContain('"discard_proposal"');
|
||||
});
|
||||
|
||||
it('contains post-specific UI elements', () => {
|
||||
const html = reviewPostHtml(viewOpts);
|
||||
const html = reviewPostHtml();
|
||||
expect(html).toContain('Review Post');
|
||||
expect(html).toContain('Publish');
|
||||
expect(html).toContain('badge-draft');
|
||||
@@ -70,13 +42,13 @@ describe('mcp-views', () => {
|
||||
});
|
||||
|
||||
it('renders tool result data via ontoolresult handler', () => {
|
||||
const html = reviewPostHtml(viewOpts);
|
||||
const html = reviewPostHtml();
|
||||
expect(html).toContain('app.ontoolresult');
|
||||
expect(html).toContain('renderReview');
|
||||
});
|
||||
|
||||
it('uses XSS-safe escaping function', () => {
|
||||
const html = reviewPostHtml(viewOpts);
|
||||
const html = reviewPostHtml();
|
||||
expect(html).toContain('function esc(');
|
||||
expect(html).toContain('document.createElement("div")');
|
||||
});
|
||||
@@ -84,24 +56,24 @@ describe('mcp-views', () => {
|
||||
|
||||
describe('reviewScriptHtml', () => {
|
||||
it('returns valid HTML document', () => {
|
||||
const html = reviewScriptHtml(viewOpts);
|
||||
const html = reviewScriptHtml();
|
||||
expect(html).toContain('<!DOCTYPE html>');
|
||||
expect(html).toContain('</html>');
|
||||
});
|
||||
|
||||
it('contains App import from ext-apps', () => {
|
||||
const html = reviewScriptHtml(viewOpts);
|
||||
const html = reviewScriptHtml();
|
||||
expect(html).toContain('@modelcontextprotocol/ext-apps/app-with-deps');
|
||||
});
|
||||
|
||||
it('contains accept and discard buttons', () => {
|
||||
const html = reviewScriptHtml(viewOpts);
|
||||
const html = reviewScriptHtml();
|
||||
expect(html).toContain('acceptProposal()');
|
||||
expect(html).toContain('discardProposal()');
|
||||
});
|
||||
|
||||
it('contains script-specific UI elements', () => {
|
||||
const html = reviewScriptHtml(viewOpts);
|
||||
const html = reviewScriptHtml();
|
||||
expect(html).toContain('Review Script');
|
||||
expect(html).toContain('Create Script');
|
||||
expect(html).toContain('Python Code');
|
||||
@@ -110,24 +82,24 @@ describe('mcp-views', () => {
|
||||
|
||||
describe('reviewTemplateHtml', () => {
|
||||
it('returns valid HTML document', () => {
|
||||
const html = reviewTemplateHtml(viewOpts);
|
||||
const html = reviewTemplateHtml();
|
||||
expect(html).toContain('<!DOCTYPE html>');
|
||||
expect(html).toContain('</html>');
|
||||
});
|
||||
|
||||
it('contains App import from ext-apps', () => {
|
||||
const html = reviewTemplateHtml(viewOpts);
|
||||
const html = reviewTemplateHtml();
|
||||
expect(html).toContain('@modelcontextprotocol/ext-apps/app-with-deps');
|
||||
});
|
||||
|
||||
it('contains accept and discard buttons', () => {
|
||||
const html = reviewTemplateHtml(viewOpts);
|
||||
const html = reviewTemplateHtml();
|
||||
expect(html).toContain('acceptProposal()');
|
||||
expect(html).toContain('discardProposal()');
|
||||
});
|
||||
|
||||
it('contains template-specific UI elements', () => {
|
||||
const html = reviewTemplateHtml(viewOpts);
|
||||
const html = reviewTemplateHtml();
|
||||
expect(html).toContain('Review Template');
|
||||
expect(html).toContain('Create Template');
|
||||
expect(html).toContain('Liquid Template');
|
||||
@@ -136,24 +108,24 @@ describe('mcp-views', () => {
|
||||
|
||||
describe('reviewMetadataHtml', () => {
|
||||
it('returns valid HTML document', () => {
|
||||
const html = reviewMetadataHtml(viewOpts);
|
||||
const html = reviewMetadataHtml();
|
||||
expect(html).toContain('<!DOCTYPE html>');
|
||||
expect(html).toContain('</html>');
|
||||
});
|
||||
|
||||
it('contains App import from ext-apps', () => {
|
||||
const html = reviewMetadataHtml(viewOpts);
|
||||
const html = reviewMetadataHtml();
|
||||
expect(html).toContain('@modelcontextprotocol/ext-apps/app-with-deps');
|
||||
});
|
||||
|
||||
it('contains accept and discard buttons', () => {
|
||||
const html = reviewMetadataHtml(viewOpts);
|
||||
const html = reviewMetadataHtml();
|
||||
expect(html).toContain('acceptProposal()');
|
||||
expect(html).toContain('discardProposal()');
|
||||
});
|
||||
|
||||
it('contains metadata-diff UI elements', () => {
|
||||
const html = reviewMetadataHtml(viewOpts);
|
||||
const html = reviewMetadataHtml();
|
||||
expect(html).toContain('Metadata Changes');
|
||||
expect(html).toContain('Apply Changes');
|
||||
expect(html).toContain('diff-table');
|
||||
@@ -162,7 +134,7 @@ describe('mcp-views', () => {
|
||||
});
|
||||
|
||||
it('contains diff formatting function', () => {
|
||||
const html = reviewMetadataHtml(viewOpts);
|
||||
const html = reviewMetadataHtml();
|
||||
expect(html).toContain('function fmt(');
|
||||
expect(html).toContain('diff-old');
|
||||
expect(html).toContain('diff-new');
|
||||
@@ -171,10 +143,10 @@ describe('mcp-views', () => {
|
||||
|
||||
describe('shared behavior', () => {
|
||||
const allViews = [
|
||||
{ name: 'reviewPostHtml', fn: () => reviewPostHtml(viewOpts) },
|
||||
{ name: 'reviewScriptHtml', fn: () => reviewScriptHtml(viewOpts) },
|
||||
{ name: 'reviewTemplateHtml', fn: () => reviewTemplateHtml(viewOpts) },
|
||||
{ name: 'reviewMetadataHtml', fn: () => reviewMetadataHtml(viewOpts) },
|
||||
{ name: 'reviewPostHtml', fn: () => reviewPostHtml() },
|
||||
{ name: 'reviewScriptHtml', fn: () => reviewScriptHtml() },
|
||||
{ name: 'reviewTemplateHtml', fn: () => reviewTemplateHtml() },
|
||||
{ name: 'reviewMetadataHtml', fn: () => reviewMetadataHtml() },
|
||||
];
|
||||
|
||||
it.each(allViews)('$name connects the App on load', ({ fn }) => {
|
||||
|
||||
Reference in New Issue
Block a user