feat(topbar): search button opens the command palette; Preview head parity
- Icon button with tooltip 'Search (Ctrl/Cmd+K)' calls openCommandPalette() - Preview passes fontScanHtml: composedBody and trims metaTitle so the Preview head matches the published head Refs #49 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
db54341006
commit
ebf75ebfe0
2 files changed
+56
-3
No files matched your search
@@ -34,12 +34,12 @@ import { act } from 'react-dom/test-utils';
|
||||
|
||||
// #54: what the pre-publish checks see. Default (no pages) means no checks, so
|
||||
// every pre-existing test publishes directly.
|
||||
const checkFixture: { pages: any[]; liveState: string; activePageId: string } = { pages: [], liveState: '{}', activePageId: 'home' };
|
||||
const checkFixture: { pages: any[]; liveState: string; activePageId: string; footerState: string | null } = { pages: [], liveState: '{}', activePageId: 'home', footerState: null };
|
||||
vi.mock('../../state/PageContext', () => ({
|
||||
usePages: () => ({
|
||||
pages: checkFixture.pages,
|
||||
headerPage: { id: '__header__', name: 'Header', slug: '__header__', craftState: null },
|
||||
footerPage: { id: '__footer__', name: 'Footer', slug: '__footer__', craftState: null },
|
||||
footerPage: { id: '__footer__', name: 'Footer', slug: '__footer__', craftState: checkFixture.footerState },
|
||||
activePageId: checkFixture.activePageId,
|
||||
switchPage: vi.fn(),
|
||||
}),
|
||||
@@ -71,6 +71,8 @@ vi.mock('../../hooks/useWhpApi', () => ({
|
||||
vi.mock('./TemplateModal', () => ({ TemplateModal: () => null }));
|
||||
vi.mock('./HeadCodeModal', () => ({ HeadCodeModal: () => null }));
|
||||
vi.mock('../sitesmith/SitesmithButton', () => ({ SitesmithButton: () => null }));
|
||||
const openPaletteMock = vi.fn();
|
||||
vi.mock('../command-palette/CommandPalette', () => ({ openCommandPalette: () => openPaletteMock() }));
|
||||
|
||||
import { TopBar } from './TopBar';
|
||||
import { EditorConfigProvider } from '../../state/EditorConfigContext';
|
||||
@@ -130,6 +132,8 @@ afterEach(() => {
|
||||
checkFixture.pages = [];
|
||||
checkFixture.liveState = '{}';
|
||||
checkFixture.activePageId = 'home';
|
||||
checkFixture.footerState = null;
|
||||
openPaletteMock.mockReset();
|
||||
});
|
||||
|
||||
describe('TopBar publish-warnings wiring', () => {
|
||||
@@ -315,3 +319,41 @@ describe('TopBar pre-publish checklist', () => {
|
||||
expect(container.querySelector('[data-testid="publish-warnings"]')!.textContent).toContain('Generated sitemap.xml and robots.txt.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TopBar search button (#49 integration)', () => {
|
||||
test('opens the command palette and carries the shortcut tooltip', () => {
|
||||
render();
|
||||
const btn = container.querySelector<HTMLButtonElement>('button[aria-label="Search"]');
|
||||
expect(btn).not.toBeNull();
|
||||
expect(btn!.getAttribute('data-tooltip')).toBe('Search (Ctrl/Cmd+K)');
|
||||
act(() => { btn!.dispatchEvent(new MouseEvent('click', { bubbles: true })); });
|
||||
expect(openPaletteMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TopBar Preview head parity (item 14)', () => {
|
||||
const rootState = (style: Record<string, string> = {}) => JSON.stringify({
|
||||
ROOT: { type: { resolvedName: 'Container' }, isCanvas: true, props: { tag: 'div', style }, displayName: 'Container', custom: {}, hidden: false, nodes: [], linkedNodes: {} },
|
||||
});
|
||||
|
||||
test('scans the composed header+body+footer for preset fonts and trims metaTitle', async () => {
|
||||
checkFixture.liveState = rootState();
|
||||
checkFixture.footerState = rootState({ fontFamily: 'Merriweather, serif' });
|
||||
checkFixture.pages = [{ id: 'home', name: 'Home', slug: 'index', craftState: null, seo: { metaTitle: ' ' } }];
|
||||
let blob: Blob | null = null;
|
||||
const createUrl = vi.spyOn(URL, 'createObjectURL').mockImplementation((b) => { blob = b as Blob; return 'blob:x'; });
|
||||
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
|
||||
try {
|
||||
render();
|
||||
const btn = container.querySelector<HTMLButtonElement>('button[aria-label="Preview"]')!;
|
||||
await act(async () => { btn.dispatchEvent(new MouseEvent('click', { bubbles: true })); });
|
||||
await vi.waitFor(() => expect(blob).not.toBeNull());
|
||||
const html = await blob!.text();
|
||||
expect(html).toContain('family=Inter:wght@300;400;500;600;700&family=Merriweather:wght@300;400;700&display=swap');
|
||||
expect(html).toContain('<title>Home</title>');
|
||||
} finally {
|
||||
createUrl.mockRestore();
|
||||
openSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -24,6 +24,7 @@ import { SitesmithButton } from '../sitesmith/SitesmithButton';
|
||||
import { useSitesmithModal } from '../../state/SitesmithContext';
|
||||
import { useRegisterCommands } from '../../editor/commands';
|
||||
import { TopBarCommandMenu } from './TopBarCommandMenu';
|
||||
import { openCommandPalette } from '../command-palette/CommandPalette';
|
||||
|
||||
interface TopBarProps {
|
||||
/** Item 10: canvas dashed guide outlines, default ON. */
|
||||
@@ -306,7 +307,7 @@ export const TopBar: React.FC<TopBarProps> = ({ showGuides, onToggleGuides }) =>
|
||||
// html-export's buildSeoMeta/og:title fallback both key off it.
|
||||
const activePage = pages.find((p) => p.id === activePageId);
|
||||
const seo = activePage?.seo;
|
||||
const title = seo?.metaTitle || activePage?.name || whpConfig?.siteName || 'Preview';
|
||||
const title = seo?.metaTitle?.trim() || activePage?.name || whpConfig?.siteName || 'Preview';
|
||||
|
||||
const result = exportToHtml(serialized, {
|
||||
title,
|
||||
@@ -323,6 +324,8 @@ export const TopBar: React.FC<TopBarProps> = ({ showGuides, onToggleGuides }) =>
|
||||
? `https://${whpConfig.siteDomain}/${activePage.slug === 'index' ? '' : activePage.slug}`
|
||||
: undefined,
|
||||
design,
|
||||
// Fonts: scan header+body+footer like the published head does (head parity).
|
||||
fontScanHtml: composedBody,
|
||||
});
|
||||
|
||||
// Replace the body in the full document with our composed version.
|
||||
@@ -528,6 +531,14 @@ export const TopBar: React.FC<TopBarProps> = ({ showGuides, onToggleGuides }) =>
|
||||
>
|
||||
<i className="fa fa-code" />
|
||||
</button>
|
||||
<button
|
||||
className="topbar-btn icon-only"
|
||||
aria-label="Search"
|
||||
data-tooltip="Search (Ctrl/Cmd+K)"
|
||||
onClick={() => openCommandPalette()}
|
||||
>
|
||||
<i className="fa fa-search" aria-hidden />
|
||||
</button>
|
||||
<button className="topbar-btn icon-only" aria-label="Preview" data-tooltip="Preview" onClick={handlePreview}>
|
||||
<i className="fa fa-eye" />
|
||||
</button>
|
||||
|
||||
Reference in new issue
Block a user