diff --git a/docs/CONFIG.md b/docs/CONFIG.md index dea7d04a..455bd24b 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -38,6 +38,7 @@ interface ConfigModel { openNewTab: string; commandConfirmation: string; pinContextHint: string; + contextSearchPlaceholder: string; dragOverlayText: string; }; // Options to show up on the overlay feedback form @@ -217,6 +218,11 @@ Default tab title text if it is not set through store data for that tab.

pinContextHint

+ +## contextSearchPlaceholder +

+ contextSearchPlaceholder +

--- ## dragOverlayText diff --git a/docs/img/texts/contextSearchPlaceholder.png b/docs/img/texts/contextSearchPlaceholder.png new file mode 100644 index 00000000..ae0dd02d Binary files /dev/null and b/docs/img/texts/contextSearchPlaceholder.png differ diff --git a/src/components/chat-item/chat-item-form-items.ts b/src/components/chat-item/chat-item-form-items.ts index d044735b..7d10a344 100644 --- a/src/components/chat-item/chat-item-form-items.ts +++ b/src/components/chat-item/chat-item-form-items.ts @@ -375,4 +375,16 @@ export class ChatItemFormItemsWrapper { }); return valueMap; }; + + clearAndFocusFirstTextInput = (): void => { + const firstOptionId = Object.keys(this.options)[0]; + if (firstOptionId != null) { + const firstOption = this.options[firstOptionId]; + // Clear first text input + if ('clear' in firstOption && typeof firstOption.clear === 'function') { + firstOption.clear(); + firstOption.focus(); + } + } + }; } diff --git a/src/components/chat-item/chat-prompt-input.ts b/src/components/chat-item/chat-prompt-input.ts index b90692ea..233479f2 100644 --- a/src/components/chat-item/chat-prompt-input.ts +++ b/src/components/chat-item/chat-prompt-input.ts @@ -671,7 +671,7 @@ export class ChatPromptInput { type: 'textinput', icon: MynahIcons.SEARCH, id: 'search', - placeholder: 'Search context', + placeholder: Config.getInstance().config.texts.contextSearchPlaceholder, autoFocus: true, }, ], @@ -717,7 +717,7 @@ export class ChatPromptInput { } else { this.quickPickItemsSelectorContainer.update({ list: detailedListItemsGroup - }); + }, false, true); } const headerInfo: QuickActionCommandsHeader = MynahUITabsStore.getInstance().getTabDataStore(this.props.tabId).getValue('quickActionCommandsHeader'); diff --git a/src/components/chat-item/prompt-input/prompt-top-bar/prompt-top-bar.ts b/src/components/chat-item/prompt-input/prompt-top-bar/prompt-top-bar.ts index 0eebff4e..b8ffec84 100644 --- a/src/components/chat-item/prompt-input/prompt-top-bar/prompt-top-bar.ts +++ b/src/components/chat-item/prompt-input/prompt-top-bar/prompt-top-bar.ts @@ -123,6 +123,7 @@ export class PromptTopBar { } if (this.titleButton == null) { this.titleButton = new Button({ + testId: testIds.prompt.topBarTitle, onClick: () => { this.props.onTopBarTitleClick?.(); }, diff --git a/src/components/detailed-list/detailed-list.ts b/src/components/detailed-list/detailed-list.ts index 726b4358..927af5c9 100644 --- a/src/components/detailed-list/detailed-list.ts +++ b/src/components/detailed-list/detailed-list.ts @@ -304,7 +304,11 @@ export class DetailedListWrapper { return this.allSelectableDetailedListElements[this.activeTargetElementIndex].getItem(); }; - public readonly update = (detailedList: DetailedList, preserveScrollPosition?: boolean): void => { + public readonly update = (detailedList: DetailedList, preserveScrollPosition?: boolean, clearFilter?: boolean): void => { + if (this.filterForm != null && clearFilter === true) { + this.filterForm.clearAndFocusFirstTextInput(); + } + if (detailedList.header != null) { this.props.detailedList.header = detailedList.header; this.headerContainer.update({ diff --git a/src/components/form-items/text-input.ts b/src/components/form-items/text-input.ts index f08f12c0..09a6aea1 100644 --- a/src/components/form-items/text-input.ts +++ b/src/components/form-items/text-input.ts @@ -36,6 +36,8 @@ export abstract class TextInputAbstract { render: ExtendedHTMLElement; setValue = (value: string): void => {}; getValue = (): string => ''; + clear = (): void => {}; + focus = (): void => {}; setEnabled = (enabled: boolean): void => {}; checkValidation = (): void => {}; } @@ -125,11 +127,7 @@ export class TextInputInternal extends TextInputAbstract { ] }); - if (this.props.autoFocus === true) { - setTimeout(() => { - this.inputElement.focus(); - }, 250); - } + this.focus(); } setValue = (value: string): void => { @@ -140,6 +138,18 @@ export class TextInputInternal extends TextInputAbstract { return this.inputElement.value; }; + focus = (): void => { + if (this.props.autoFocus === true) { + setTimeout(() => { + this.inputElement.focus(); + }, 250); + } + }; + + clear = (): void => { + this.setValue(''); + }; + setEnabled = (enabled: boolean): void => { if (enabled) { this.inputElement.removeAttribute('disabled'); @@ -161,6 +171,8 @@ export class TextInput extends TextInputAbstract { setValue = (value: string): void => {}; getValue = (): string => ''; + clear = (): void => {}; + focus = (): void => {}; setEnabled = (enabled: boolean): void => {}; checkValidation = (): void => {}; } diff --git a/src/helper/config.ts b/src/helper/config.ts index 17bc7449..10e39dd8 100644 --- a/src/helper/config.ts +++ b/src/helper/config.ts @@ -82,6 +82,7 @@ const configDefaults: ConfigFullModel = { openNewTab: 'New tab', commandConfirmation: 'Press enter to continue', pinContextHint: 'Pin context with \u2325 Enter', + contextSearchPlaceholder: 'Search context', dragOverlayText: 'Add image to context', }, }; diff --git a/src/helper/test-ids.ts b/src/helper/test-ids.ts index 78d0a536..7dd1d868 100644 --- a/src/helper/test-ids.ts +++ b/src/helper/test-ids.ts @@ -22,6 +22,7 @@ export default { progress: 'prompt-input-progress-wrapper', label: 'prompt-input-label', topBar: 'prompt-input-top-bar', + topBarTitle: 'prompt-input-top-bar-title', topBarButton: 'prompt-input-top-bar-button', topBarContextPill: 'prompt-input-top-bar-context-pill', topBarContextTooltip: 'prompt-input-top-bar-context-tooltip', diff --git a/src/static.ts b/src/static.ts index 22bcb669..ca2ce9b0 100644 --- a/src/static.ts +++ b/src/static.ts @@ -761,6 +761,7 @@ export interface ConfigTexts { openNewTab: string; commandConfirmation: string; pinContextHint: string; + contextSearchPlaceholder: string; dragOverlayText: string; } diff --git a/ui-tests/__snapshots__/chromium/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-children-shown.png b/ui-tests/__snapshots__/chromium/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-children-shown.png new file mode 100644 index 00000000..ffbb66fa Binary files /dev/null and b/ui-tests/__snapshots__/chromium/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-children-shown.png differ diff --git a/ui-tests/__snapshots__/chromium/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-filtered.png b/ui-tests/__snapshots__/chromium/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-filtered.png new file mode 100644 index 00000000..a2b1ed28 Binary files /dev/null and b/ui-tests/__snapshots__/chromium/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-filtered.png differ diff --git a/ui-tests/__snapshots__/chromium/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-navigation.png b/ui-tests/__snapshots__/chromium/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-navigation.png new file mode 100644 index 00000000..b337751b Binary files /dev/null and b/ui-tests/__snapshots__/chromium/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-navigation.png differ diff --git a/ui-tests/__snapshots__/chromium/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-nested-children-keyboard.png b/ui-tests/__snapshots__/chromium/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-nested-children-keyboard.png new file mode 100644 index 00000000..745e6d4b Binary files /dev/null and b/ui-tests/__snapshots__/chromium/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-nested-children-keyboard.png differ diff --git a/ui-tests/__snapshots__/chromium/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-nested-children-shown.png b/ui-tests/__snapshots__/chromium/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-nested-children-shown.png new file mode 100644 index 00000000..cea1bd71 Binary files /dev/null and b/ui-tests/__snapshots__/chromium/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-nested-children-shown.png differ diff --git a/ui-tests/__snapshots__/chromium/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-opened.png b/ui-tests/__snapshots__/chromium/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-opened.png new file mode 100644 index 00000000..7fe6fd33 Binary files /dev/null and b/ui-tests/__snapshots__/chromium/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-opened.png differ diff --git a/ui-tests/__snapshots__/webkit/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-children-shown.png b/ui-tests/__snapshots__/webkit/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-children-shown.png new file mode 100644 index 00000000..86395dc0 Binary files /dev/null and b/ui-tests/__snapshots__/webkit/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-children-shown.png differ diff --git a/ui-tests/__snapshots__/webkit/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-filtered.png b/ui-tests/__snapshots__/webkit/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-filtered.png new file mode 100644 index 00000000..db84e0bd Binary files /dev/null and b/ui-tests/__snapshots__/webkit/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-filtered.png differ diff --git a/ui-tests/__snapshots__/webkit/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-navigation.png b/ui-tests/__snapshots__/webkit/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-navigation.png new file mode 100644 index 00000000..02be72bc Binary files /dev/null and b/ui-tests/__snapshots__/webkit/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-navigation.png differ diff --git a/ui-tests/__snapshots__/webkit/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-nested-children-keyboard.png b/ui-tests/__snapshots__/webkit/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-nested-children-keyboard.png new file mode 100644 index 00000000..fa274e7d Binary files /dev/null and b/ui-tests/__snapshots__/webkit/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-nested-children-keyboard.png differ diff --git a/ui-tests/__snapshots__/webkit/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-nested-children-shown.png b/ui-tests/__snapshots__/webkit/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-nested-children-shown.png new file mode 100644 index 00000000..ecd0e1f9 Binary files /dev/null and b/ui-tests/__snapshots__/webkit/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-nested-children-shown.png differ diff --git a/ui-tests/__snapshots__/webkit/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-opened.png b/ui-tests/__snapshots__/webkit/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-opened.png new file mode 100644 index 00000000..d4e1f48b Binary files /dev/null and b/ui-tests/__snapshots__/webkit/Open-MynahUI-Prompt-Top-Bar-should-show-context-menu-when-clicking-top-bar-title/context-selector-opened.png differ diff --git a/ui-tests/__test__/flows/prompt-top-bar/index.ts b/ui-tests/__test__/flows/prompt-top-bar/index.ts index 67c7d387..800ba88c 100644 --- a/ui-tests/__test__/flows/prompt-top-bar/index.ts +++ b/ui-tests/__test__/flows/prompt-top-bar/index.ts @@ -1,9 +1,11 @@ import { renderPromptTopBar } from './render-prompt-top-bar'; import { promptTopBarTooltip } from './prompt-top-bar-tooltip'; import { promptTopBarButtonOverlay } from './prompt-top-bar-button-overlay'; +import { promptTopBarTitle } from './prompt-top-bar-title'; export { renderPromptTopBar, promptTopBarTooltip, promptTopBarButtonOverlay, + promptTopBarTitle, }; diff --git a/ui-tests/__test__/flows/prompt-top-bar/prompt-top-bar-title.ts b/ui-tests/__test__/flows/prompt-top-bar/prompt-top-bar-title.ts new file mode 100644 index 00000000..74f6149a --- /dev/null +++ b/ui-tests/__test__/flows/prompt-top-bar/prompt-top-bar-title.ts @@ -0,0 +1,367 @@ +import { expect, Page } from 'playwright/test'; +import { getSelector, waitForAnimationEnd, justWait } from '../../helpers'; +import testIds from '../../../../src/helper/test-ids'; + +export const promptTopBarTitle = async (page: Page): Promise => { + // Set up the prompt top bar with a title and context commands + await page.evaluate(() => { + const selectedTabId = window.mynahUI.getSelectedTabId(); + if (selectedTabId != null) { + window.mynahUI.updateStore(selectedTabId, { + promptTopBarTitle: '@Pin Context', + contextCommands: [ + { + commands: [ + { + command: '@workspace', + placeholder: 'Yes, you selected workspace :P', + description: 'Reference all code in workspace.', + }, + { + command: 'image', + icon: 'image', + description: 'Add an image to the context', + placeholder: 'Select an image file' + }, + { + command: 'folder', + icon: 'folder', + children: [ + { + groupName: 'Folders', + commands: [ + { + command: 'src', + icon: 'folder', + children: [ + { + groupName: 'src/', + commands: [ + { + command: 'index.ts', + icon: 'file', + }, + ], + }, + ], + }, + { + command: 'main', + description: './src/', + icon: 'folder', + }, + { + command: 'components', + description: './src/', + icon: 'folder', + }, + { + command: 'helper', + description: './src/', + icon: 'folder', + }, + { + command: 'src', + description: './example/', + icon: 'folder', + }, + ], + }, + ], + placeholder: 'Mention a specific folder', + description: 'Include entire folder as context', + }, + { + command: 'file', + icon: 'file', + children: [ + { + groupName: 'Files', + commands: [ + { + command: 'monarch.ts', + description: 'spring-boot-template/.github/workflows/p-openapi.yaml', + icon: 'file', + }, + { + command: 'main.ts', + description: './src/', + icon: 'file', + }, + { + command: 'button.ts', + description: './src/components/', + icon: 'file', + }, + { + command: 'ex-dom.ts', + description: './src/helper/', + icon: 'file', + }, + { + command: 'dom.ts', + description: './src/helper/', + icon: 'file', + }, + { + command: '_dark.scss', + description: './src/styles/', + icon: 'file', + // add route just to check if it returns back + route: [ 'src', 'styles' ], + }, + ], + }, + ], + placeholder: 'Mention a specific file', + description: 'Add a file to context', + }, + { + command: 'symbols', + icon: 'code-block', + children: [ + { + groupName: 'Symbols', + commands: [ + { + command: 'DomBuilder', + icon: 'code-block', + description: 'The DomGeneration function in dom.ts file', + }, + ...Array(10) + .fill(null) + .map((_, i) => ({ + command: `item${i}`, + description: `./src/${i}`, + icon: 'code-block' as any, + })), + ], + }, + ], + placeholder: 'Select a symbol', + description: 'After that mention a specific file/folder, or leave blank for full project!', + }, + { + command: 'prompts', + icon: 'flash', + description: 'Saved prompts, to reuse them in your current prompt', + children: [ + { + groupName: 'Prompts', + actions: [ + { + id: 'add-new-prompt', + icon: 'plus', + text: 'Add', + description: 'Add new prompt', + }, + ], + commands: [ + { + command: 'python_expert', + icon: 'chat', + description: 'Expert on python stuff', + }, + { + command: 'javascript_expert', + icon: 'chat', + description: 'Expert on Javascript and typescript', + }, + { + command: 'Add Prompt', + icon: 'plus', + }, + ], + }, + ], + }, + ], + }, + ], + }); + } + }); + + await waitForAnimationEnd(page); + + // Verify the top bar title is visible + const topBarTitle = page.locator(getSelector(testIds.prompt.topBarTitle)); + expect(await topBarTitle.isVisible()).toBeTruthy(); + + // Click on the top bar title to open the context selector + await topBarTitle.click(); + await waitForAnimationEnd(page); + + // Wait for the overlay to appear + await justWait(500); + + // Verify the quick picks overlay is visible + const quickPicksWrapper = page.locator(getSelector(testIds.prompt.quickPicksWrapper)); + expect(await quickPicksWrapper.isVisible()).toBeTruthy(); + + // Take screenshot of the opened context selector + expect(await quickPicksWrapper.screenshot()).toMatchSnapshot('context-selector-opened.png'); + + // Verify that we can see the groups and items + const quickPickItems = page.locator(getSelector(testIds.prompt.quickPickItem)); + expect(await quickPickItems.count()).toBeGreaterThan(0); + + // Verify the search filter input is present and focused + const searchInput = page.getByRole('textbox', { name: 'Search context' }); + await expect(searchInput).toBeFocused(); + expect(await searchInput.isVisible()).toBeTruthy(); + expect(await searchInput.getAttribute('placeholder')).toBe('Search context'); + + // Type in the search filter to filter items + await searchInput.fill('folder'); + await waitForAnimationEnd(page); + + // Take screenshot after filtering + expect(await quickPicksWrapper.screenshot()).toMatchSnapshot('context-selector-filtered.png'); + + // Verify that only folder-related items are shown + const filteredItems = await quickPickItems.allInnerTexts(); + expect(filteredItems.some(text => text.includes('folder'))).toBeTruthy(); + expect(filteredItems.some(text => text.includes('file'))).toBeFalsy(); + + // Click on the folder item (which has children) + const folderItem = page.locator(getSelector(testIds.prompt.quickPickItem)).filter({ hasText: 'folder' }); + await folderItem.click(); + await page.mouse.move(0, 0); // move cursor away so hover effect is removed + await waitForAnimationEnd(page); + + // Verify that the children are now shown (Folders group) + const childItems = page.locator(getSelector(testIds.prompt.quickPickItem)); + const childTexts = await childItems.allInnerTexts(); + expect(childTexts.some(text => text.includes('src'))).toBeTruthy(); + expect(childTexts.some(text => text.includes('main'))).toBeTruthy(); + expect(childTexts.some(text => text.includes('components'))).toBeTruthy(); + + // Take screenshot showing the children + expect(await quickPicksWrapper.screenshot()).toMatchSnapshot('context-selector-children-shown.png'); + + // Select the 'src' item which has further children + const srcItem = page.locator(getSelector(testIds.prompt.quickPickItem)).filter({ hasText: 'src' }).first(); + await srcItem.click(); + await waitForAnimationEnd(page); + + // Verify that the nested children are now shown (src/ group with index.ts) + const nestedChildItems = page.locator(getSelector(testIds.prompt.quickPickItem)); + const nestedChildTexts = await nestedChildItems.allInnerTexts(); + expect(nestedChildTexts.some(text => text.includes('index.ts'))).toBeTruthy(); + + // Hover over index.ts item to verify hover effect in screenshot + const indexTsItem = page.locator(getSelector(testIds.prompt.quickPickItem)).filter({ hasText: 'index.ts' }).first(); + await indexTsItem.hover(); + await waitForAnimationEnd(page); + + // Take screenshot showing the nested children + expect(await quickPicksWrapper.screenshot()).toMatchSnapshot('context-selector-nested-children-shown.png'); + + // Select index.ts context item + await indexTsItem.click(); + await waitForAnimationEnd(page); + + // Verify the overlay is closed after selection + expect(await page.locator(getSelector(testIds.prompt.quickPicksWrapper)).count()).toBe(0); + + // Verify that the selected context item appears in the top bar + const contextPills = page.locator(getSelector(testIds.prompt.topBarContextPill)); + expect(await contextPills.count()).toBe(1); + + const pillText = await contextPills.first().innerText(); + expect(pillText).toContain('index.ts'); + + // Test keyboard navigation to select nested children + await topBarTitle.click(); + await waitForAnimationEnd(page); + + // Clear search and navigate to folder item using arrow keys + const searchInputKeyboard = page.getByRole('textbox', { name: 'Search context' }); + await searchInputKeyboard.clear(); + await searchInputKeyboard.fill('folder'); + await waitForAnimationEnd(page); + + // Use arrow keys to navigate to the folder item and press Enter to select it + await page.keyboard.press('ArrowDown'); // Navigate to first item (folder) + await waitForAnimationEnd(page); + await page.keyboard.press('Enter'); // Select folder item to view its children + await waitForAnimationEnd(page); + + // Verify that the children are now shown (Folders group) + const keyboardChildItems = page.locator(getSelector(testIds.prompt.quickPickItem)); + const keyboardChildTexts = await keyboardChildItems.allInnerTexts(); + expect(keyboardChildTexts.some(text => text.includes('src'))).toBeTruthy(); + expect(keyboardChildTexts.some(text => text.includes('main'))).toBeTruthy(); + expect(keyboardChildTexts.some(text => text.includes('components'))).toBeTruthy(); + + // Navigate to 'src' item using arrow keys and select it to view nested children + await page.keyboard.press('ArrowDown'); // Navigate to src item + await waitForAnimationEnd(page); + await page.keyboard.press('Enter'); // Select src item to view its nested children + await waitForAnimationEnd(page); + + // Verify that the nested children are now shown using keyboard navigation + const keyboardNestedChildItems = page.locator(getSelector(testIds.prompt.quickPickItem)); + const keyboardNestedChildTexts = await keyboardNestedChildItems.allInnerTexts(); + expect(keyboardNestedChildTexts.some(text => text.includes('index.ts'))).toBeTruthy(); + + // Take screenshot showing the nested children selected via keyboard + expect(await quickPicksWrapper.screenshot()).toMatchSnapshot('context-selector-nested-children-keyboard.png'); + + // Navigate to index.ts and select it using keyboard + await page.keyboard.press('ArrowDown'); // Navigate to index.ts + await waitForAnimationEnd(page); + await page.keyboard.press('Enter'); // Select index.ts + await waitForAnimationEnd(page); + + // Verify the overlay is closed after keyboard selection + expect(await page.locator(getSelector(testIds.prompt.quickPicksWrapper)).count()).toBe(0); + + // Verify that another context item was added to the top bar (should now have 2 items total) + expect(await contextPills.count()).toBe(2); + + const keyboardPillText = await contextPills.last().innerText(); + expect(keyboardPillText).toContain('index.ts'); + + // Test selecting a different type of item - open context selector again + await topBarTitle.click(); + await waitForAnimationEnd(page); + + // Search for and select a file item + const searchInput2 = page.getByRole('textbox', { name: 'Search context' }); + await expect(searchInput2).toBeFocused(); + await searchInput2.fill('file'); + await waitForAnimationEnd(page); + + // Click on the file item + const fileItem = page.locator(getSelector(testIds.prompt.quickPickItem)).filter({ hasText: 'file' }); + await fileItem.click(); + await waitForAnimationEnd(page); + + // Select one of the file items (main.ts) + const mainTsItem = page.locator(getSelector(testIds.prompt.quickPickItem)).filter({ hasText: 'main.ts' }); + await mainTsItem.click(); + await waitForAnimationEnd(page); + + // Verify another context item was added (should now have 3 total: first index.ts, keyboard index.ts, and main.ts) + expect(await contextPills.count()).toBe(3); + + // Test keyboard navigation by opening the context selector again + await topBarTitle.click(); + await waitForAnimationEnd(page); + + // Use arrow keys to navigate + await page.keyboard.press('ArrowDown'); + await waitForAnimationEnd(page); + + // Take screenshot showing keyboard navigation + expect(await quickPicksWrapper.screenshot()).toMatchSnapshot('context-selector-navigation.png'); + + // Press Escape to close without selecting + await page.keyboard.press('Escape'); + await waitForAnimationEnd(page); + + // Verify the overlay is closed + expect(await page.locator(getSelector(testIds.prompt.quickPicksWrapper)).count()).toBe(0); +}; diff --git a/ui-tests/__test__/main.spec.ts b/ui-tests/__test__/main.spec.ts index 6f8e9a1b..3b72599f 100644 --- a/ui-tests/__test__/main.spec.ts +++ b/ui-tests/__test__/main.spec.ts @@ -55,6 +55,7 @@ import { renderPromptTopBar, promptTopBarTooltip, promptTopBarButtonOverlay, + promptTopBarTitle, } from './flows/prompt-top-bar'; test.describe('Open MynahUI', () => { @@ -200,6 +201,11 @@ test.describe('Open MynahUI', () => { test('should show overlay when clicking top bar button', async ({ page }) => { await promptTopBarButtonOverlay(page); }); + + test('should show context menu when clicking top bar title', async ({ page }) => { + test.setTimeout(40_000); + await promptTopBarTitle(page); + }); }); test.describe('File tree', () => {