Make the terminal dock toggle, scroll, and remember commands honestly.
The desktop trigger now collapses through the shared toggle, the log stays on the newest line, Shift+K is left to the browser, history walks like a shell, and maximize keeps one accessible name. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -91,6 +91,42 @@ describe('TerminalDock', () => {
|
||||
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)(
|
||||
'returns focus to the trigger after a trigger-initiated collapse (%s)',
|
||||
async (locale) => {
|
||||
const fixture = await createFixture(locale);
|
||||
await openViaTrigger(fixture);
|
||||
expect(document.activeElement).toBe(input(fixture));
|
||||
|
||||
trigger(fixture).click();
|
||||
await flush(fixture);
|
||||
|
||||
expect(panel(fixture)).toBeNull();
|
||||
expect(document.activeElement).toBe(trigger(fixture));
|
||||
},
|
||||
);
|
||||
|
||||
it.each(APP_LOCALES)('moves through collapsed, expanded and maximized (%s)', async (locale) => {
|
||||
const fixture = await createFixture(locale);
|
||||
const dock = TestBed.inject(TerminalDockService);
|
||||
@@ -111,6 +147,27 @@ 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) => {
|
||||
const fixture = await createFixture(locale);
|
||||
await flush(fixture);
|
||||
@@ -138,6 +195,66 @@ describe('TerminalDock', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('does not toggle or preventDefault on Ctrl/Cmd+Shift+K', async () => {
|
||||
const fixture = await createFixture();
|
||||
await flush(fixture);
|
||||
|
||||
for (const modifier of [
|
||||
{ ctrlKey: true, shiftKey: true },
|
||||
{ metaKey: true, shiftKey: true },
|
||||
] as const) {
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key: 'k',
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
...modifier,
|
||||
});
|
||||
const prevent = vi.spyOn(event, 'preventDefault');
|
||||
document.dispatchEvent(event);
|
||||
await flush(fixture);
|
||||
|
||||
expect(prevent).not.toHaveBeenCalled();
|
||||
expect(panel(fixture)).toBeNull();
|
||||
}
|
||||
|
||||
const openEvent = new KeyboardEvent('keydown', {
|
||||
key: 'k',
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
ctrlKey: true,
|
||||
});
|
||||
const preventOpen = vi.spyOn(openEvent, 'preventDefault');
|
||||
document.dispatchEvent(openEvent);
|
||||
await flush(fixture);
|
||||
expect(preventOpen).toHaveBeenCalled();
|
||||
expect(panel(fixture)).toBeTruthy();
|
||||
|
||||
const shiftWhileOpen = new KeyboardEvent('keydown', {
|
||||
key: 'k',
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
ctrlKey: true,
|
||||
shiftKey: true,
|
||||
});
|
||||
const preventShift = vi.spyOn(shiftWhileOpen, 'preventDefault');
|
||||
document.dispatchEvent(shiftWhileOpen);
|
||||
await flush(fixture);
|
||||
expect(preventShift).not.toHaveBeenCalled();
|
||||
expect(panel(fixture)).toBeTruthy();
|
||||
|
||||
const metaOpen = new KeyboardEvent('keydown', {
|
||||
key: 'k',
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
metaKey: true,
|
||||
});
|
||||
const preventMeta = vi.spyOn(metaOpen, 'preventDefault');
|
||||
document.dispatchEvent(metaOpen);
|
||||
await flush(fixture);
|
||||
expect(preventMeta).toHaveBeenCalled();
|
||||
expect(panel(fixture)).toBeNull();
|
||||
});
|
||||
|
||||
it.each(APP_LOCALES)('closes on Escape and returns focus to the trigger (%s)', async (locale) => {
|
||||
const fixture = await createFixture(locale);
|
||||
await openViaTrigger(fixture);
|
||||
@@ -211,6 +328,71 @@ describe('TerminalDock', () => {
|
||||
},
|
||||
);
|
||||
|
||||
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 });
|
||||
|
||||
await submitQuery(fixture, 'help');
|
||||
|
||||
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');
|
||||
|
||||
const field = input(fixture);
|
||||
expect(field?.value).toBe('');
|
||||
|
||||
field?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
|
||||
await flush(fixture);
|
||||
expect(field?.value).toBe('');
|
||||
|
||||
field?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }));
|
||||
await flush(fixture);
|
||||
expect(field?.value).toBe('rev');
|
||||
|
||||
field?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }));
|
||||
await flush(fixture);
|
||||
expect(field?.value).toBe('brew');
|
||||
|
||||
field?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }));
|
||||
await flush(fixture);
|
||||
expect(field?.value).toBe('help');
|
||||
|
||||
field?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }));
|
||||
await flush(fixture);
|
||||
expect(field?.value).toBe('help');
|
||||
|
||||
field?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
|
||||
await flush(fixture);
|
||||
expect(field?.value).toBe('brew');
|
||||
|
||||
field?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
|
||||
await flush(fixture);
|
||||
expect(field?.value).toBe('rev');
|
||||
|
||||
field?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
|
||||
await flush(fixture);
|
||||
expect(field?.value).toBe('');
|
||||
|
||||
field?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
|
||||
await flush(fixture);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user