Open the systems-map gist dialog and turn the dock into a default-open TTY.

Plain node activation no longer navigates, case cards stay quiet evidence rows, and the terminal keeps one top-to-bottom scrollport.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-27 00:41:20 +02:00
parent 2635d0c639
commit 55d012cc10
25 changed files with 915 additions and 544 deletions

View File

@@ -53,6 +53,26 @@ describe('TerminalDock', () => {
return fixture.nativeElement.querySelector('input');
}
function session(fixture: ComponentFixture<TerminalDock>): HTMLElement | null {
return fixture.nativeElement.querySelector('.terminal-dock-session');
}
function collapseControl(
fixture: ComponentFixture<TerminalDock>,
locale: AppLocale,
): HTMLButtonElement {
const label = SIGNATURE_COPY[locale].terminal.collapseLabel;
return fixture.nativeElement.querySelector(`[aria-label="${label}"]`);
}
async function collapseViaControl(
fixture: ComponentFixture<TerminalDock>,
locale: AppLocale,
): Promise<void> {
collapseControl(fixture, locale).click();
await flush(fixture);
}
async function openViaTrigger(fixture: ComponentFixture<TerminalDock>): Promise<void> {
trigger(fixture).click();
await flush(fixture);
@@ -73,66 +93,73 @@ describe('TerminalDock', () => {
}
function logText(fixture: ComponentFixture<TerminalDock>): string {
return fixture.nativeElement.querySelector('.terminal-dock-log')?.textContent ?? '';
return session(fixture)?.textContent ?? '';
}
it.each(APP_LOCALES)('renders the collapsed trigger on the server (%s)', async (locale) => {
const fixture = await createFixture(locale, [{ provide: PLATFORM_ID, useValue: 'server' }]);
expect(trigger(fixture)).toBeTruthy();
expect(panel(fixture)).toBeNull();
});
it.each(APP_LOCALES)('opens from the trigger and focuses the input (%s)', async (locale) => {
const fixture = await createFixture(locale);
await openViaTrigger(fixture);
expect(panel(fixture)).toBeTruthy();
expect(document.activeElement).toBe(input(fixture));
expect(fixture.nativeElement.querySelector('[role="dialog"]')).toBeNull();
expect(fixture.nativeElement.querySelector('.command-palette-scrim')).toBeNull();
});
it.each(APP_LOCALES)('toggles the trigger open, closed, then open again (%s)', async (locale) => {
const fixture = await createFixture(locale);
const dock = TestBed.inject(TerminalDockService);
await openViaTrigger(fixture);
expect(panel(fixture)).toBeTruthy();
expect(dock.state()).toBe('expanded');
expect(document.activeElement).toBe(input(fixture));
trigger(fixture).click();
await flush(fixture);
expect(panel(fixture)).toBeNull();
expect(dock.state()).toBe('collapsed');
expect(document.activeElement).toBe(trigger(fixture));
await openViaTrigger(fixture);
expect(panel(fixture)).toBeTruthy();
expect(dock.state()).toBe('expanded');
expect(document.activeElement).toBe(input(fixture));
});
it.each(APP_LOCALES)(
'renders the expanded panel on the server without stealing focus (%s)',
async (locale) => {
const fixture = await createFixture(locale, [{ provide: PLATFORM_ID, useValue: 'server' }]);
expect(trigger(fixture)).toBeTruthy();
expect(trigger(fixture).hasAttribute('hidden')).toBe(true);
expect(panel(fixture)).toBeTruthy();
expect(session(fixture)).toBeTruthy();
expect(session(fixture)?.contains(input(fixture))).toBe(true);
expect(document.activeElement).not.toBe(input(fixture));
},
);
it.each(APP_LOCALES)(
'returns focus to the trigger after a trigger-initiated collapse (%s)',
'defaults to expanded and does not steal focus on load (%s)',
async (locale) => {
const fixture = await createFixture(locale);
await openViaTrigger(fixture);
expect(document.activeElement).toBe(input(fixture));
trigger(fixture).click();
await flush(fixture);
const dock = TestBed.inject(TerminalDockService);
const copy = SIGNATURE_COPY[locale].terminal;
expect(dock.state()).toBe('expanded');
expect(panel(fixture)).toBeTruthy();
expect(trigger(fixture).hasAttribute('hidden')).toBe(true);
expect(document.activeElement).not.toBe(input(fixture));
expect(fixture.nativeElement.querySelector('[role="dialog"]')).toBeNull();
expect(fixture.nativeElement.querySelector('.command-palette-scrim')).toBeNull();
const size = fixture.nativeElement.querySelector('[aria-pressed]') as HTMLButtonElement;
const collapse = collapseControl(fixture, locale);
expect(size.getAttribute('aria-label')).toBe(copy.maximizeLabel);
expect(collapse.getAttribute('aria-label')).toBe(copy.collapseLabel);
expect(size.getAttribute('title')).toBe(copy.maximizeTooltip);
expect(collapse.getAttribute('title')).toBe(copy.collapseTooltip);
expect(size.getAttribute('aria-label')).not.toBe(collapse.getAttribute('aria-label'));
},
);
it.each(APP_LOCALES)(
'hides the trigger while open and shows it again after collapse (%s)',
async (locale) => {
const fixture = await createFixture(locale);
const dock = TestBed.inject(TerminalDockService);
expect(trigger(fixture).hasAttribute('hidden')).toBe(true);
expect(panel(fixture)).toBeTruthy();
await collapseViaControl(fixture, locale);
expect(panel(fixture)).toBeNull();
expect(dock.state()).toBe('collapsed');
expect(trigger(fixture).hasAttribute('hidden')).toBe(false);
expect(document.activeElement).toBe(trigger(fixture));
await openViaTrigger(fixture);
expect(panel(fixture)).toBeTruthy();
expect(dock.state()).toBe('expanded');
expect(trigger(fixture).hasAttribute('hidden')).toBe(true);
expect(document.activeElement).toBe(input(fixture));
},
);
it.each(APP_LOCALES)('moves through collapsed, expanded and maximized (%s)', async (locale) => {
const fixture = await createFixture(locale);
const dock = TestBed.inject(TerminalDockService);
expect(dock.state()).toBe('collapsed');
await openViaTrigger(fixture);
expect(dock.state()).toBe('expanded');
fixture.nativeElement.querySelector('[aria-pressed]')?.click();
@@ -147,39 +174,28 @@ describe('TerminalDock', () => {
expect(dock.state()).toBe('expanded');
});
it.each(APP_LOCALES)(
'keeps one stable maximize name across both aria-pressed states (%s)',
async (locale) => {
const fixture = await createFixture(locale);
await openViaTrigger(fixture);
const label = SIGNATURE_COPY[locale].terminal.maximizeLabel;
const button = fixture.nativeElement.querySelector('[aria-pressed]') as HTMLButtonElement;
expect(button.getAttribute('aria-label')).toBe(label);
expect(button.textContent?.trim()).toBe(label);
expect(button.getAttribute('aria-pressed')).toBe('false');
button.click();
await flush(fixture);
expect(button.getAttribute('aria-label')).toBe(label);
expect(button.textContent?.trim()).toBe(label);
expect(button.getAttribute('aria-pressed')).toBe('true');
},
);
it.each(APP_LOCALES)('opens from Ctrl+K and Meta+K (%s)', async (locale) => {
it.each(APP_LOCALES)('uses distinct maximize and restore names (%s)', async (locale) => {
const fixture = await createFixture(locale);
const copy = SIGNATURE_COPY[locale].terminal;
const button = fixture.nativeElement.querySelector('[aria-pressed]') as HTMLButtonElement;
expect(button.getAttribute('aria-label')).toBe(copy.maximizeLabel);
expect(button.getAttribute('title')).toBe(copy.maximizeTooltip);
button.click();
await flush(fixture);
for (const modifier of [{ ctrlKey: true }, { metaKey: true }] as const) {
if (panel(fixture)) {
input(fixture)?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }),
);
await flush(fixture);
}
expect(button.getAttribute('aria-label')).toBe(copy.restoreLabel);
expect(button.getAttribute('title')).toBe(copy.restoreTooltip);
expect(button.getAttribute('aria-pressed')).toBe('true');
});
it.each(APP_LOCALES)('toggles from Ctrl+K and Meta+K (%s)', async (locale) => {
const fixture = await createFixture(locale);
await flush(fixture);
expect(panel(fixture)).toBeTruthy();
for (const modifier of [{ ctrlKey: true }, { metaKey: true }] as const) {
const event = new KeyboardEvent('keydown', {
key: 'k',
bubbles: true,
@@ -191,13 +207,15 @@ describe('TerminalDock', () => {
await flush(fixture);
expect(prevent).toHaveBeenCalled();
expect(panel(fixture)).toBeTruthy();
}
expect(panel(fixture)).toBeTruthy();
});
it('does not toggle or preventDefault on Ctrl/Cmd+Shift+K', async () => {
const fixture = await createFixture();
await flush(fixture);
expect(panel(fixture)).toBeTruthy();
for (const modifier of [
{ ctrlKey: true, shiftKey: true },
@@ -214,33 +232,33 @@ describe('TerminalDock', () => {
await flush(fixture);
expect(prevent).not.toHaveBeenCalled();
expect(panel(fixture)).toBeNull();
expect(panel(fixture)).toBeTruthy();
}
const openEvent = new KeyboardEvent('keydown', {
const closeEvent = new KeyboardEvent('keydown', {
key: 'k',
bubbles: true,
cancelable: true,
ctrlKey: true,
});
const preventOpen = vi.spyOn(openEvent, 'preventDefault');
document.dispatchEvent(openEvent);
const preventClose = vi.spyOn(closeEvent, 'preventDefault');
document.dispatchEvent(closeEvent);
await flush(fixture);
expect(preventOpen).toHaveBeenCalled();
expect(panel(fixture)).toBeTruthy();
expect(preventClose).toHaveBeenCalled();
expect(panel(fixture)).toBeNull();
const shiftWhileOpen = new KeyboardEvent('keydown', {
const shiftWhileClosed = new KeyboardEvent('keydown', {
key: 'k',
bubbles: true,
cancelable: true,
ctrlKey: true,
shiftKey: true,
});
const preventShift = vi.spyOn(shiftWhileOpen, 'preventDefault');
document.dispatchEvent(shiftWhileOpen);
const preventShift = vi.spyOn(shiftWhileClosed, 'preventDefault');
document.dispatchEvent(shiftWhileClosed);
await flush(fixture);
expect(preventShift).not.toHaveBeenCalled();
expect(panel(fixture)).toBeTruthy();
expect(panel(fixture)).toBeNull();
const metaOpen = new KeyboardEvent('keydown', {
key: 'k',
@@ -252,12 +270,11 @@ describe('TerminalDock', () => {
document.dispatchEvent(metaOpen);
await flush(fixture);
expect(preventMeta).toHaveBeenCalled();
expect(panel(fixture)).toBeNull();
expect(panel(fixture)).toBeTruthy();
});
it.each(APP_LOCALES)('closes on Escape and returns focus to the trigger (%s)', async (locale) => {
const fixture = await createFixture(locale);
await openViaTrigger(fixture);
input(fixture)?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
await flush(fixture);
@@ -269,8 +286,6 @@ describe('TerminalDock', () => {
'closes on Escape from a panel control and returns focus to the trigger (%s)',
async (locale) => {
const fixture = await createFixture(locale);
await openViaTrigger(fixture);
const maximize = fixture.nativeElement.querySelector('[aria-pressed]') as HTMLButtonElement;
maximize.focus();
expect(document.activeElement).toBe(maximize);
@@ -286,7 +301,7 @@ describe('TerminalDock', () => {
it.each(APP_LOCALES)('does not lock scroll or mark the page inert (%s)', async (locale) => {
document.body.style.overflow = 'auto';
const fixture = await createFixture(locale);
await openViaTrigger(fixture);
await flush(fixture);
expect(document.body.style.overflow).toBe('auto');
expect(document.querySelector('.site')?.hasAttribute('inert')).toBeFalsy();
@@ -298,7 +313,6 @@ describe('TerminalDock', () => {
'echoes the prompt, keeps the log and walks command history (%s)',
async (locale) => {
const fixture = await createFixture(locale);
await openViaTrigger(fixture);
const copy = SIGNATURE_COPY[locale].terminal;
await submitQuery(fixture, 'help');
@@ -324,31 +338,32 @@ describe('TerminalDock', () => {
expect(field?.value).toBe('history');
await submitQuery(fixture, 'clear');
expect(logText(fixture).trim()).toBe(copy.clearedMessage);
expect(logText(fixture)).toContain(copy.clearedMessage);
},
);
it.each(APP_LOCALES)('scrolls the log to the newest line after output (%s)', async (locale) => {
const fixture = await createFixture(locale);
await openViaTrigger(fixture);
const logEl = fixture.nativeElement.querySelector('.terminal-dock-log') as HTMLElement;
const scrollTo = vi.fn();
Object.defineProperty(logEl, 'scrollHeight', { configurable: true, get: () => 480 });
Object.defineProperty(logEl, 'scrollTo', { configurable: true, value: scrollTo });
it.each(APP_LOCALES)(
'scrolls the session to keep the prompt visible after output (%s)',
async (locale) => {
const fixture = await createFixture(locale);
const sessionEl = session(fixture) as HTMLElement;
const scrollTo = vi.fn();
Object.defineProperty(sessionEl, 'scrollHeight', { configurable: true, get: () => 480 });
Object.defineProperty(sessionEl, 'scrollTo', { configurable: true, value: scrollTo });
await submitQuery(fixture, 'help');
await submitQuery(fixture, 'help');
expect(scrollTo).toHaveBeenCalled();
expect(scrollTo.mock.calls.at(-1)?.[0]).toEqual(
expect.objectContaining({
top: 480,
}),
);
});
expect(scrollTo).toHaveBeenCalled();
expect(scrollTo.mock.calls.at(-1)?.[0]).toEqual(
expect.objectContaining({
top: 480,
}),
);
},
);
it.each(APP_LOCALES)('walks command history like a conventional shell (%s)', async (locale) => {
const fixture = await createFixture(locale);
await openViaTrigger(fixture);
await submitQuery(fixture, 'help');
await submitQuery(fixture, 'brew');
await submitQuery(fixture, 'rev');
@@ -393,21 +408,51 @@ describe('TerminalDock', () => {
expect(field?.value).toBe('');
});
it.each(APP_LOCALES)('navigates a known target and opens the CV asset (%s)', async (locale) => {
const fixture = await createFixture(locale);
const router = TestBed.inject(Router);
const navigation = TestBed.inject(NavigationService);
const navigate = vi.spyOn(router, 'navigate').mockResolvedValue(true);
const view = TestBed.inject(DOCUMENT).defaultView;
const open = vi.spyOn(view as Window, 'open').mockReturnValue(null);
it.each(APP_LOCALES)(
'restores an in-progress draft after walking off the newest history (%s)',
async (locale) => {
const fixture = await createFixture(locale);
await submitQuery(fixture, 'help');
const field = input(fixture);
field!.value = 'nav';
field!.dispatchEvent(new Event('input', { bubbles: true }));
await flush(fixture);
await openViaTrigger(fixture);
await submitQuery(fixture, 'navigate pitch');
expect(navigate).toHaveBeenCalledWith(navigation.link('pitch'));
field?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }));
await flush(fixture);
expect(field?.value).toBe('help');
await submitQuery(fixture, 'navigate cv');
expect(open).toHaveBeenCalledWith(SITE_CONFIG.cvAssetPath, '_blank', 'noopener,noreferrer');
});
field?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
await flush(fixture);
expect(field?.value).toBe('nav');
},
);
it.each(APP_LOCALES)(
'navigates a known target, jumps to a fragment, and opens the CV asset (%s)',
async (locale) => {
const fixture = await createFixture(locale);
const router = TestBed.inject(Router);
const navigation = TestBed.inject(NavigationService);
const navigate = vi.spyOn(router, 'navigate').mockResolvedValue(true);
const view = TestBed.inject(DOCUMENT).defaultView;
const open = vi.spyOn(view as Window, 'open').mockReturnValue(null);
const copy = SIGNATURE_COPY[locale].terminal;
await submitQuery(fixture, 'navigate pitch');
expect(navigate).toHaveBeenCalledWith(navigation.link('pitch'), undefined);
expect(logText(fixture)).toContain(copy.navigating.replace('{target}', 'pitch'));
await submitQuery(fixture, 'navigate services software');
expect(navigate).toHaveBeenCalledWith(navigation.link('services'), {
fragment: 'software-data',
});
expect(logText(fixture)).toContain(copy.jumping.replace('{target}', 'services software'));
await submitQuery(fixture, 'navigate cv');
expect(open).toHaveBeenCalledWith(SITE_CONFIG.cvAssetPath, '_blank', 'noopener,noreferrer');
},
);
it('registers no document listener on the server', async () => {
const add = vi.spyOn(document, 'addEventListener');