Replace the modal command palette with a nonmodal terminal dock.
Ctrl+K now opens a corner session with a parsed navigate grammar, persistent history, and no page lock. Unknown targets stay on a closed alias table and never become URLs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { pagePath } from './helpers';
|
||||
|
||||
test.describe('keyboard and palette', () => {
|
||||
test.describe('keyboard and terminal dock', () => {
|
||||
test('skip link is first and moves focus to main', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.keyboard.press('Tab');
|
||||
@@ -87,37 +87,32 @@ test.describe('keyboard and palette', () => {
|
||||
const headerBox = await page.locator('.site-header').boundingBox();
|
||||
expect(headerBox, 'header while submenu is open').toBeTruthy();
|
||||
expect(headerBox!.height, 'header height while submenu is open').toBeLessThanOrEqual(200);
|
||||
|
||||
const background = await firstChild.evaluate((element) => {
|
||||
const submenu = element.closest('.submenu');
|
||||
return submenu ? getComputedStyle(submenu).backgroundColor : '';
|
||||
});
|
||||
expect(background, 'submenu background must be fully opaque').toMatch(
|
||||
/^rgb\(\d+,\s*\d+,\s*\d+\)$/,
|
||||
);
|
||||
});
|
||||
|
||||
test('palette opens with Control+K, traps focus, locks scroll and restores on Escape', async ({
|
||||
test('terminal dock opens with Control+K without locking the page and restores on Escape', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/');
|
||||
const trigger = page.locator('.command-palette-trigger');
|
||||
const trigger = page.locator('.terminal-dock-trigger');
|
||||
await trigger.focus();
|
||||
await page.keyboard.press('Control+k');
|
||||
|
||||
const dialog = page.locator('[role="dialog"]');
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(page.locator('.site')).toHaveAttribute('inert', '');
|
||||
expect(await page.evaluate(() => document.body.style.overflow)).toBe('hidden');
|
||||
|
||||
const focusable = dialog.locator(
|
||||
'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled])',
|
||||
);
|
||||
const count = await focusable.count();
|
||||
const first = focusable.first();
|
||||
const last = focusable.nth(count - 1);
|
||||
await last.focus();
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(first).toBeFocused();
|
||||
await page.keyboard.press('Shift+Tab');
|
||||
await expect(last).toBeFocused();
|
||||
const panel = page.locator('.terminal-dock-panel');
|
||||
await expect(panel).toBeVisible();
|
||||
await expect(page.locator('.site')).not.toHaveAttribute('inert');
|
||||
expect(await page.evaluate(() => document.body.style.overflow)).not.toBe('hidden');
|
||||
await expect(page.locator('[role="dialog"]')).toHaveCount(0);
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(dialog).toHaveCount(0);
|
||||
await expect(page.locator('.site')).not.toHaveAttribute('inert');
|
||||
expect(await page.evaluate(() => document.body.style.overflow)).toBe('');
|
||||
await expect(panel).toHaveCount(0);
|
||||
await expect(trigger).toBeFocused();
|
||||
});
|
||||
});
|
||||
|
||||
67
e2e/terminal.e2e.ts
Normal file
67
e2e/terminal.e2e.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { SIGNATURE_COPY } from '../src/app/core/content/signature-copy';
|
||||
|
||||
test.describe('terminal dock', () => {
|
||||
test('walks collapsed, expanded and maximized and runs the grammar', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
const copy = SIGNATURE_COPY.de.terminal;
|
||||
await page.goto('/');
|
||||
|
||||
const trigger = page.locator('.terminal-dock-trigger');
|
||||
const panel = page.locator('.terminal-dock-panel');
|
||||
const input = page.locator('#terminal-dock-input');
|
||||
const log = page.locator('.terminal-dock-log');
|
||||
const maximize = page.locator('.terminal-dock-control[aria-pressed]');
|
||||
|
||||
await expect(trigger).toBeVisible();
|
||||
await expect(panel).toHaveCount(0);
|
||||
|
||||
await trigger.click();
|
||||
await expect(panel).toBeVisible();
|
||||
await expect(page.locator('.terminal-dock-prompt')).toContainText(copy.prompt);
|
||||
|
||||
if (testInfo.project.name === 'mobile') {
|
||||
const box = await panel.boundingBox();
|
||||
expect(box, 'bottom-sheet panel').toBeTruthy();
|
||||
expect(box!.width).toBeGreaterThan(300);
|
||||
} else {
|
||||
await maximize.click();
|
||||
await expect(maximize).toHaveAttribute('aria-pressed', 'true');
|
||||
await maximize.click();
|
||||
await expect(maximize).toHaveAttribute('aria-pressed', 'false');
|
||||
}
|
||||
|
||||
await input.fill('navigate pitch');
|
||||
await input.press('Enter');
|
||||
await page.waitForURL('**/pitch');
|
||||
await expect(page).toHaveURL(/\/pitch$/);
|
||||
await expect(log).toContainText(`${copy.prompt} navigate pitch`);
|
||||
|
||||
await input.press('ArrowUp');
|
||||
await expect(input).toHaveValue('navigate pitch');
|
||||
|
||||
await input.fill('history');
|
||||
await input.press('Enter');
|
||||
await expect(log).toContainText(copy.historyIntro);
|
||||
await expect(log).toContainText('navigate pitch');
|
||||
|
||||
await input.fill('clear');
|
||||
await input.press('Enter');
|
||||
await expect(log).toHaveText(copy.clearedMessage);
|
||||
|
||||
await input.fill('navigate p');
|
||||
await input.press('Tab');
|
||||
await expect(log).toContainText('pitch');
|
||||
await expect(log).toContainText('projects');
|
||||
|
||||
await input.fill('navigate nowhere');
|
||||
await input.press('Enter');
|
||||
await expect(log).toContainText(copy.validTargetsLabel);
|
||||
await expect(log).toContainText('pitch');
|
||||
|
||||
await input.fill('nav');
|
||||
await input.press('Tab');
|
||||
await expect(input).toHaveValue('navigate');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user