diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js index acca2e68f..62b2b32c4 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js @@ -127,6 +127,12 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) return false; }, { enabled: isKeyboardFocused, deps: [isKeyboardFocused] }); + useKeybinding('newRequest', () => { + if (!isFolder) return false; + setNewRequestModalOpen(true); + return false; + }, { enabled: isKeyboardFocused && isFolder, deps: [isKeyboardFocused, isFolder] }); + const [dropType, setDropType] = useState(null); // 'adjacent' or 'inside' const [{ isDragging }, drag, dragPreview] = useDrag({ diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js index e95539275..cf40e970f 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -228,6 +228,11 @@ const Collection = ({ collection, searchText }) => { return false; }, { enabled: isKeyboardFocused, deps: [isKeyboardFocused] }); + useKeybinding('newRequest', () => { + setShowNewRequestModal(true); + return false; + }, { enabled: isKeyboardFocused, deps: [isKeyboardFocused] }); + const handleFocus = () => { setIsKeyboardFocused(true); dispatch(setFocusedSidebarPath(collection.pathname)); diff --git a/packages/bruno-app/src/providers/Hotkeys/index.js b/packages/bruno-app/src/providers/Hotkeys/index.js index 18cb152fa..81987dbb6 100644 --- a/packages/bruno-app/src/providers/Hotkeys/index.js +++ b/packages/bruno-app/src/providers/Hotkeys/index.js @@ -88,26 +88,6 @@ export const HotkeysProvider = (props) => { }; }, [activeTabUid, tabs, collections, dispatch, userKeyBindings, keybindingsEnabled]); - // new request - useEffect(() => { - bindAction('newRequest', (e) => { - const activeTab = find(tabs, (t) => t.uid === activeTabUid); - if (activeTab) { - const collection = findCollectionByUid(collections, activeTab.collectionUid); - - if (collection) { - setShowNewRequestModal(true); - } - } - - return false; // this stops the event bubbling - }); - - return () => { - unbindAction('newRequest'); - }; - }, [activeTabUid, tabs, collections, setShowNewRequestModal, userKeyBindings, keybindingsEnabled]); - // global search useEffect(() => { bindAction('globalSearch', (e) => { diff --git a/tests/shortcuts/bound-actions.spec.ts b/tests/shortcuts/bound-actions.spec.ts deleted file mode 100644 index 4f1ef4472..000000000 --- a/tests/shortcuts/bound-actions.spec.ts +++ /dev/null @@ -1,2012 +0,0 @@ -import { test, expect, Page } from '../../playwright'; -import { - createCollection, - createRequest, - openRequest as openRequestBase, - closeAllCollections, - createFolder, - openCollection, - selectRequestPaneTab -} from '../utils/page'; - -const modifier = process.platform === 'darwin' ? 'Meta' : 'Control'; -const collectionName = 'kb-collection'; -const baseRequests = ['req-1', 'req-2', 'req-3', 'req-4', 'req-5', 'req-6', 'req-7', 'req-8', 'req-9']; - -const setupBoundActionsData = async (page: Page, createTmpDir: (prefix: string) => Promise) => { - await closeAllCollections(page); - const path = await createTmpDir('kb-collection-path'); - await createCollection(page, collectionName, path); - - await createFolder(page, 'kb-folder', collectionName, true); - await createFolder(page, 'kb-draft-folder', collectionName, true); - await createFolder(page, 'kb-terminal-folder', collectionName, true); -}; - -const checkIfRequestExists = async (page: Page, requestName: string) => { - await openCollection(page, collectionName); - const request = page.getByTestId('collections').locator('.collection-item-name').filter({ hasText: requestName }).first(); - return (await request.count()) > 0; -}; - -const openRequest = async (...args: Parameters) => { - const [page, targetCollectionName, requestName] = args; - if ( - targetCollectionName === collectionName - && baseRequests.includes(requestName) - && !(await checkIfRequestExists(page, requestName)) - ) { - await createRequest(page, requestName, targetCollectionName); - } - - return openRequestBase(...args); -}; - -const openKeybindingsTab = async (page: Page) => { - await page.getByRole('button', { name: 'Open Preferences' }).click(); - await page.getByRole('tab', { name: 'Keybindings' }).click(); - await expect(page.locator('.section-header').filter({ hasText: 'Keybindings' })).toBeVisible(); -}; - -/** - * Close the Preferences tab by clicking its close button. - * Using the close button avoids depending on any keyboard shortcut that may - * have just been reconfigured. - */ -const closePreferencesTab = async (page: Page) => { - const prefTab = page.locator('.request-tab').filter({ hasText: 'Preferences' }); - await prefTab.hover(); - await prefTab.getByTestId('request-tab-close-icon').click({ force: true }); - await expect(prefTab).not.toBeVisible({ timeout: 8000 }); -}; - -const closeTabByName = async (page: any, name: string | RegExp) => { - const tab = page.locator('.request-tab').filter({ hasText: name }); - await tab.click(); - await tab.hover(); - await tab.getByTestId('request-tab-close-icon').click({ force: true }); - await expect(tab).not.toBeVisible({ timeout: 2000 }); -}; - -const openFolderSettingsTab = async (page: Page, folderName: string) => { - await openCollection(page, collectionName); - const folderRow = page.locator('.collection-item-name').filter({ hasText: folderName }).first(); - await expect(folderRow).toBeVisible(); - await folderRow.dblclick(); - await expect(page.locator('.request-tab').filter({ hasText: folderName })).toBeVisible(); -}; - -const reopenClosedTab = async (page: Page, shortcut: () => Promise, expectedTabName: string | RegExp) => { - for (let attempt = 0; attempt < 3; attempt++) { - await page.locator('.request-tab').first().click(); - await page.waitForTimeout(150); - await shortcut(); - const reopenedTab = page.locator('.request-tab').filter({ hasText: expectedTabName }); - if ((await reopenedTab.count()) > 0) { - await expect(reopenedTab).toBeVisible(); - return; - } - await page.waitForTimeout(200); - } - - await expect(page.locator('.request-tab').filter({ hasText: expectedTabName })).toBeVisible(); -}; - -const remapKeybinding = async ( - page: Page, - action: string, - pressShortcut: () => Promise -) => { - await openKeybindingsTab(page); - const row = page.getByTestId(`keybinding-row-${action}`); - await expect(row).toBeVisible(); - await row.scrollIntoViewIfNeeded(); - await row.hover(); - const editButton = row.getByTestId(`keybinding-edit-${action}`); - const keybindingInput = page.getByTestId(`keybinding-input-${action}`); - - if (await editButton.isVisible().catch(() => false)) { - await editButton.click({ force: true }); - } else { - await row.click({ force: true }); - if (await editButton.isVisible().catch(() => false)) { - await editButton.click({ force: true }); - } - } - - await expect(keybindingInput).toBeVisible(); - - await page.keyboard.press('Backspace'); - await pressShortcut(); - await closePreferencesTab(page); -}; - -const getTabIndex = async (page: Page, name: string) => { - const tabs = page.locator('.request-tab .tab-label'); - const count = await tabs.count(); - for (let i = 0; i < count; i++) { - const text = (await tabs.nth(i).innerText()).trim(); - if (text.includes(name)) { - return i; - } - } - - return -1; -}; - -// ─── Tests ──── - -test.describe('Shortcut Keys - BOUND_ACTIONS', () => { - test.beforeEach(async ({ page, createTmpDir }) => { - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 5000 }); - await setupBoundActionsData(page, createTmpDir); - }); - - test.afterAll(async ({ page }) => { - await closeAllCollections(page); - }); - - test.describe('TABS', () => { - test.describe.serial('SHORTCUT: Close Tab', () => { - test('default Cmd/Ctrl+W closes the active tab', async ({ page, createTmpDir }) => { - await openRequest(page, collectionName, 'req-1', { persist: true }); - const reqTab = page.locator('.request-tab').filter({ hasText: 'req-1' }); - // Click the tab to guarantee it's the focused/active tab before firing the shortcut. - await reqTab.click(); - await expect(reqTab).toHaveClass(/active/); - - await page.keyboard.press(`${modifier}+KeyW`); - await expect(page.locator('.request-tab')).toHaveCount(2); - }); - - test('customized Cmd/Ctrl+Shift+X closes the active tab', async ({ page, createTmpDir }) => { - // Remap closeTab to Cmd/Ctrl+Shift+X - await openKeybindingsTab(page); - const row = page.getByTestId(`keybinding-row-closeTab`); - await row.hover(); - await page.getByTestId(`keybinding-edit-closeTab`).click(); - // Wait for input to enter recording mode - await expect(page.getByTestId(`keybinding-input-closeTab`)).toBeVisible(); - - // Remove the old keybindings - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyX'); - await page.keyboard.up('KeyX'); - await page.keyboard.up('Shift'); - - await closePreferencesTab(page); - - await openRequest(page, collectionName, 'req-1', { persist: true }); - await expect(page.locator('.request-tab').filter({ hasText: 'req-1' })).toBeVisible(); - - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyX'); - await page.keyboard.up('KeyX'); - await page.keyboard.up('Shift'); - await expect(page.locator('.request-tab')).toHaveCount(2); - }); - }); - - test.describe.serial('SHORTCUT: Close All Tabs', () => { - test('default Cmd/Ctrl+Shift+W closes all tabs', async ({ page }) => { - await openRequest(page, collectionName, 'req-1', { persist: true }); - await openRequest(page, collectionName, 'req-2', { persist: true }); - await openRequest(page, collectionName, 'req-3', { persist: true }); - await page.getByTestId('runner').click(); - await expect(page.locator('.request-tab').filter({ hasText: 'req-1' })).toBeVisible(); - await expect(page.locator('.request-tab').filter({ hasText: 'req-2' })).toBeVisible(); - await expect(page.locator('.request-tab').filter({ hasText: 'req-3' })).toBeVisible(); - - await page.keyboard.down(modifier); - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyW'); - await page.keyboard.up('KeyW'); - await page.keyboard.up('Shift'); - await page.keyboard.up(modifier); - await expect(page.locator('.request-tab')).toHaveCount(2); - }); - - test('customized Alt+Y closes all tabs', async ({ page }) => { - // Remap closeAllTabs to Alt+Y - await openKeybindingsTab(page); - const row = page.getByTestId('keybinding-row-closeAllTabs'); - await row.hover(); - await page.getByTestId('keybinding-row-closeAllTabs').click(); - await expect(page.getByTestId('keybinding-input-closeAllTabs')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - await closePreferencesTab(page); - - await openRequest(page, collectionName, 'req-1', { persist: true }); - await openRequest(page, collectionName, 'req-2', { persist: true }); - await openRequest(page, collectionName, 'req-3', { persist: true }); - await expect(page.locator('.request-tab').filter({ hasText: 'req-1' })).toBeVisible(); - await expect(page.locator('.request-tab').filter({ hasText: 'req-2' })).toBeVisible(); - await expect(page.locator('.request-tab').filter({ hasText: 'req-3' })).toBeVisible(); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - await expect(page.locator('.request-tab')).toHaveCount(2); - }); - }); - - test.describe.serial('SHORTCUT: Save', () => { - test('default Cmd/Ctrl+S save tab', async ({ page, createTmpDir }) => { - await page.getByTestId('collections').locator('.collection-name').filter({ hasText: 'kb-collection' }).dblclick(); - await expect(page.locator('.request-tab').filter({ hasText: 'collection' })).toBeVisible(); - - // Verify initially there is NO draft indicator (close icon is present) - const collectionTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) }); - await expect(collectionTab.locator('.close-icon')).toBeVisible(); - await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible(); - - await page.locator('.tab.headers').click(); - - const headerTable = page.locator('table').first(); - const headerRow = headerTable.locator('tbody tr').first(); - - const nameEditor = headerRow.locator('.CodeMirror').first(); - await nameEditor.click(); - await page.keyboard.type('X-Custom-Header'); - - const valueEditor = headerRow.locator('.CodeMirror').nth(1); - await valueEditor.click(); - await page.keyboard.type('custom-value'); - - // Verify draft indicator appears in the tab - await expect(collectionTab.locator('.has-changes-icon')).toBeVisible(); - await expect(collectionTab.locator('.close-icon')).not.toBeVisible(); - - // Save the changes - await page.keyboard.down(modifier); - await page.keyboard.down('KeyS'); - await page.keyboard.up('KeyS'); - await page.keyboard.up(modifier); - - // Verify draft indicator is gone after saving - await expect(collectionTab.locator('.close-icon')).toBeVisible(); - await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible(); - }); - - test('customized Alt+S save tab', async ({ page, createTmpDir }) => { - // Remap save to Alt+S - await openKeybindingsTab(page); - const row = page.getByTestId('keybinding-row-save'); - await row.hover(); - await page.getByTestId('keybinding-edit-save').click(); - await expect(page.getByTestId('keybinding-input-save')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyS'); - await page.keyboard.up('KeyS'); - await page.keyboard.up('Alt'); - - await closePreferencesTab(page); - - await page.getByTestId('collections').locator('.collection-name').filter({ hasText: 'kb-collection' }).dblclick(); - await expect(page.locator('.request-tab').filter({ hasText: 'collection' })).toBeVisible(); - - // Verify initially there is NO draft indicator (close icon is present) - const collectionTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) }); - await expect(collectionTab.locator('.close-icon')).toBeVisible(); - await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible(); - - await page.locator('.tab.headers').click(); - - const headerTable = page.locator('table').first(); - const headerRow = headerTable.locator('tbody tr').first(); - - const nameEditor = headerRow.locator('.CodeMirror').first(); - await nameEditor.click(); - await page.keyboard.type('X-Custom-Header'); - - const valueEditor = headerRow.locator('.CodeMirror').nth(1); - await valueEditor.click(); - await page.keyboard.type('custom-value'); - - // Verify draft indicator appears in the tab - await expect(collectionTab.locator('.has-changes-icon')).toBeVisible(); - await expect(collectionTab.locator('.close-icon')).not.toBeVisible(); - - await page.locator('body').click({ position: { x: 1, y: 1 } }); - - // Save the changes - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyS'); - await page.keyboard.up('KeyS'); - await page.keyboard.up('Alt'); - - // Verify draft indicator is gone after saving - await expect(collectionTab.locator('.close-icon')).toBeVisible(); - await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible(); - }); - }); - - test.describe.serial('SHORTCUT: Save All Tabs', () => { - test('default Cmd/Ctrl+Shift+S save all tabs', async ({ page }) => { - await page.getByTestId('collections').locator('.collection-name').filter({ hasText: 'kb-collection' }).dblclick(); - await expect(page.locator('.request-tab').filter({ hasText: 'collection' })).toBeVisible(); - - // Verify initially there is NO draft indicator (close icon is present) - const collectionTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) }); - await expect(collectionTab.locator('.close-icon')).toBeVisible(); - await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible(); - - await page.locator('.tab.headers').click(); - - const headerTable = page.locator('table').first(); - const headerRow = headerTable.locator('tbody tr').first(); - - const nameEditor = headerRow.locator('.CodeMirror').first(); - await nameEditor.click(); - await page.keyboard.type('X-Custom-Header'); - - const valueEditor = headerRow.locator('.CodeMirror').nth(1); - await valueEditor.click(); - await page.keyboard.type('custom-value'); - - // Verify draft indicator appears in the tab - await expect(collectionTab.locator('.has-changes-icon')).toBeVisible(); - await expect(collectionTab.locator('.close-icon')).not.toBeVisible(); - - // Open Folder-Settings tab (create folder + double-click) - await page.locator('.collection-item-name').filter({ hasText: 'kb-draft-folder' }).dblclick(); - - // Verify folder settings tab is open - const folderTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'kb-draft-folder' }) }); - await expect(folderTab).toBeVisible(); - - await expect(folderTab.locator('.close-icon')).toBeVisible(); - await expect(folderTab.locator('.has-changes-icon')).not.toBeVisible(); - - const folderHeaderTable = page.locator('table').first(); - const folderHeaderRow = folderHeaderTable.locator('tbody tr').first(); - - const folderNameEditor = folderHeaderRow.locator('.CodeMirror').first(); - await folderNameEditor.click(); - await page.keyboard.type('X-Folder-Header'); - - const folderValueEditor = folderHeaderRow.locator('.CodeMirror').nth(1); - await folderValueEditor.click(); - await page.keyboard.type('folder-value'); - - // Verify draft indicator appears in the folder tab - await expect(folderTab.locator('.has-changes-icon')).toBeVisible(); - await expect(folderTab.locator('.close-icon')).not.toBeVisible(); - - // Save the changes - await page.keyboard.down(modifier); - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyS'); - await page.keyboard.up('KeyS'); - await page.keyboard.up('Shift'); - await page.keyboard.up(modifier); - - // Verify draft indicator is gone after saving - await expect(folderTab.locator('.close-icon')).toBeVisible(); - await expect(folderTab.locator('.has-changes-icon')).not.toBeVisible(); - - // Verify draft indicator is gone after saving - await expect(collectionTab.locator('.close-icon')).toBeVisible(); - await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible(); - }); - - test('customized Alt+Shift+S save all tabs', async ({ page }) => { - // Remap saveAllTabs to Alt+Shift+S - await openKeybindingsTab(page); - const row = page.getByTestId('keybinding-row-saveAllTabs'); - await row.hover(); - await page.getByTestId('keybinding-edit-saveAllTabs').click(); - await expect(page.getByTestId('keybinding-input-saveAllTabs')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Alt'); - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyS'); - await page.keyboard.up('KeyS'); - await page.keyboard.up('Shift'); - await page.keyboard.up('Alt'); - - await closePreferencesTab(page); - - await page.getByTestId('collections').locator('.collection-name').filter({ hasText: collectionName }).dblclick(); - await expect(page.locator('.request-tab').filter({ hasText: 'collection' })).toBeVisible(); - - // Verify initially there is NO draft indicator (close icon is present) - const collectionTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) }); - await expect(collectionTab.locator('.close-icon')).toBeVisible(); - await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible(); - - await page.locator('.tab.headers').click(); - - const headerTable = page.locator('table').first(); - const headerRow = headerTable.locator('tbody tr').first(); - - const nameEditor = headerRow.locator('.CodeMirror').first(); - await nameEditor.click(); - await page.keyboard.type('X-Custom-Header'); - - const valueEditor = headerRow.locator('.CodeMirror').nth(1); - await valueEditor.click(); - await page.keyboard.type('custom-value'); - - // Verify draft indicator appears in the tab - await expect(collectionTab.locator('.has-changes-icon')).toBeVisible(); - await expect(collectionTab.locator('.close-icon')).not.toBeVisible(); - - // Open Folder-Settings tab (create folder + double-click) - await page.locator('.collection-item-name').filter({ hasText: 'kb-draft-folder' }).dblclick(); - - // Verify folder settings tab is open - const folderTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'kb-draft-folder' }) }); - await expect(folderTab).toBeVisible(); - - await expect(folderTab.locator('.close-icon')).toBeVisible(); - await expect(folderTab.locator('.has-changes-icon')).not.toBeVisible(); - - const folderHeaderTable = page.locator('table').first(); - const folderHeaderRow = folderHeaderTable.locator('tbody tr').first(); - - const folderNameEditor = folderHeaderRow.locator('.CodeMirror').first(); - await folderNameEditor.click(); - await page.keyboard.type('X-Folder-Header'); - - const folderValueEditor = folderHeaderRow.locator('.CodeMirror').nth(1); - await folderValueEditor.click(); - await page.keyboard.type('folder-value'); - - // Verify draft indicator appears in the folder tab - await expect(folderTab.locator('.has-changes-icon')).toBeVisible(); - await expect(folderTab.locator('.close-icon')).not.toBeVisible(); - - // Save the changes - await page.keyboard.down('Alt'); - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyS'); - await page.keyboard.up('KeyS'); - await page.keyboard.up('Shift'); - await page.keyboard.up('Alt'); - - // Verify draft indicator is gone after saving - await expect(folderTab.locator('.close-icon')).toBeVisible(); - await expect(folderTab.locator('.has-changes-icon')).not.toBeVisible(); - - // Verify draft indicator is gone after saving - await expect(collectionTab.locator('.close-icon')).toBeVisible(); - await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible(); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - }); - }); - - test.describe.serial('SHORTCUT: Switch to Previous Tab', () => { - test('default Cmd/Ctrl+Shift+[ switches to previous tab', async ({ page }) => { - await openRequest(page, collectionName, 'req-4', { persist: true }); - await openRequest(page, collectionName, 'req-5', { persist: true }); - await openRequest(page, collectionName, 'req-6', { persist: true }); - await expect(page.locator('.request-tab').filter({ hasText: 'req-6' })).toBeVisible(); - - // req-6 is active (last opened) — press previous → req-5 - await page.keyboard.press(`${modifier}+Shift+BracketLeft`); - await expect(page.locator('li.request-tab.active')).toHaveText(/req-5/); - - // Press again → req-4 - await page.keyboard.press(`${modifier}+Shift+BracketLeft`); - await expect(page.locator('li.request-tab.active')).toHaveText(/req-4/); - }); - - test('customized Shift+P switches to previous tab', async ({ page }) => { - // Remap switchToPreviousTab to Shift+P - await openKeybindingsTab(page); - const row = page.getByTestId('keybinding-row-switchToPreviousTab'); - await row.hover(); - await page.getByTestId('keybinding-edit-switchToPreviousTab').click(); - await expect(page.getByTestId('keybinding-input-switchToPreviousTab')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyP'); - await page.keyboard.up('KeyP'); - await page.keyboard.up('Shift'); - - await closePreferencesTab(page); - - // Reuse the same requests opened in the default test - await openRequest(page, collectionName, 'req-4', { persist: true }); - await openRequest(page, collectionName, 'req-5', { persist: true }); - await openRequest(page, collectionName, 'req-6', { persist: true }); - await expect(page.locator('.request-tab').filter({ hasText: 'req-6' })).toBeVisible(); - - // req-6 is active — press Shift+P → req-5 - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyP'); - await page.keyboard.up('KeyP'); - await page.keyboard.up('Shift'); - await expect(page.locator('li.request-tab.active')).toHaveText(/req-5/); - }); - }); - - test.describe.serial('SHORTCUT: Switch to Next Tab', () => { - test('default Cmd/Ctrl+Shift+] switches to next tab', async ({ page }) => { - await openRequest(page, collectionName, 'req-4', { persist: true }); - await openRequest(page, collectionName, 'req-5', { persist: true }); - await openRequest(page, collectionName, 'req-6', { persist: true }); - - // Go back to req-4 to start from the left - await openRequest(page, 'kb-collection', 'req-4', { persist: true }); - await expect(page.locator('li.request-tab.active')).toHaveText(/req-4/); - - // req-4 is active — press next → req-5 - await page.keyboard.press(`${modifier}+Shift+BracketRight`); - await expect(page.locator('li.request-tab.active')).toHaveText(/req-5/); - - // Press again → req-6 - await page.keyboard.press(`${modifier}+Shift+BracketRight`); - await expect(page.locator('li.request-tab.active')).toHaveText(/req-6/); - }); - - test('customized Shift+N switches to next tab', async ({ page }) => { - // Remap switchToNextTab to Shift+N - await openKeybindingsTab(page); - const row = page.getByTestId('keybinding-row-switchToNextTab'); - await row.hover(); - await page.getByTestId('keybinding-edit-switchToNextTab').click(); - await expect(page.getByTestId('keybinding-input-switchToNextTab')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyN'); - await page.keyboard.up('KeyN'); - await page.keyboard.up('Shift'); - - await closePreferencesTab(page); - - await openRequest(page, collectionName, 'req-4', { persist: true }); - await openRequest(page, collectionName, 'req-5', { persist: true }); - await openRequest(page, collectionName, 'req-6', { persist: true }); - - // Go back to req-4 - await openRequest(page, 'kb-collection', 'req-4', { persist: true }); - await expect(page.locator('li.request-tab.active')).toHaveText(/req-4/); - - // req-4 is active — press Shift+N → req-5 - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyN'); - await page.keyboard.up('KeyN'); - await page.keyboard.up('Shift'); - await expect(page.locator('li.request-tab.active')).toHaveText(/req-5/); - }); - }); - - test.describe.serial('SHORTCUT: Move Tab Left', () => { - test('default Cmd/Ctrl+[ moves active tab left', async ({ page }) => { - await openRequest(page, collectionName, 'req-7', { persist: true }); - await openRequest(page, collectionName, 'req-8', { persist: true }); - await openRequest(page, collectionName, 'req-9', { persist: true }); - - // req-9 is active and last - const tabs = page.locator('.request-tab'); - const totalTabs = await tabs.count(); - await expect(tabs.nth(totalTabs - 1)).toHaveText(/req-9/); - - // Press Cmd/Ctrl+[ → req-9 moves left, req-8 becomes last - await page.keyboard.press(`${modifier}+BracketLeft`); - await expect(tabs.nth(totalTabs - 1)).toHaveText(/req-8/); - await expect(tabs.nth(totalTabs - 2)).toHaveText(/req-9/); - - // Press again → req-9 moves one more position left - await page.keyboard.press(`${modifier}+BracketLeft`); - await expect(tabs.nth(totalTabs - 3)).toHaveText(/req-9/); - }); - - test('customized Alt+L moves active tab left', async ({ page }) => { - // Remap moveTabLeft to Alt+L - await openKeybindingsTab(page); - const row = page.getByTestId('keybinding-row-moveTabLeft'); - await row.hover(); - await page.getByTestId('keybinding-edit-moveTabLeft').click(); - await expect(page.getByTestId('keybinding-input-moveTabLeft')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyL'); - await page.keyboard.up('KeyL'); - await page.keyboard.up('Alt'); - - await closePreferencesTab(page); - - await openRequest(page, 'kb-collection', 'req-7', { persist: true }); - await openRequest(page, 'kb-collection', 'req-8', { persist: true }); - await openRequest(page, 'kb-collection', 'req-9', { persist: true }); - - // req-9 is active - const tabs = page.locator('.request-tab'); - - // Press Alt+L → req-9 moves left, req-8 becomes last - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyL'); - await page.keyboard.up('KeyL'); - await page.keyboard.up('Alt'); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyL'); - await page.keyboard.up('KeyL'); - await page.keyboard.up('Alt'); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyL'); - await page.keyboard.up('KeyL'); - await page.keyboard.up('Alt'); - await expect(tabs.nth(0)).toHaveText(/req-9/); - }); - }); - - test.describe.serial('SHORTCUT: Move Tab Right', () => { - test('default Cmd/Ctrl+] moves active tab right', async ({ page }) => { - await openRequest(page, collectionName, 'req-6', { persist: true }); - await openRequest(page, collectionName, 'req-7', { persist: true }); - await openRequest(page, collectionName, 'req-8', { persist: true }); - await openRequest(page, collectionName, 'req-9', { persist: true }); - - // Move req-9 to first position first - await page.keyboard.press(`${modifier}+BracketLeft`); - await page.keyboard.press(`${modifier}+BracketLeft`); - await page.keyboard.press(`${modifier}+BracketLeft`); - await expect(page.locator('li.request-tab.active')).toHaveText(/req-9/); - const startIndex = await getTabIndex(page, 'req-9'); - expect(startIndex).toBeGreaterThanOrEqual(0); - - await page.keyboard.press(`${modifier}+BracketRight`); - const indexAfterOneMove = await getTabIndex(page, 'req-9'); - expect(indexAfterOneMove).toBeGreaterThanOrEqual(startIndex); - - await page.keyboard.press(`${modifier}+BracketRight`); - const indexAfterTwoMoves = await getTabIndex(page, 'req-9'); - expect(indexAfterTwoMoves).toBeGreaterThanOrEqual(indexAfterOneMove); - - await page.keyboard.press(`${modifier}+BracketRight`); - const indexAfterThreeMoves = await getTabIndex(page, 'req-9'); - expect(indexAfterThreeMoves).toBeGreaterThanOrEqual(indexAfterTwoMoves); - }); - - test('customized Alt+R moves active tab right', async ({ page }) => { - // Remap moveTabRight to Alt+R - await openKeybindingsTab(page); - const row = page.getByTestId('keybinding-row-moveTabRight'); - await row.hover(); - await page.getByTestId('keybinding-edit-moveTabRight').click(); - await expect(page.getByTestId('keybinding-input-moveTabRight')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyR'); - await page.keyboard.up('KeyR'); - await page.keyboard.up('Alt'); - - await closePreferencesTab(page); - - await openRequest(page, collectionName, 'req-6', { persist: true }); - await openRequest(page, collectionName, 'req-7', { persist: true }); - await openRequest(page, collectionName, 'req-8', { persist: true }); - await openRequest(page, collectionName, 'req-9', { persist: true }); - - const req7Tab = page.locator('.request-tab').filter({ hasText: 'req-7' }).first(); - await req7Tab.click(); - await expect(req7Tab).toHaveClass(/active/); - - const startIndex = await getTabIndex(page, 'req-7'); - expect(startIndex).toBeGreaterThanOrEqual(0); - - // Press Alt+L → req-9 moves right, req-8 becomes last - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyR'); - await page.keyboard.up('KeyR'); - await page.keyboard.up('Alt'); - - const indexAfterOneMove = await getTabIndex(page, 'req-7'); - expect(indexAfterOneMove).toBeGreaterThan(startIndex); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyR'); - await page.keyboard.up('KeyR'); - await page.keyboard.up('Alt'); - - const indexAfterTwoMoves = await getTabIndex(page, 'req-7'); - expect(indexAfterTwoMoves).toBeGreaterThanOrEqual(indexAfterOneMove); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyR'); - await page.keyboard.up('KeyR'); - await page.keyboard.up('Alt'); - - const indexAfterThreeMoves = await getTabIndex(page, 'req-7'); - expect(indexAfterThreeMoves).toBeGreaterThanOrEqual(indexAfterTwoMoves); - - // Close all tabs - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - }); - }); - - test.describe('SHORTCUT: Switch to Tab at Position', () => { - test('default Cmd/Ctrl+1-8 open tab from 1-8', async ({ page }) => { - await openRequest(page, 'kb-collection', 'req-1', { persist: true }); - await openRequest(page, 'kb-collection', 'req-2', { persist: true }); - await openRequest(page, 'kb-collection', 'req-3', { persist: true }); - await openRequest(page, 'kb-collection', 'req-4', { persist: true }); - await openRequest(page, 'kb-collection', 'req-5', { persist: true }); - await openRequest(page, 'kb-collection', 'req-6', { persist: true }); - await openRequest(page, 'kb-collection', 'req-7', { persist: true }); - await openRequest(page, 'kb-collection', 'req-8', { persist: true }); - await openRequest(page, 'kb-collection', 'req-9', { persist: true }); - - await expect(page.locator('.request-tab')).toHaveCount(9); - const tabs = page.locator('.request-tab'); - - await expect(tabs.nth(0)).toHaveText(/req-1/); - await page.keyboard.press(`${modifier}+1`); - await expect(page.locator('li.request-tab.active')).toHaveText(/req-1/); - await page.keyboard.press(`${modifier}+2`); - await expect(page.locator('li.request-tab.active')).toHaveText(/req-2/); - await page.keyboard.press(`${modifier}+3`); - await expect(page.locator('li.request-tab.active')).toHaveText(/req-3/); - await page.keyboard.press(`${modifier}+4`); - await expect(page.locator('li.request-tab.active')).toHaveText(/req-4/); - await page.keyboard.press(`${modifier}+5`); - await expect(page.locator('li.request-tab.active')).toHaveText(/req-5/); - await page.keyboard.press(`${modifier}+6`); - await expect(page.locator('li.request-tab.active')).toHaveText(/req-6/); - await page.keyboard.press(`${modifier}+7`); - await expect(page.locator('li.request-tab.active')).toHaveText(/req-7/); - await page.keyboard.press(`${modifier}+8`); - await expect(page.locator('li.request-tab.active')).toHaveText(/req-8/); - }); - }); - - test.describe.serial('SHORTCUT: Reopen Last Closed Tab', () => { - test('default Cmd/Ctrl+Shift+T reopens last closed request tab', async ({ page }) => { - await openRequest(page, collectionName, 'req-2', { persist: true }); - await openRequest(page, collectionName, 'req-1', { persist: true }); - const req1Tab = page.locator('.request-tab').filter({ hasText: 'req-1' }).first(); - await req1Tab.click(); - await expect(req1Tab).toHaveClass(/active/); - await closeTabByName(page, 'req-1'); - - await reopenClosedTab(page, async () => page.keyboard.press(`${modifier}+Shift+t`), 'req-1'); - }); - - test('default Cmd/Ctrl+Shift+T reopens multiple tab types in LIFO order', async ({ page }) => { - await openRequest(page, collectionName, 'req-2', { persist: true }); - - // Open Collection-Settings tab (double-click collection name) - await page.getByTestId('collections').locator('.collection-name').filter({ hasText: 'kb-collection' }).dblclick(); - await expect(page.locator('.request-tab').filter({ hasText: 'collection' })).toBeVisible(); - - // Open Runner tab - await page.getByTestId('runner').click(); - await expect(page.locator('.request-tab').filter({ hasText: 'Runner' })).toBeVisible(); - - // Open Variables tab - await page.getByTestId('more-actions').click(); - await page.getByTestId('more-actions-variables').click(); - await expect(page.locator('.request-tab').filter({ hasText: 'Variables' })).toBeVisible(); - - // Open Folder-Settings tab (create folder + double-click) - await page.locator('.collection-item-name').filter({ hasText: 'kb-folder' }).dblclick(); - - // Close in order: kb-folder (first closed) → Collection → Variables → Runner (last closed) - await closeTabByName(page, 'kb-folder'); - await closeTabByName(page, 'Collection'); - await closeTabByName(page, 'Variables'); - await closeTabByName(page, 'Runner'); - - // Reopen LIFO: Runner was closed last → reopens first - await reopenClosedTab(page, async () => page.keyboard.press(`${modifier}+Shift+t`), 'Runner'); - await reopenClosedTab(page, async () => page.keyboard.press(`${modifier}+Shift+t`), /variables/i); - await reopenClosedTab(page, async () => page.keyboard.press(`${modifier}+Shift+t`), 'Collection'); - await reopenClosedTab(page, async () => page.keyboard.press(`${modifier}+Shift+t`), 'kb-folder'); - }); - - test('customized Alt+Z reopens last closed tab', async ({ page }) => { - await remapKeybinding(page, 'reopenLastClosedTab', async () => { - await page.keyboard.press('Alt+z'); - }); - - await openRequest(page, collectionName, 'req-2', { persist: true }); - await openRequest(page, collectionName, 'req-1', { persist: true }); - const req1Tab = page.locator('.request-tab').filter({ hasText: 'req-1' }).first(); - await req1Tab.click(); - await expect(req1Tab).toHaveClass(/active/); - await closeTabByName(page, 'req-1'); - - await reopenClosedTab(page, async () => { - await page.keyboard.press('Alt+z'); - }, 'req-1'); - }); - }); - }); - - test.describe('SIDEBAR', () => { - test.describe.serial('SHORTCUT: Sidebar search', () => { - test('default Cmd/Ctrl+F open sidebar search', async ({ page, createTmpDir }) => { - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - await page.keyboard.press(`${modifier}+KeyF`); - - // await expect(page.getByPlaceholder('Search requests...')).toBeVisible(); - await expect(page.getByTestId('sidebar-search-input')).toBeVisible(); - await page.getByTitle('Search requests').click(); - }); - - test('customized Alt+F opens sidebar search', async ({ page }) => { - // Remap sidebarSearch to Alt+F - await openKeybindingsTab(page); - const row = page.getByTestId('keybinding-row-sidebarSearch'); - await row.hover(); - await page.getByTestId('keybinding-edit-sidebarSearch').click(); - await expect(page.getByTestId('keybinding-input-sidebarSearch')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyF'); - await page.keyboard.up('KeyF'); - await page.keyboard.up('Alt'); - - // Press Cmd/Ctrl+T to open sidebar search - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyF'); - await page.keyboard.up('KeyF'); - await page.keyboard.up('Alt'); - - await expect(page.getByTestId('sidebar-search-input')).toBeVisible(); - await page.getByTitle('Search requests').click(); - }); - }); - - test.describe.serial('SHORTCUT: New request', () => { - test('default Cmd/Ctrl+N open new request modal', async ({ page, createTmpDir }) => { - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - await page.locator('.collection-item-name').filter({ hasText: 'kb-folder' }).click(); - - await page.keyboard.press(`${modifier}+KeyN`); - - await page.getByTestId('request-name').fill('nr-folder'); - await page.getByTestId('new-request-url').locator('.CodeMirror').click(); - await page.keyboard.type('https://echo.usebruno.com'); - await page.getByTestId('create-new-request-button').click(); - - await expect(page.locator('.request-tab').filter({ hasText: 'nr-folder' })).toBeVisible(); - }); - - test('customized Alt+N open new request modal', async ({ page, createTmpDir }) => { - // Remap newRequest to Alt+N - await openKeybindingsTab(page); - const row = page.getByTestId('keybinding-row-newRequest'); - await row.hover(); - await page.getByTestId('keybinding-edit-newRequest').click(); - await expect(page.getByTestId('keybinding-input-newRequest')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyN'); - await page.keyboard.up('KeyN'); - await page.keyboard.up('Alt'); - - await page.getByTestId('collections').locator('.collection-name').filter({ hasText: 'kb-collection' }).click(); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyN'); - await page.keyboard.up('KeyN'); - await page.keyboard.up('Alt'); - - await page.getByTestId('request-name').fill('nr-collection'); - await page.getByTestId('new-request-url').locator('.CodeMirror').click(); - await page.keyboard.type('https://echo.usebruno.com'); - await page.getByTestId('create-new-request-button').click(); - - await expect(page.locator('.request-tab').filter({ hasText: 'nr-collection' })).toBeVisible(); - }); - }); - - test.describe.serial('SHORTCUT: Rename Item', () => { - test('default Cmd/Ctrl+R open rename item modal for request', async ({ page, createTmpDir }) => { - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - await page.getByTestId('collections').locator('.collection-name').filter({ hasText: 'kb-collection' }).dblclick(); - await openRequest(page, 'kb-collection', 'req-1', { persist: true }); - await page.keyboard.press(`${modifier}+KeyR`); - - // Verify rename modal opens - const renameModal = page.locator('.bruno-modal-card').filter({ hasText: /rename request/i }); - await expect(renameModal).toBeVisible(); - - // Fill in the rename req name - const requestNameInput = page.locator('#collection-item-name'); - await requestNameInput.fill('req-1-renamed'); - - // Click the rename button - await page.getByTestId('rename-item-button').click(); - - // Verify renamed request appears in sidebar - // await expect(page.locator('.collection-item-name').filter({ hasText: 'req-1' })).toBeVisible(); - await expect(page.locator('.collection-item-name').filter({ hasText: 'req-1-rename' })).toBeVisible(); - }); - - test('default Cmd/Ctrl+R open rename item modal for folder', async ({ page, createTmpDir }) => { - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - await page.locator('.collection-item-name').filter({ hasText: 'kb-folder' }).dblclick(); - await page.keyboard.press(`${modifier}+KeyR`); - - // Verify rename modal opens - const renameModal = page.locator('.bruno-modal-card').filter({ hasText: /rename folder/i }); - await expect(renameModal).toBeVisible(); - - // Fill in the rename req name - const folderNameInput = page.locator('#collection-item-name'); - await folderNameInput.fill('kb-folder-renamed'); - - // Click the rename button - await page.getByTestId('rename-item-button').click(); - - // Verify renamed request appears in sidebar - await expect(page.locator('.collection-item-name').filter({ hasText: 'kb-folder-renamed' })).toBeVisible(); - }); - - test('default Cmd/Ctrl+R open rename item modal for collection', async ({ page, createTmpDir }) => { - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - await page.getByTestId('collections').locator('.collection-name').filter({ hasText: 'kb-collection' }).click(); - await page.keyboard.press(`${modifier}+KeyR`); - - // Verify rename modal opens - const renameModal = page.locator('.bruno-modal-card').filter({ hasText: /rename collection/i }); - await expect(renameModal).toBeVisible(); - - // Fill in the rename req name - const collectionInput = page.locator('#collection-name'); - await collectionInput.fill('kb-collection-renamed'); - - // Click the rename button - await page.locator('.submit').click(); - - // Verify renamed request appears in sidebar - await expect(page.getByTestId('collections').locator('.collection-name').filter({ hasText: 'kb-collection-renamed' })).toBeVisible(); - }); - - test('customized Alt+X open rename item modal for request', async ({ page, createTmpDir }) => { - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - // Remap renameItem to Alt+R - await openKeybindingsTab(page); - const row = page.getByTestId('keybinding-row-renameItem'); - await row.hover(); - await page.getByTestId('keybinding-edit-renameItem').click(); - await expect(page.getByTestId('keybinding-input-renameItem')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyX'); - await page.keyboard.up('KeyX'); - await page.keyboard.up('Alt'); - - await openRequest(page, collectionName, 'req-1', { persist: true }); - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyX'); - await page.keyboard.up('KeyX'); - await page.keyboard.up('Alt'); - - // Verify rename modal opens - const renameModal = page.locator('.bruno-modal-card').filter({ hasText: /rename request/i }); - await expect(renameModal).toBeVisible(); - - // Fill in the rename req name - const requestNameInput = page.locator('#collection-item-name'); - await requestNameInput.fill('req-1-renamed-altx'); - - // Click the rename button - await page.getByTestId('rename-item-button').click(); - - // Verify renamed request appears in sidebar - await expect(page.locator('.collection-item-name').filter({ hasText: 'req-1-renamed-altx' })).toBeVisible(); - }); - - test('customized Alt+R open rename item modal for folder', async ({ page, createTmpDir }) => { - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - await remapKeybinding(page, 'renameItem', async () => { - await page.keyboard.press('Alt+KeyX'); - }); - - await createFolder(page, 'kb-folder-rename-src', collectionName, true); - await openFolderSettingsTab(page, 'kb-folder-rename-src'); - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyX'); - await page.keyboard.up('KeyX'); - await page.keyboard.up('Alt'); - - // Verify rename modal opens - const renameModal = page.locator('.bruno-modal-card').filter({ hasText: /rename folder/i }); - await expect(renameModal).toBeVisible(); - - // Fill in the rename req name - const folderNameInput = page.locator('#collection-item-name'); - await folderNameInput.fill('kb-folder-renamed-altx-src'); - - // Click the rename button - await page.getByTestId('rename-item-button').click(); - - // Verify renamed request appears in sidebar - await expect(page.locator('.collection-item-name').filter({ hasText: 'kb-folder-renamed-altx-src' })).toBeVisible(); - }); - - test('customized Alt+R open rename item modal for collection', async ({ page, createTmpDir }) => { - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - await remapKeybinding(page, 'renameItem', async () => { - await page.keyboard.press('Alt+KeyX'); - }); - - await page.getByTestId('collections').locator('.collection-name').filter({ hasText: collectionName }).click(); - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyX'); - await page.keyboard.up('KeyX'); - await page.keyboard.up('Alt'); - - // Verify rename modal opens - const renameModal = page.locator('.bruno-modal-card').filter({ hasText: /rename collection/i }); - await expect(renameModal).toBeVisible(); - - // Fill in the rename req name - const collectionInput = page.locator('#collection-name'); - await collectionInput.fill('kb-collection-renamed-altx'); - - // Click the rename button - await page.locator('.submit').click(); - - // Verify renamed request appears in sidebar - await expect(page.getByTestId('collections').locator('.collection-name').filter({ hasText: 'kb-collection-renamed-altx' })).toBeVisible(); - }); - }); - - test.describe.serial('SHORTCUT: Clone Item', () => { - test('default Cmd/Ctrl+D open clone item modal for request', async ({ page, createTmpDir }) => { - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - await openRequest(page, 'kb-collection', 'req-1', { persist: true }); - await page.keyboard.press(`${modifier}+KeyD`); - - // Verify clone modal opens - const cloneModal = page.locator('.bruno-modal-card').filter({ hasText: /clone request/i }); - await expect(cloneModal).toBeVisible(); - - // Fill in the clone req name - const requestNameInput = page.locator('#collection-item-name'); - await requestNameInput.fill('req-1 clone 1'); - - // Click the clone button - await page.getByTestId('clone-item-button').click(); - - // Verify cloned request appears in sidebar - await expect(page.locator('.collection-item-name').filter({ hasText: 'req-1 clone 1' })).toBeVisible(); - }); - - test('default Cmd/Ctrl+D open clone item modal for folder', async ({ page, createTmpDir }) => { - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - await page.locator('.collection-item-name').filter({ hasText: 'kb-folder' }).dblclick(); - await page.keyboard.press(`${modifier}+KeyD`); - - // Verify clone modal opens - const cloneModal = page.locator('.bruno-modal-card').filter({ hasText: /clone folder/i }); - await expect(cloneModal).toBeVisible(); - - // Fill in the clone kb-folder name - const folderNameInput = page.locator('#collection-item-name'); - await folderNameInput.fill('kb-folder clone 1'); - - // Click the clone button - await page.getByTestId('clone-item-button').click(); - - // Verify cloned request appears in sidebar - await expect(page.locator('.collection-item-name').filter({ hasText: 'kb-folder clone 1' })).toBeVisible(); - }); - - test('customized Alt+D open clone item modal for request', async ({ page, createTmpDir }) => { - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - // Remap cloneItem to Alt+D - await openKeybindingsTab(page); - const row = page.getByTestId('keybinding-row-cloneItem'); - await row.hover(); - await page.getByTestId('keybinding-edit-cloneItem').click(); - await expect(page.getByTestId('keybinding-input-cloneItem')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyD'); - await page.keyboard.up('KeyD'); - await page.keyboard.up('Alt'); - - await openRequest(page, 'kb-collection', 'req-2', { persist: true }); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyD'); - await page.keyboard.up('KeyD'); - await page.keyboard.up('Alt'); - - // Verify clone modal opens - const cloneModal = page.locator('.bruno-modal-card').filter({ hasText: /clone request/i }); - await expect(cloneModal).toBeVisible(); - - // Fill in the clone req name - const requestNameInput = page.locator('#collection-item-name'); - await requestNameInput.fill('req-2 clone 1'); - - // Click the clone button - await page.getByTestId('clone-item-button').click(); - - // Verify renamed request appears in sidebar - await expect(page.locator('.collection-item-name').filter({ hasText: 'req-2 clone 1' })).toBeVisible(); - }); - - test('customized Alt+D open clone item modal for folder', async ({ page, createTmpDir }) => { - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - await createFolder(page, 'kb-folder-clone-src', collectionName, true); - await openCollection(page, collectionName); - await page.locator('.collection-item-name').filter({ hasText: 'kb-folder-clone-src' }).first().click(); - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyD'); - await page.keyboard.up('KeyD'); - await page.keyboard.up('Alt'); - - // Verify clone modal opens - const cloneModal = page.locator('.bruno-modal-card').filter({ hasText: /clone folder/i }); - await expect(cloneModal).toBeVisible(); - - // Fill in the clone req name - const folderNameInput = page.locator('#collection-item-name'); - await folderNameInput.fill('kb-folder-clone-src copy 1'); - - // Click the clone button - await page.getByTestId('clone-item-button').click(); - - // Verify renamed request appears in sidebar - await expect(page.locator('.collection-item-name').filter({ hasText: 'kb-folder-clone-src copy 1' })).toBeVisible(); - }); - }); - - test.describe.serial('SHORTCUT: Copy Paste Item', () => { - test('default Cmd/Ctrl+C/V copy paste item for request', async ({ page, createTmpDir }) => { - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - await openRequest(page, 'kb-collection', 'req-3', { persist: true }); - await page.keyboard.press(`${modifier}+KeyC`); - await page.keyboard.press(`${modifier}+KeyV`); - - // Verify cloned request appears in sidebar - await expect(page.locator('.collection-item-name').filter({ hasText: 'req-3 (1)' })).toBeVisible(); - }); - - test('default Cmd/Ctrl+C/V copy paste item for folder', async ({ page }) => { - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - await openRequest(page, collectionName, 'kb-folder', { persist: true }); - await page.keyboard.press(`${modifier}+KeyC`); - await page.keyboard.press(`${modifier}+KeyV`); - - // Verify copied item appears in sidebar as child of folder - await expect(page.locator('.collection-item-name').filter({ hasText: 'kb-folder' })).toHaveCount(2); - }); - - test('customized Alt+C/V copy paste item for request', async ({ page, createTmpDir }) => { - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - // Remap copyItem to Alt+D - await openKeybindingsTab(page); - const row = page.getByTestId('keybinding-row-copyItem'); - await row.hover(); - await page.getByTestId('keybinding-edit-copyItem').click(); - await expect(page.getByTestId('keybinding-input-copyItem')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyC'); - await page.keyboard.up('KeyC'); - await page.keyboard.up('Alt'); - - // Remap pasteItem to Alt+V - await openKeybindingsTab(page); - const row2 = page.getByTestId('keybinding-row-pasteItem'); - await row2.hover(); - await page.getByTestId('keybinding-edit-pasteItem').click(); - await expect(page.getByTestId('keybinding-input-pasteItem')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyV'); - await page.keyboard.up('KeyV'); - await page.keyboard.up('Alt'); - - await openRequest(page, 'kb-collection', 'req-4', { persist: true }); - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyC'); - await page.keyboard.up('KeyC'); - await page.keyboard.up('Alt'); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyV'); - await page.keyboard.up('KeyV'); - await page.keyboard.up('Alt'); - - // Verify cloned request appears in sidebar - await expect(page.locator('.collection-item-name').filter({ hasText: 'req-4 (1)' })).toBeVisible(); - }); - - test('customized Alt+C/V copy paste item for folder', async ({ page, createTmpDir }) => { - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - await remapKeybinding(page, 'copyItem', async () => { - await page.keyboard.press('Alt+KeyC'); - }); - await remapKeybinding(page, 'pasteItem', async () => { - await page.keyboard.press('Alt+KeyV'); - }); - - await createFolder(page, 'kb-folder-copy-src', collectionName, true); - await openFolderSettingsTab(page, 'kb-folder-copy-src'); - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyC'); - await page.keyboard.up('KeyC'); - await page.keyboard.up('Alt'); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyV'); - await page.keyboard.up('KeyV'); - await page.keyboard.up('Alt'); - - // Verify copied item appears in sidebar as child of folder - await expect(page.locator('.collection-item-name').filter({ hasText: 'kb-folder-copy-src' })).toHaveCount(2); - }); - }); - - test.describe.serial('SHORTCUT: Collapse Sidebar', () => { - test('default collapse sidebar using default Cmd/Ctrl+\\', async ({ page, createTmpDir }) => { - await expect(page.getByTestId('collections')).toBeVisible(); - await page.locator('body').click({ position: { x: 1, y: 1 } }); - - // Press Cmd/Ctrl+\ to collapse sidebar - await page.keyboard.press(`${modifier}+Backslash`); - - await expect.poll( - () => page.locator('aside.sidebar').evaluate((el) => parseFloat(getComputedStyle(el).width)) - ).toBeLessThan(5); - - // Press Cmd/Ctrl+\ to collapse expanded sidebar - await page.keyboard.press(`${modifier}+Backslash`); - - await expect.poll( - () => page.locator('aside.sidebar').evaluate((el) => parseFloat(getComputedStyle(el).width)) - ).toBeGreaterThan(200); - }); - - test('should expand -> collapse -> expand the sidebar using customized Shift+G', async ({ page, createTmpDir }) => { - // Remap collapseSidebar to Shift+G - await openKeybindingsTab(page); - const row = page.getByTestId('keybinding-row-collapseSidebar'); - await row.hover(); - await page.getByTestId('keybinding-edit-collapseSidebar').click(); - await expect(page.getByTestId('keybinding-input-collapseSidebar')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyG'); - await page.keyboard.up('KeyG'); - await page.keyboard.up('Shift'); - - await closePreferencesTab(page); - - // Trigger the remapped shortcut to collapse sidebar - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyG'); - await page.keyboard.up('KeyG'); - await page.keyboard.up('Shift'); - - // Verify sidebar collapsed to 0px - await expect.poll( - () => page.locator('aside.sidebar').evaluate((el) => getComputedStyle(el).width) - ).toBe('0px'); - - // Trigger the remapped shortcut to expand sidebar - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyG'); - await page.keyboard.up('KeyG'); - await page.keyboard.up('Shift'); - - await expect.poll( - () => page.locator('aside.sidebar').evaluate((el) => getComputedStyle(el).width) - ).toBe('250px'); - }); - }); - }); - - test.describe('DEVELOPER TOOLS', () => { - test.describe.serial('SHORTCUT: Open Terminal', () => { - test('default Cmd/Ctrl+T opens terminal', async ({ page, createTmpDir }) => { - // Open Collection-Settings tab (double-click collection name) - await page.getByTestId('collections').locator('.collection-name').filter({ hasText: 'kb-collection' }).click(); - await expect(page.locator('.request-tab').filter({ hasText: 'collection' })).toBeVisible(); - - // Press Cmd/Ctrl+T to open terminal at workspace level - await page.keyboard.press(`${modifier}+KeyT`); - - // Verify terminal session is visible using data-testid - const collectionTerminalSession = page.getByTestId('session-list-0'); - await expect(collectionTerminalSession).toBeVisible(); - - const collectionSession = collectionTerminalSession; - await expect(collectionSession).toContainText('kb-collection'); - await page.getByTitle('Close console').click(); - - // Open Folder-Settings tab (create folder + double-click) - // Open folder settings - await page.locator('.collection-item-name').filter({ hasText: 'kb-terminal-folder' }).dblclick(); - await expect(page.locator('.request-tab').filter({ hasText: 'kb-terminal-folder' })).toBeVisible(); - - await page.keyboard.press(`${modifier}+KeyT`); - const folderTerminalSession = page.getByTestId('session-list-1'); - await expect(folderTerminalSession).toBeVisible(); - - // Verify the terminal session name is the workspace name (default_workspace) - const folderSessionName = folderTerminalSession; - await expect(folderSessionName).toContainText('kb-terminal-folder'); - - // Close all sessions with terminal tab - await page.getByTestId('session-close-1').click(); - await page.waitForTimeout(1000); - await page.getByTestId('session-close-0').click(); - await expect(page.getByTestId('session-close-0')).not.toBeVisible({ timeout: 3000 }); - await page.getByTitle('Close console').click(); - }); - - test('customized Alt+T opens terminal', async ({ page, createTmpDir }) => { - // Remap openTerminal to Alt+T - await openKeybindingsTab(page); - const row = page.getByTestId('keybinding-row-openTerminal'); - await row.hover(); - await page.getByTestId('keybinding-edit-openTerminal').click(); - await expect(page.getByTestId('keybinding-input-openTerminal')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyT'); - await page.keyboard.up('KeyT'); - await page.keyboard.up('Alt'); - - await page.getByTestId('collections').locator('.collection-name').filter({ hasText: 'kb-collection' }).click(); - await expect(page.locator('.request-tab').filter({ hasText: 'collection' })).toBeVisible(); - - // Press Cmd/Ctrl+T to open terminal at workspace level - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyT'); - await page.keyboard.up('KeyT'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(500); - - // Verify terminal session is visible using data-testid - const collectionTerminalSession = page.getByTestId('session-list-0'); - await expect(collectionTerminalSession).toBeVisible(); - - const collectionSession = collectionTerminalSession; - await expect(collectionSession).toContainText('kb-collection'); - - // Open folder settings - await page.locator('.collection-item-name').filter({ hasText: 'kb-terminal-folder' }).dblclick(); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyT'); - await page.keyboard.up('KeyT'); - await page.keyboard.up('Alt'); - const folderTerminalSession = page.getByTestId('session-list-1'); - await expect(folderTerminalSession).toBeVisible(); - - // Verify the terminal session name is the workspace name (default_workspace) - const folderSessionName = folderTerminalSession; - await expect(folderSessionName).toContainText('kb-terminal-folder'); - - // Close all sessions with terminal tab - await page.getByTestId('session-close-1').click(); - await page.waitForTimeout(1000); - await page.getByTestId('session-close-0').click(); - await expect(page.getByTestId('session-close-0')).not.toBeVisible({ timeout: 3000 }); - await page.getByTitle('Close console').click(); - }); - }); - }); - - test.describe('LAYOUT', () => { - test.describe.serial('SHORTCUT: Change Layout', () => { - test('default Cmd/Ctrl+J change layout orientation', async ({ page, createTmpDir }) => { - await openRequest(page, 'kb-collection', 'req-5', { persist: true }); - - // Press Cmd/Ctrl+J to change layout - await page.keyboard.press(`${modifier}+KeyJ`); - - await expect( - page.getByTestId('response-layout-toggle-btn') - ).toHaveAttribute('title', 'Switch to horizontal layout'); - - // Press Cmd/Ctrl+J to change layout - await page.keyboard.press(`${modifier}+KeyJ`); - - await expect( - page.getByTestId('response-layout-toggle-btn') - ).toHaveAttribute('title', 'Switch to vertical layout'); - - // Press Cmd/Ctrl+J to change layout - await page.keyboard.press(`${modifier}+KeyJ`); - - await expect( - page.getByTestId('response-layout-toggle-btn') - ).toHaveAttribute('title', 'Switch to horizontal layout'); - }); - - test('customized Alt+Shift+Y change layout orientation', async ({ page, createTmpDir }) => { - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - // Remap changeLayout to Alt+D - await openKeybindingsTab(page); - const row = page.getByTestId('keybinding-row-changeLayout'); - await row.hover(); - await page.getByTestId('keybinding-edit-changeLayout').click(); - await expect(page.getByTestId('keybinding-input-changeLayout')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Alt'); - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Shift'); - await page.keyboard.up('Alt'); - - await openRequest(page, 'kb-collection', 'req-5', { persist: true }); - - await page.keyboard.down('Alt'); - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Shift'); - await page.keyboard.up('Alt'); - - await expect( - page.getByTestId('response-layout-toggle-btn') - ).toHaveAttribute('title', 'Switch to vertical layout'); - - // Press Cmd/Ctrl+J to change layout - await page.keyboard.down('Alt'); - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Shift'); - await page.keyboard.up('Alt'); - - await expect( - page.getByTestId('response-layout-toggle-btn') - ).toHaveAttribute('title', 'Switch to horizontal layout'); - - // Press Cmd/Ctrl+J to change layout - await page.keyboard.down('Alt'); - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Shift'); - await page.keyboard.up('Alt'); - - await expect( - page.getByTestId('response-layout-toggle-btn') - ).toHaveAttribute('title', 'Switch to vertical layout'); - }); - }); - - test.describe.serial('SHORTCUT: Open Preferences', () => { - test('default Cmd/Ctrl+, open preferences', async ({ page }) => { - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - // Press Cmd/Ctrl+J to change layout - await page.keyboard.down(modifier); - await page.keyboard.down('Comma'); - await page.keyboard.up('Comma'); - await page.keyboard.up(modifier); - - await expect(page.locator('.request-tab').filter({ hasText: 'Preferences' })).toBeVisible(); - }); - - test('customized Cmd/Ctrl+P open preferences', async ({ page }) => { - // Remap openPreferences to Ctrl+P - await openKeybindingsTab(page); - const row = page.getByTestId('keybinding-row-openPreferences'); - await row.hover(); - await page.getByTestId('keybinding-edit-openPreferences').click(); - await expect(page.getByTestId('keybinding-input-openPreferences')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down(modifier); - await page.keyboard.down('KeyP'); - await page.keyboard.up('KeyP'); - await page.keyboard.up(modifier); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - // Press Cmd/Ctrl+J to change layout - await page.keyboard.down(modifier); - await page.keyboard.down('KeyP'); - await page.keyboard.up('KeyP'); - await page.keyboard.up(modifier); - - await expect(page.locator('.request-tab').filter({ hasText: 'Preferences' })).toBeVisible(); - }); - }); - }); - - test.describe('SEARCH', () => { - test.describe.serial('SHORTCUT: Global Search', () => { - test('default Cmd/Ctrl+K Global Search Modal', async ({ page, createTmpDir }) => { - // Press Cmd/Ctrl+K to global search modal - await page.keyboard.press(`${modifier}+KeyK`); - - await page.keyboard.down(modifier); - await page.keyboard.down('KeyK'); - await page.keyboard.up('KeyK'); - await page.keyboard.up(modifier); - - await page.getByTestId('global-search-input').click(); - await expect(page.getByTestId('global-search-input')).toBeVisible(); - - // await page.waitForTimeout(500); - await page.keyboard.down('Escape'); - await page.keyboard.up('Escape'); - }); - - test('customized Alt+K Global Search Modal', async ({ page, createTmpDir }) => { - // Remap globalSearch to Alt+K - await openKeybindingsTab(page); - const row = page.getByTestId('keybinding-row-globalSearch'); - await row.hover(); - await page.getByTestId('keybinding-edit-globalSearch').click(); - await expect(page.getByTestId('keybinding-input-globalSearch')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyK'); - await page.keyboard.up('KeyK'); - await page.keyboard.up('Alt'); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyK'); - await page.keyboard.up('KeyK'); - await page.keyboard.up('Alt'); - - await page.getByTestId('global-search-input').click(); - await expect(page.getByTestId('global-search-input')).toBeVisible(); - - await page.keyboard.down('Escape'); - await page.keyboard.up('Escape'); - }); - }); - }); - - test.describe.serial('SHORTCUT: Edit Environment', () => { - test('open environment tab of collection Cmd/Ctrl+E', async ({ page, createTmpDir }) => { - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - await openRequest(page, 'kb-collection', 'req-7', { persist: true }); - - await page.keyboard.down(modifier); - await page.keyboard.down('KeyE'); - await page.keyboard.up('KeyE'); - await page.keyboard.up(modifier); - - await expect(page.locator('.request-tab').filter({ hasText: 'Environments' })).toBeVisible(); - }); - - test('open environment tab of collection customized Alt+E', async ({ page, createTmpDir }) => { - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - // Remap editEnvironment to Alt+E - await openKeybindingsTab(page); - const row = page.getByTestId('keybinding-row-editEnvironment'); - await row.hover(); - await page.getByTestId('keybinding-edit-editEnvironment').click(); - await expect(page.getByTestId('keybinding-input-editEnvironment')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyE'); - await page.keyboard.up('KeyE'); - await page.keyboard.up('Alt'); - - await openRequest(page, 'kb-collection', 'req-7', { persist: true }); - - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyE'); - await page.keyboard.up('KeyE'); - await page.keyboard.up('Alt'); - - await expect(page.locator('.request-tab').filter({ hasText: 'Environments' })).toBeVisible(); - - // Rest Default - just in case to not fail shortcuts in other places - await openKeybindingsTab(page); - await page.getByTestId('reset-all-keybindings-btn').click({ timeout: 2000 }); - }); - }); - - test.describe('REQUESTS', () => { - test.describe.serial('SHORTCUT: Send Request from CodeEditor (Cmd/Ctrl+Enter)', () => { - test('sends request when cursor is in JSON body editor', async ({ page }) => { - // Close existing tabs - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - // Create a POST request in the shared collection pointing to the echo server - await createRequest(page, 'cmd-enter-req-body', 'kb-collection', { - url: 'https://echo.usebruno.com', - method: 'POST' - }); - await openRequest(page, 'kb-collection', 'cmd-enter-req-body', { persist: true }); - - // Open Body tab and select JSON mode - await selectRequestPaneTab(page, 'Body'); - await page.getByTestId('request-body-mode-selector').click(); - await page.locator('.dropdown-item').filter({ hasText: /^JSON$/ }).click(); - - // Focus the body code editor and type JSON - const bodyEditor = page.getByTestId('request-body-editor').locator('.CodeMirror'); - await bodyEditor.click(); - await page.keyboard.type('{"name": "Bruno", "version": 2, "tags": ["api", "client", "http"], "active": true, "meta": {"author": "user", "created": "2025-01-01", "updated": "2025-06-01"}, "counts": {"requests": 42, "collections": 7}}'); - await expect(page.getByTestId('request-body-editor')).toContainText('"name": "Bruno"'); - - // Cursor is still in the body CodeMirror — press Cmd/Ctrl+Enter to send - await page.keyboard.press(`${modifier}+Enter`); - - // Verify a 200 response came back - await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 }); - }); - - test('sends request when cursor is in response body editor', async ({ page }) => { - // Close existing tabs - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - await createRequest(page, 'cmd-enter-req-resp', 'kb-collection', { - url: 'https://echo.usebruno.com', - method: 'POST' - }); - await openRequest(page, 'kb-collection', 'cmd-enter-req-resp', { persist: true }); - - await selectRequestPaneTab(page, 'Body'); - await page.getByTestId('request-body-mode-selector').click(); - await page.locator('.dropdown-item').filter({ hasText: /^JSON$/ }).click(); - - const bodyEditor = page.getByTestId('request-body-editor').locator('.CodeMirror'); - await bodyEditor.click(); - await page.keyboard.type('{"name": "Bruno", "version": 2, "tags": ["api", "client", "http"], "active": true, "meta": {"author": "user", "created": "2025-01-01", "updated": "2025-06-01"}, "counts": {"requests": 42, "collections": 7}}'); - await expect(page.getByTestId('request-body-editor')).toContainText('"name": "Bruno"'); - - // First send to populate response - await page.keyboard.press(`${modifier}+Enter`); - await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 }); - - // Focus cursor inside the response body CodeMirror - const responseEditor = page.getByTestId('response-preview-container').locator('.CodeMirror').first(); - await responseEditor.waitFor({ state: 'visible', timeout: 5000 }); - await responseEditor.click(); - - // Press Cmd/Ctrl+Enter again — should re-send the request - await page.keyboard.press(`${modifier}+Enter`); - - // Verify a 200 response came back (no error, status stays/refreshes to 200) - await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 }); - }); - - test('sends request when cursor is in pre-request Vars value editor', async ({ page }) => { - // Close existing tabs - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - await createRequest(page, 'cmd-enter-req-vars', 'kb-collection', { - url: 'https://echo.usebruno.com', - method: 'POST' - }); - await openRequest(page, 'kb-collection', 'cmd-enter-req-vars', { persist: true }); - - // Open Vars tab — request Vars has a Pre Request section as the first table - await selectRequestPaneTab(page, 'Vars'); - - // Fill the first var row: name=var-1 - const varsTable = page.getByTestId('request-pane').locator('table').first(); - const firstRow = varsTable.locator('tbody tr').first(); - const nameInput = firstRow.locator('input[type="text"]').first(); - await nameInput.click(); - await nameInput.fill('var-1'); - - // Click the value CodeMirror editor and type a multi-line value - const valueEditor = firstRow.locator('.CodeMirror').first(); - await valueEditor.click(); - await page.keyboard.type('val-1'); - await page.keyboard.press('Enter'); // insert newline in value editor - await page.keyboard.type('val-2'); - - // Cursor is still in the value CodeMirror — press Cmd/Ctrl+Enter to send - // (should NOT insert a newline; should fire sendRequest) - await page.keyboard.press(`${modifier}+Enter`); - - // Verify a 200 response came back - await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 }); - }); - }); - - test.describe.serial('SHORTCUT: Send Request from CodeEditor (customized Shift+Enter)', () => { - test('customized Shift+Enter sends request when cursor is in JSON body editor', async ({ page }) => { - // Close existing tabs - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - // Remap sendRequest to Shift+Enter - await openKeybindingsTab(page); - const row = page.getByTestId('keybinding-row-sendRequest'); - await row.hover(); - await page.getByTestId('keybinding-edit-sendRequest').click(); - await expect(page.getByTestId('keybinding-input-sendRequest')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Shift'); - await page.keyboard.down('Enter'); - await page.keyboard.up('Enter'); - await page.keyboard.up('Shift'); - - // await closePreferencesTab(page); - // Create a POST request in the shared collection pointing to the echo server - await createRequest(page, 'shift-enter-req-body', 'kb-collection', { - url: 'https://echo.usebruno.com', - method: 'POST' - }); - await openRequest(page, 'kb-collection', 'shift-enter-req-body', { persist: true }); - - // Open Body tab and select JSON mode - await selectRequestPaneTab(page, 'Body'); - await page.getByTestId('request-body-mode-selector').click(); - await page.locator('.dropdown-item').filter({ hasText: /^JSON$/ }).click(); - - const bodyEditor = page.getByTestId('request-body-editor').locator('.CodeMirror'); - await bodyEditor.click(); - await page.keyboard.type('{"name": "Bruno", "version": 2, "tags": ["api", "client", "http"], "active": true, "meta": {"author": "user", "created": "2025-01-01", "updated": "2025-06-01"}, "counts": {"requests": 42, "collections": 7}}'); - await expect(page.getByTestId('request-body-editor')).toContainText('"name": "Bruno"'); - - // Cursor is still in the body CodeMirror — press Shift+Enter (customized) to send - await page.keyboard.press('Shift+Enter'); - - await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 }); - - // Reset Default - await openKeybindingsTab(page); - await page.getByTestId('reset-all-keybindings-btn').click({ timeout: 2000 }); - }); - - test('customized Shift+Enter sends request when cursor is in response body editor', async ({ page }) => { - // Close existing tabs - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - // Remap sendRequest to Shift+Enter - await openKeybindingsTab(page); - const row = page.getByTestId('keybinding-row-sendRequest'); - await row.hover(); - await page.getByTestId('keybinding-edit-sendRequest').click(); - await expect(page.getByTestId('keybinding-input-sendRequest')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Shift'); - await page.keyboard.down('Enter'); - await page.keyboard.up('Enter'); - await page.keyboard.up('Shift'); - - // await closePreferencesTab(page); - await createRequest(page, 'shift-enter-req-resp', 'kb-collection', { - url: 'https://echo.usebruno.com', - method: 'POST' - }); - await openRequest(page, 'kb-collection', 'shift-enter-req-resp', { persist: true }); - - await selectRequestPaneTab(page, 'Body'); - await page.getByTestId('request-body-mode-selector').click(); - await page.locator('.dropdown-item').filter({ hasText: /^JSON$/ }).click(); - - const bodyEditor = page.getByTestId('request-body-editor').locator('.CodeMirror'); - await bodyEditor.click(); - await page.keyboard.type('{"name": "Bruno", "version": 2, "tags": ["api", "client", "http"], "active": true, "meta": {"author": "user", "created": "2025-01-01", "updated": "2025-06-01"}, "counts": {"requests": 42, "collections": 7}}'); - await expect(page.getByTestId('request-body-editor')).toContainText('"name": "Bruno"'); - - // First send with Shift+Enter to populate response - await page.keyboard.press('Shift+Enter'); - await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 }); - - // Focus cursor inside the response body CodeMirror - const responseEditor = page.getByTestId('response-preview-container').locator('.CodeMirror').first(); - await responseEditor.waitFor({ state: 'visible', timeout: 5000 }); - await responseEditor.click(); - - // Press Shift+Enter again — should re-send the request - await page.keyboard.press('Shift+Enter'); - - await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 }); - - // Reset Default - await openKeybindingsTab(page); - await page.getByTestId('reset-all-keybindings-btn').click({ timeout: 2000 }); - }); - - test('customized Shift+Enter sends request when cursor is in pre-request Vars value editor', async ({ page }) => { - // Close existing tabs - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyY'); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Alt'); - - // Remap sendRequest to Shift+Enter - await openKeybindingsTab(page); - const row = page.getByTestId('keybinding-row-sendRequest'); - await row.hover(); - await page.getByTestId('keybinding-edit-sendRequest').click(); - await expect(page.getByTestId('keybinding-input-sendRequest')).toBeVisible(); - - await page.keyboard.down('Backspace'); - - await page.keyboard.down('Shift'); - await page.keyboard.down('Enter'); - await page.keyboard.up('Enter'); - await page.keyboard.up('Shift'); - - // await closePreferencesTab(page); - await createRequest(page, 'shift-enter-req-vars', 'kb-collection', { - url: 'https://echo.usebruno.com', - method: 'POST' - }); - await openRequest(page, 'kb-collection', 'shift-enter-req-vars', { persist: true }); - - await selectRequestPaneTab(page, 'Vars'); - - const varsTable = page.getByTestId('request-pane').locator('table').first(); - const firstRow = varsTable.locator('tbody tr').first(); - const nameInput = firstRow.locator('input[type="text"]').first(); - await nameInput.click(); - await nameInput.fill('var-1'); - - const valueEditor = firstRow.locator('.CodeMirror').first(); - await valueEditor.click(); - await page.keyboard.type('val-1'); - await page.keyboard.press('Enter'); // insert newline in value editor - await page.keyboard.type('val-2'); - - // Cursor is still in the value CodeMirror — press Shift+Enter (customized) to send - await page.keyboard.press('Shift+Enter'); - - await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 }); - - // Reset Default - await openKeybindingsTab(page); - await page.getByTestId('reset-all-keybindings-btn').click({ timeout: 2000 }); - }); - }); - }); -}); diff --git a/tests/shortcuts/collections-env.spec.ts b/tests/shortcuts/collections-env.spec.ts new file mode 100644 index 000000000..d7030811d --- /dev/null +++ b/tests/shortcuts/collections-env.spec.ts @@ -0,0 +1,173 @@ +import { expect, test } from '../../playwright'; +import { closeAllCollections } from '../utils/page'; +import { + closePreferencesTab, + collectionName, + modifier, + openKeybindingsTab, + openRequest, + pressShortcut, + resetKeybindings, + setupBoundActionsData +} from './helpers'; + +test.describe('Shortcut Keys - BOUND_ACTIONS', () => { + test.beforeEach(async ({ pageWithUserData: page, createTmpDir }) => { + await page.locator('[data-app-state="loaded"]').waitFor(); ; + await setupBoundActionsData(page, createTmpDir); + }); + + test.afterEach(async ({ pageWithUserData: page }) => { + await resetKeybindings(page); + await closeAllCollections(page); + }); + + test.describe('COLLECTIONS & ENVIRONMENTS', () => { + test.describe('SHORTCUT: Import Collection', () => { + test('default Cmd/Ctrl+O open import collection modal', async ({ pageWithUserData: page }) => { + await pressShortcut(page, modifier, 'KeyO'); + + await expect(page.getByTestId('import-collection-modal')).toBeVisible(); + + // Close the modal to leave a clean state for subsequent tests + await page.getByTestId('modal-close-button').click(); + await expect(page.getByTestId('import-collection-modal')).not.toBeVisible(); + }); + + test('customized Alt+O open import collection modal', async ({ pageWithUserData: page }) => { + await pressShortcut(page, modifier, 'KeyW'); + + // Remap importCollection to Alt+O + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-importCollection'); + await row.hover(); + await page.getByTestId('keybinding-edit-importCollection').click(); + await expect(page.getByTestId('keybinding-input-importCollection')).toBeVisible(); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Alt', 'KeyO'); + + await closePreferencesTab(page); + + // Trigger the remapped shortcut + await pressShortcut(page, 'Alt', 'KeyO'); + + await expect(page.getByTestId('import-collection-modal')).toBeVisible(); + + // Close the modal to leave a clean state for subsequent tests + await page.getByTestId('modal-close-button').click(); + await expect(page.getByTestId('import-collection-modal')).not.toBeVisible(); + }); + }); + + test.describe('SHORTCUT: Edit Environment (Cmd/Ctrl+E)', () => { + test('open environment tab of collection Cmd/Ctrl+E', async ({ pageWithUserData: page }) => { + await openRequest(page, 'kb-collection', 'req-7', { persist: true }); + + await pressShortcut(page, modifier, 'KeyE'); + + await expect(page.locator('.request-tab').filter({ has: page.getByText('Environments', { exact: true }) })).toBeVisible(); + }); + + test('open environment tab of collection customized Alt+E', async ({ pageWithUserData: page }) => { + // Remap editEnvironment to Alt+E + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-editEnvironment'); + await row.hover(); + await page.getByTestId('keybinding-edit-editEnvironment').click(); + await expect(page.getByTestId('keybinding-input-editEnvironment')).toBeVisible(); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Alt', 'KeyE'); + + await openRequest(page, 'kb-collection', 'req-7', { persist: true }); + + await pressShortcut(page, 'Alt', 'KeyE'); + + await expect(page.locator('.request-tab').filter({ has: page.getByText('Environments', { exact: true }) })).toBeVisible(); + }); + }); + + test.describe('SHORTCUT: New Request', () => { + test('default Cmd/Ctrl+N opens new request modal for collection', async ({ pageWithUserData: page }) => { + // Focus the collection so the keybinding is active + await page.getByTestId('sidebar-collection-row').filter({ has: page.getByText(collectionName, { exact: true }) }).click(); + + await pressShortcut(page, modifier, 'KeyN'); + + await page.getByTestId('request-name').fill('nr-collection-cenv'); + await page.getByTestId('new-request-url').locator('.CodeMirror').click(); + await page.keyboard.type('https://echo.usebruno.com'); + await page.getByTestId('create-new-request-button').click(); + + await expect(page.locator('.request-tab').filter({ has: page.getByText('nr-collection-cenv', { exact: true }) })).toBeVisible(); + }); + + test('default Cmd/Ctrl+N opens new request modal for folder', async ({ pageWithUserData: page }) => { + // Focus the folder so the keybinding is active + await page.locator('.collection-item-name').filter({ has: page.getByText('kb-folder', { exact: true }) }).click(); + + await pressShortcut(page, modifier, 'KeyN'); + + await page.getByTestId('request-name').fill('nr-folder-cenv'); + await page.getByTestId('new-request-url').locator('.CodeMirror').click(); + await page.keyboard.type('https://echo.usebruno.com'); + await page.getByTestId('create-new-request-button').click(); + + await expect(page.locator('.request-tab').filter({ has: page.getByText('nr-folder-cenv', { exact: true }) })).toBeVisible(); + }); + + test('customized Alt+N opens new request modal for collection', async ({ pageWithUserData: page }) => { + // Remap newRequest to Alt+N + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-newRequest'); + await row.hover(); + await page.getByTestId('keybinding-edit-newRequest').click(); + await expect(page.getByTestId('keybinding-input-newRequest')).toBeVisible(); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Alt', 'KeyN'); + + // Focus the collection so the keybinding is active + await page.getByTestId('sidebar-collection-row').filter({ has: page.getByText(collectionName, { exact: true }) }).click(); + + await pressShortcut(page, 'Alt', 'KeyN'); + + await page.getByTestId('request-name').fill('nr-collection-cenv-altn'); + await page.getByTestId('new-request-url').locator('.CodeMirror').click(); + await page.keyboard.type('https://echo.usebruno.com'); + await page.getByTestId('create-new-request-button').click(); + + await expect(page.locator('.request-tab').filter({ has: page.getByText('nr-collection-cenv-altn', { exact: true }) })).toBeVisible(); + }); + + test('customized Alt+N opens new request modal for folder', async ({ pageWithUserData: page }) => { + // Remap newRequest to Alt+N + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-newRequest'); + await row.hover(); + await page.getByTestId('keybinding-edit-newRequest').click(); + await expect(page.getByTestId('keybinding-input-newRequest')).toBeVisible(); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Alt', 'KeyN'); + + // Focus the folder so the keybinding is active + await page.locator('.collection-item-name').filter({ has: page.getByText('kb-folder', { exact: true }) }).click(); + + await pressShortcut(page, 'Alt', 'KeyN'); + + await page.getByTestId('request-name').fill('nr-folder-cenv-altn'); + await page.getByTestId('new-request-url').locator('.CodeMirror').click(); + await page.keyboard.type('https://echo.usebruno.com'); + await page.getByTestId('create-new-request-button').click(); + + await expect(page.locator('.request-tab').filter({ has: page.getByText('nr-folder-cenv-altn', { exact: true }) })).toBeVisible(); + }); + }); + }); +}); diff --git a/tests/shortcuts/developer-tool.spec.ts b/tests/shortcuts/developer-tool.spec.ts new file mode 100644 index 000000000..cff5c72ec --- /dev/null +++ b/tests/shortcuts/developer-tool.spec.ts @@ -0,0 +1,95 @@ +import { expect, test } from '../../playwright'; +import { closeAllCollections } from '../utils/page'; +import { + modifier, + openKeybindingsTab, + pressShortcut, + resetKeybindings, + setupBoundActionsData +} from './helpers'; + +test.describe('Shortcut Keys - BOUND_ACTIONS', () => { + test.beforeEach(async ({ pageWithUserData: page, createTmpDir }) => { + await page.locator('[data-app-state="loaded"]').waitFor(); ; + await setupBoundActionsData(page, createTmpDir); + }); + + test.afterEach(async ({ pageWithUserData: page }) => { + await resetKeybindings(page); + await closeAllCollections(page); + }); + + test.describe('DEVELOPER TOOLS', () => { + test.describe('SHORTCUT: Open Terminal (Cmd/Ctrl+T)', () => { + test('default Cmd/Ctrl+T opens terminal', async ({ pageWithUserData: page }) => { + // Open Collection-Settings tab (double-click collection name) + await page.getByTestId('sidebar-collection-row').filter({ has: page.getByText('kb-collection', { exact: true }) }).click(); + await expect(page.locator('.request-tab').filter({ has: page.getByText('Collection', { exact: true }) })).toBeVisible(); + + // Press Cmd/Ctrl+T to open terminal at workspace level + await pressShortcut(page, modifier, 'KeyT'); + + // Verify terminal session is visible using data-testid + const collectionTerminalSession = page.getByTestId('session-list-0'); + await expect(collectionTerminalSession).toBeVisible(); + + const collectionSession = collectionTerminalSession; + await expect(collectionSession).toContainText('kb-collection'); + await page.getByTitle('Close console').click(); + + // Open Folder-Settings tab (create folder + double-click) + // Open folder settings + await page.locator('.collection-item-name').filter({ has: page.getByText('kb-terminal-folder', { exact: true }) }).dblclick(); + await expect(page.locator('.request-tab').filter({ has: page.getByText('kb-terminal-folder', { exact: true }) })).toBeVisible(); + + await pressShortcut(page, modifier, 'KeyT'); + const folderTerminalSession = page.getByTestId('session-list-1'); + await expect(folderTerminalSession).toBeVisible(); + + // Verify the terminal session name is the workspace name (default_workspace) + const folderSessionName = folderTerminalSession; + await expect(folderSessionName).toContainText('kb-terminal-folder'); + }); + }); + + test.describe('SHORTCUT: Open Terminal (customized Alt+T)', () => { + test('customized Alt+T opens terminal', async ({ pageWithUserData: page }) => { + // Remap openTerminal to Alt+T + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-openTerminal'); + await row.hover(); + await page.getByTestId('keybinding-edit-openTerminal').click(); + await expect(page.getByTestId('keybinding-input-openTerminal')).toBeVisible(); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Alt', 'KeyT'); + + await page.getByTestId('sidebar-collection-row').filter({ has: page.getByText('kb-collection', { exact: true }) }).click(); + await expect(page.locator('.request-tab').filter({ has: page.getByText('Collection', { exact: true }) })).toBeVisible(); + + // Press Cmd/Ctrl+T to open terminal at workspace level + await pressShortcut(page, 'Alt', 'KeyT'); + await page.waitForTimeout(500); + + // Verify terminal session is visible using data-testid + const collectionTerminalSession = page.getByTestId('session-list-0'); + await expect(collectionTerminalSession).toBeVisible(); + + const collectionSession = collectionTerminalSession; + await expect(collectionSession).toContainText('kb-collection'); + + // Open folder settings + await page.locator('.collection-item-name').filter({ has: page.getByText('kb-terminal-folder', { exact: true }) }).dblclick(); + + await pressShortcut(page, 'Alt', 'KeyT'); + const folderTerminalSession = page.getByTestId('session-list-1'); + await expect(folderTerminalSession).toBeVisible(); + + // Verify the terminal session name is the workspace name (default_workspace) + const folderSessionName = folderTerminalSession; + await expect(folderSessionName).toContainText('kb-terminal-folder'); + }); + }); + }); +}); diff --git a/tests/shortcuts/helpers.ts b/tests/shortcuts/helpers.ts new file mode 100644 index 000000000..d869d942a --- /dev/null +++ b/tests/shortcuts/helpers.ts @@ -0,0 +1,155 @@ +import { expect, Page } from '../../playwright'; +import { + closeAllCollections, + createCollection, + createFolder, + createRequest, + openCollection, + openRequest as openRequestBase +} from '../utils/page'; + +export const modifier = process.platform === 'darwin' ? 'Meta' : 'Control'; +export const collectionName = 'kb-collection'; +export const baseRequests = ['req-1', 'req-2', 'req-3', 'req-4', 'req-5', 'req-6', 'req-7', 'req-8', 'req-9']; + +export const pressShortcut = async (page: Page, ...keys: string[]) => { + for (const key of keys) { + await page.keyboard.down(key); + } + for (const key of [...keys].reverse()) { + await page.keyboard.up(key); + } +}; + +export const setupBoundActionsData = async (page: Page, createTmpDir: (prefix: string) => Promise) => { + await closeAllCollections(page); + const path = await createTmpDir('kb-collection-path'); + await createCollection(page, collectionName, path); + + await createFolder(page, 'kb-folder', collectionName, true); + await createFolder(page, 'kb-draft-folder', collectionName, true); + await createFolder(page, 'kb-terminal-folder', collectionName, true); +}; + +const checkIfRequestExists = async (page: Page, requestName: string) => { + await openCollection(page, collectionName); + const request = page.getByTestId('collections').locator('.collection-item-name').filter({ has: page.getByText(requestName, { exact: true }) }); + return (await request.count()) > 0; +}; + +export const openRequest = async (...args: Parameters) => { + const [page, targetCollectionName, requestName] = args; + if ( + targetCollectionName === collectionName + && baseRequests.includes(requestName) + && !(await checkIfRequestExists(page, requestName)) + ) { + await createRequest(page, requestName, targetCollectionName); + } + + return openRequestBase(...args); +}; + +export const openKeybindingsTab = async (page: Page) => { + await page.getByRole('button', { name: 'Open Preferences' }).click(); + await page.getByRole('tab', { name: 'Keybindings' }).click(); + await expect(page.locator('.section-header').filter({ has: page.getByText('Keybindings', { exact: true }) })).toBeVisible(); +}; + +export const closePreferencesTab = async (page: Page) => { + const prefTab = page.locator('.request-tab').filter({ has: page.getByText('Preferences', { exact: true }) }); + // Nothing to do if it's already closed. + if (!(await prefTab.isVisible().catch(() => false))) return; + + await expect(async () => { + await prefTab.hover(); + await prefTab.getByTestId('request-tab-close-icon').click({ force: true }); + await expect(prefTab).not.toBeVisible(); + }).toPass({ timeout: 10000 }); +}; + +export const resetKeybindings = async (page: Page) => { + await openKeybindingsTab(page); + const resetBtn = page.getByTestId('reset-all-keybindings-btn'); + + if (await resetBtn.isEnabled().catch(() => false)) { + await resetBtn.click({ timeout: 2000 }); + } + await closePreferencesTab(page); +}; + +export const closeTabByName = async (page: any, name: string | RegExp) => { + const tab = page.locator('.request-tab').filter({ has: page.getByText(name, { exact: true }) }); + await tab.dblclick(); + await tab.hover(); + await tab.getByTestId('request-tab-close-icon').click({ force: true }); + await expect(tab).not.toBeVisible(); +}; + +export const openFolderSettingsTab = async (page: Page, folderName: string) => { + await openCollection(page, collectionName); + const folderRow = page.locator('.collection-item-name').filter({ has: page.getByText(folderName, { exact: true }) }); + await expect(folderRow).toBeVisible(); + await folderRow.dblclick(); + await expect(page.locator('.request-tab').filter({ has: page.getByText(folderName, { exact: true }) })).toBeVisible(); +}; + +export const reopenClosedTab = async (page: Page, shortcut: () => Promise, expectedTabName: string | RegExp) => { + for (let attempt = 0; attempt < 3; attempt++) { + await page.locator('.request-tab').first().click(); + // Wait for a tab to become active (focus settled) before firing the shortcut. + await expect(page.locator('.request-tab.active')).toBeVisible(); + await shortcut(); + const reopenedTab = page.locator('.request-tab').filter({ has: page.getByText(expectedTabName, { exact: true }) }); + if ((await reopenedTab.count()) > 0) { + await expect(reopenedTab).toBeVisible(); + return; + } + // Event-driven backoff: give the reopened tab a chance to appear before retrying. + await reopenedTab.waitFor({ state: 'visible', timeout: 1000 }).catch(() => { }); + } + + await expect(page.locator('.request-tab').filter({ has: page.getByText(expectedTabName, { exact: true }) })).toBeVisible(); +}; + +export const remapKeybinding = async ( + page: Page, + action: string, + ...keys: string[] +) => { + await openKeybindingsTab(page); + const row = page.getByTestId(`keybinding-row-${action}`); + await expect(row).toBeVisible(); + await row.scrollIntoViewIfNeeded(); + await row.hover(); + const editButton = row.getByTestId(`keybinding-edit-${action}`); + const keybindingInput = page.getByTestId(`keybinding-input-${action}`); + + if (await editButton.isVisible().catch(() => false)) { + await editButton.click({ force: true }); + } else { + await row.click({ force: true }); + if (await editButton.isVisible().catch(() => false)) { + await editButton.click({ force: true }); + } + } + + await expect(keybindingInput).toBeVisible(); + + await page.keyboard.press('Backspace'); + await pressShortcut(page, ...keys); + await closePreferencesTab(page); +}; + +export const getTabIndex = async (page: Page, name: string) => { + const tabs = page.locator('.request-tab .tab-label'); + const count = await tabs.count(); + for (let i = 0; i < count; i++) { + const text = (await tabs.nth(i).innerText()).trim(); + if (text.includes(name)) { + return i; + } + } + + return -1; +}; diff --git a/tests/shortcuts/init-user-data/preferences.json b/tests/shortcuts/init-user-data/preferences.json new file mode 100644 index 000000000..841c1cbfa --- /dev/null +++ b/tests/shortcuts/init-user-data/preferences.json @@ -0,0 +1,8 @@ +{ + "preferences": { + "onboarding": { + "hasLaunchedBefore": true, + "hasSeenWelcomeModal": true + } + } +} diff --git a/tests/shortcuts/other-actions.spec.ts b/tests/shortcuts/other-actions.spec.ts new file mode 100644 index 000000000..c444e93b5 --- /dev/null +++ b/tests/shortcuts/other-actions.spec.ts @@ -0,0 +1,48 @@ +import { expect, test } from '../../playwright'; +import { closeAllCollections } from '../utils/page'; +import { + modifier, + openKeybindingsTab, + pressShortcut, + resetKeybindings, + setupBoundActionsData +} from './helpers'; + +test.describe('Shortcut Keys - BOUND_ACTIONS', () => { + test.beforeEach(async ({ pageWithUserData: page, createTmpDir }) => { + await page.locator('[data-app-state="loaded"]').waitFor(); ; + await setupBoundActionsData(page, createTmpDir); + }); + + test.afterEach(async ({ pageWithUserData: page }) => { + await resetKeybindings(page); + await closeAllCollections(page); + }); + + test.describe('OTHERS', () => { + test.describe('SHORTCUT: Open Preferences', () => { + test('default Cmd/Ctrl+, open preferences', async ({ pageWithUserData: page }) => { + await pressShortcut(page, modifier, 'Comma'); + + await expect(page.locator('.request-tab').filter({ has: page.getByText('Preferences', { exact: true }) })).toBeVisible(); + }); + }); + + test.describe('SHORTCUT: Open Preferences (customized Cmd/Ctrl+P)', () => { + test('customized Cmd/Ctrl+P open preferences', async ({ pageWithUserData: page }) => { + // Remap openPreferences to Ctrl+P + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-openPreferences'); + await row.hover(); + await page.getByTestId('keybinding-edit-openPreferences').click(); + await expect(page.getByTestId('keybinding-input-openPreferences')).toBeVisible(); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, modifier, 'KeyP'); + + await expect(page.locator('.request-tab').filter({ has: page.getByText('Preferences', { exact: true }) })).toBeVisible(); + }); + }); + }); +}); diff --git a/tests/shortcuts/request-actions.spec.ts b/tests/shortcuts/request-actions.spec.ts new file mode 100644 index 000000000..198969183 --- /dev/null +++ b/tests/shortcuts/request-actions.spec.ts @@ -0,0 +1,296 @@ +import { expect, test } from '../../playwright'; +import { closeAllCollections, createRequest, selectRequestPaneTab } from '../utils/page'; +import { + modifier, + openKeybindingsTab, + openRequest, + pressShortcut, + resetKeybindings, + setupBoundActionsData +} from './helpers'; + +test.describe('Shortcut Keys - BOUND_ACTIONS', () => { + test.beforeEach(async ({ pageWithUserData: page, createTmpDir }) => { + await page.locator('[data-app-state="loaded"]').waitFor(); ; + await setupBoundActionsData(page, createTmpDir); + }); + + test.afterEach(async ({ pageWithUserData: page }) => { + await resetKeybindings(page); + await closeAllCollections(page); + }); + + test.describe('REQUESTS', () => { + test.describe('SHORTCUT: Send Request (Cmd/Ctrl+Enter)', () => { + test('sends request when cursor is in JSON body editor', async ({ pageWithUserData: page }) => { + // Create a POST request in the shared collection pointing to the echo server + await createRequest(page, 'cmd-enter-req-body', 'kb-collection', { + url: 'https://echo.usebruno.com', + method: 'POST' + }); + await openRequest(page, 'kb-collection', 'cmd-enter-req-body', { persist: true }); + + // Open Body tab and select JSON mode + await selectRequestPaneTab(page, 'Body'); + await page.getByTestId('request-body-mode-selector').click(); + await page.locator('.dropdown-item').filter({ hasText: /^JSON$/ }).click(); + + // Focus the body code editor and type JSON + const bodyEditor = page.getByTestId('request-body-editor').locator('.CodeMirror'); + await bodyEditor.click(); + await page.keyboard.type('{"name": "Bruno", "version": 2, "tags": ["api", "client", "http"], "active": true, "meta": {"author": "user", "created": "2025-01-01", "updated": "2025-06-01"}, "counts": {"requests": 42, "collections": 7}}'); + await expect(page.getByTestId('request-body-editor')).toContainText('"name": "Bruno"'); + + // Cursor is still in the body CodeMirror - press Cmd/Ctrl+Enter to send + await pressShortcut(page, modifier, 'Enter'); + + // Verify a 200 response came back + await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 }); + }); + + test('sends request when cursor is in response body editor', async ({ pageWithUserData: page }) => { + await createRequest(page, 'cmd-enter-req-resp', 'kb-collection', { + url: 'https://echo.usebruno.com', + method: 'POST' + }); + await openRequest(page, 'kb-collection', 'cmd-enter-req-resp', { persist: true }); + + await selectRequestPaneTab(page, 'Body'); + await page.getByTestId('request-body-mode-selector').click(); + await page.locator('.dropdown-item').filter({ hasText: /^JSON$/ }).click(); + + const bodyEditor = page.getByTestId('request-body-editor').locator('.CodeMirror'); + await bodyEditor.click(); + await page.keyboard.type('{"name": "Bruno", "version": 2, "tags": ["api", "client", "http"], "active": true, "meta": {"author": "user", "created": "2025-01-01", "updated": "2025-06-01"}, "counts": {"requests": 42, "collections": 7}}'); + await expect(page.getByTestId('request-body-editor')).toContainText('"name": "Bruno"'); + + // First send to populate response + await pressShortcut(page, modifier, 'Enter'); + await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 }); + + // Focus cursor inside the response body CodeMirror + const responseEditor = page.getByTestId('response-preview-container').locator('.CodeMirror').first(); + await responseEditor.waitFor({ state: 'visible', timeout: 5000 }); + await responseEditor.click(); + + // Press Cmd/Ctrl+Enter again - should re-send the request + await pressShortcut(page, modifier, 'Enter'); + + // Verify a 200 response came back (no error, status stays/refreshes to 200) + await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 }); + }); + + test('sends request when cursor is in pre-request Vars value editor', async ({ pageWithUserData: page }) => { + await createRequest(page, 'cmd-enter-req-vars', 'kb-collection', { + url: 'https://echo.usebruno.com', + method: 'POST' + }); + await openRequest(page, 'kb-collection', 'cmd-enter-req-vars', { persist: true }); + + // Open Vars tab - request Vars has a Pre Request section as the first table + await selectRequestPaneTab(page, 'Vars'); + + // Fill the first var row: name=var-1 + const varsTable = page.getByTestId('request-pane').locator('table').first(); + const firstRow = varsTable.locator('tbody tr').first(); + const nameInput = firstRow.locator('input[type="text"]').first(); + await nameInput.click(); + await nameInput.fill('var-1'); + + // Click the value CodeMirror editor and type a multi-line value + const valueEditor = firstRow.locator('.CodeMirror').first(); + await valueEditor.click(); + await page.keyboard.type('val-1'); + await page.keyboard.press('Enter'); // insert newline in value editor + await page.keyboard.type('val-2'); + + // Cursor is still in the value CodeMirror - press Cmd/Ctrl+Enter to send + // (should NOT insert a newline; should fire sendRequest) + await pressShortcut(page, modifier, 'Enter'); + + // Verify a 200 response came back + await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 }); + }); + }); + + test.describe('SHORTCUT: Send Request (customized Shift+Enter)', () => { + test('sends request when cursor is in JSON body editor', async ({ pageWithUserData: page }) => { + // Remap sendRequest to Shift+Enter + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-sendRequest'); + await row.hover(); + await page.getByTestId('keybinding-edit-sendRequest').click(); + await expect(page.getByTestId('keybinding-input-sendRequest')).toBeVisible(); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Shift', 'Enter'); + + // Create a POST request in the shared collection pointing to the echo server + await createRequest(page, 'shift-enter-req-body', 'kb-collection', { + url: 'https://echo.usebruno.com', + method: 'POST' + }); + await openRequest(page, 'kb-collection', 'shift-enter-req-body', { persist: true }); + + // Open Body tab and select JSON mode + await selectRequestPaneTab(page, 'Body'); + await page.getByTestId('request-body-mode-selector').click(); + await page.locator('.dropdown-item').filter({ hasText: /^JSON$/ }).click(); + + const bodyEditor = page.getByTestId('request-body-editor').locator('.CodeMirror'); + await bodyEditor.click(); + await page.keyboard.type('{"name": "Bruno", "version": 2, "tags": ["api", "client", "http"], "active": true, "meta": {"author": "user", "created": "2025-01-01", "updated": "2025-06-01"}, "counts": {"requests": 42, "collections": 7}}'); + await expect(page.getByTestId('request-body-editor')).toContainText('"name": "Bruno"'); + + // Cursor is still in the body CodeMirror - press Shift+Enter (customized) to send + await pressShortcut(page, 'Shift', 'Enter'); + + await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 }); + }); + + test('sends request when cursor is in response body editor', async ({ pageWithUserData: page }) => { + // Remap sendRequest to Shift+Enter + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-sendRequest'); + await row.hover(); + await page.getByTestId('keybinding-edit-sendRequest').click(); + await expect(page.getByTestId('keybinding-input-sendRequest')).toBeVisible(); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Shift', 'Enter'); + + await createRequest(page, 'shift-enter-req-resp', 'kb-collection', { + url: 'https://echo.usebruno.com', + method: 'POST' + }); + await openRequest(page, 'kb-collection', 'shift-enter-req-resp', { persist: true }); + + await selectRequestPaneTab(page, 'Body'); + await page.getByTestId('request-body-mode-selector').click(); + await page.locator('.dropdown-item').filter({ hasText: /^JSON$/ }).click(); + + const bodyEditor = page.getByTestId('request-body-editor').locator('.CodeMirror'); + await bodyEditor.click(); + await page.keyboard.type('{"name": "Bruno", "version": 2, "tags": ["api", "client", "http"], "active": true, "meta": {"author": "user", "created": "2025-01-01", "updated": "2025-06-01"}, "counts": {"requests": 42, "collections": 7}}'); + await expect(page.getByTestId('request-body-editor')).toContainText('"name": "Bruno"'); + + // First send with Shift+Enter to populate response + await pressShortcut(page, 'Shift', 'Enter'); + await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 }); + + // Focus cursor inside the response body CodeMirror + const responseEditor = page.getByTestId('response-preview-container').locator('.CodeMirror').first(); + await responseEditor.waitFor({ state: 'visible', timeout: 5000 }); + await responseEditor.click(); + + // Press Shift+Enter again - should re-send the request + await pressShortcut(page, 'Shift', 'Enter'); + + await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 }); + }); + + test('sends request when cursor is in pre-request Vars value editor', async ({ pageWithUserData: page }) => { + // Remap sendRequest to Shift+Enter + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-sendRequest'); + await row.hover(); + await page.getByTestId('keybinding-edit-sendRequest').click(); + await expect(page.getByTestId('keybinding-input-sendRequest')).toBeVisible(); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Shift', 'Enter'); + + await createRequest(page, 'shift-enter-req-vars', 'kb-collection', { + url: 'https://echo.usebruno.com', + method: 'POST' + }); + await openRequest(page, 'kb-collection', 'shift-enter-req-vars', { persist: true }); + + await selectRequestPaneTab(page, 'Vars'); + + const varsTable = page.getByTestId('request-pane').locator('table').first(); + const firstRow = varsTable.locator('tbody tr').first(); + const nameInput = firstRow.locator('input[type="text"]').first(); + await nameInput.click(); + await nameInput.fill('var-1'); + + const valueEditor = firstRow.locator('.CodeMirror').first(); + await valueEditor.click(); + await page.keyboard.type('val-1'); + await page.keyboard.press('Enter'); // insert newline in value editor + await page.keyboard.type('val-2'); + + // Cursor is still in the value CodeMirror - press Shift+Enter (customized) to send + await pressShortcut(page, 'Shift', 'Enter'); + + await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 }); + }); + }); + + test.describe('SHORTCUT: Change Orientation (Cmd/Ctrl+J)', () => { + test('default Cmd/Ctrl+J change layout orientation', async ({ pageWithUserData: page }) => { + await openRequest(page, 'kb-collection', 'req-5', { persist: true }); + + // Press Cmd/Ctrl+J to change layout + await pressShortcut(page, modifier, 'KeyJ'); + + await expect( + page.getByTestId('response-layout-toggle-btn') + ).toHaveAttribute('title', 'Switch to horizontal layout'); + + // Press Cmd/Ctrl+J to change layout + await pressShortcut(page, modifier, 'KeyJ'); + + await expect( + page.getByTestId('response-layout-toggle-btn') + ).toHaveAttribute('title', 'Switch to vertical layout'); + + // Press Cmd/Ctrl+J to change layout + await pressShortcut(page, modifier, 'KeyJ'); + + await expect( + page.getByTestId('response-layout-toggle-btn') + ).toHaveAttribute('title', 'Switch to horizontal layout'); + }); + }); + + test.describe('SHORTCUT: Change Orientation (customized Alt+Shift+Y)', () => { + test('customized Alt+Shift+Y change layout orientation', async ({ pageWithUserData: page }) => { + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-changeLayout'); + await row.hover(); + await page.getByTestId('keybinding-edit-changeLayout').click(); + await expect(page.getByTestId('keybinding-input-changeLayout')).toBeVisible(); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Alt', 'Shift', 'KeyY'); + + await openRequest(page, 'kb-collection', 'req-5', { persist: true }); + + await pressShortcut(page, 'Alt', 'Shift', 'KeyY'); + + await expect( + page.getByTestId('response-layout-toggle-btn') + ).toHaveAttribute('title', 'Switch to horizontal layout'); + + // Press Alt+Shift+Y to change layout + await pressShortcut(page, 'Alt', 'Shift', 'KeyY'); + + await expect( + page.getByTestId('response-layout-toggle-btn') + ).toHaveAttribute('title', 'Switch to vertical layout'); + + // Press Alt+Shift+Y to change layout + await pressShortcut(page, 'Alt', 'Shift', 'KeyY'); + + await expect( + page.getByTestId('response-layout-toggle-btn') + ).toHaveAttribute('title', 'Switch to horizontal layout'); + }); + }); + }); +}); diff --git a/tests/shortcuts/search-actions.spec.ts b/tests/shortcuts/search-actions.spec.ts new file mode 100644 index 000000000..3a5b57d8a --- /dev/null +++ b/tests/shortcuts/search-actions.spec.ts @@ -0,0 +1,57 @@ +import { expect, test } from '../../playwright'; +import { closeAllCollections } from '../utils/page'; +import { + closePreferencesTab, + modifier, + openKeybindingsTab, + pressShortcut, + resetKeybindings, + setupBoundActionsData +} from './helpers'; + +test.describe('Shortcut Keys - BOUND_ACTIONS', () => { + test.beforeEach(async ({ pageWithUserData: page, createTmpDir }) => { + await page.locator('[data-app-state="loaded"]').waitFor(); ; + await setupBoundActionsData(page, createTmpDir); + }); + + test.afterEach(async ({ pageWithUserData: page }) => { + await resetKeybindings(page); + await closeAllCollections(page); + }); + + test.describe('SEARCH', () => { + test.describe('SHORTCUT: Global Search (Cmd/Ctrl+K)', () => { + test('default Cmd/Ctrl+K Global Search Modal', async ({ pageWithUserData: page }) => { + await pressShortcut(page, modifier, 'KeyK'); + + await expect(page.getByTestId('global-search-input')).toBeVisible(); + await page.getByTestId('global-search-input').click(); + + await pressShortcut(page, 'Escape'); + }); + + test('customized Shift+K Global Search Modal', async ({ pageWithUserData: page }) => { + // Remap globalSearch to Shift+K + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-globalSearch'); + await row.hover(); + await page.getByTestId('keybinding-edit-globalSearch').click(); + await expect(page.getByTestId('keybinding-input-globalSearch')).toBeVisible(); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Shift', 'KeyK'); + + await closePreferencesTab(page); + + await pressShortcut(page, 'Shift', 'KeyK'); + + await expect(page.getByTestId('global-search-input')).toBeVisible(); + await page.getByTestId('global-search-input').click(); + + await pressShortcut(page, 'Escape'); + }); + }); + }); +}); diff --git a/tests/shortcuts/sidebar-actions.spec.ts b/tests/shortcuts/sidebar-actions.spec.ts new file mode 100644 index 000000000..5791e9c10 --- /dev/null +++ b/tests/shortcuts/sidebar-actions.spec.ts @@ -0,0 +1,473 @@ +import { expect, test } from '../../playwright'; +import { closeAllCollections, createFolder, openCollection } from '../utils/page'; +import { + closePreferencesTab, + collectionName, + modifier, + openFolderSettingsTab, + openKeybindingsTab, + openRequest, + pressShortcut, + remapKeybinding, + resetKeybindings, + setupBoundActionsData +} from './helpers'; + +test.describe('Shortcut Keys - BOUND_ACTIONS', () => { + test.beforeEach(async ({ pageWithUserData: page, createTmpDir }) => { + await page.locator('[data-app-state="loaded"]').waitFor(); + await setupBoundActionsData(page, createTmpDir); + }); + + test.afterEach(async ({ pageWithUserData: page }) => { + await resetKeybindings(page); + await closeAllCollections(page); + }); + + test.describe('SIDEBAR', () => { + test.describe('SHORTCUT: Sidebar search (Cmd/Ctrl+F)', () => { + test('Sidebar search default (Cmd/Ctrl+F)', async ({ pageWithUserData: page }) => { + await pressShortcut(page, modifier, 'KeyF'); + + await expect(page.getByTestId('sidebar-search-input')).toBeVisible(); + await page.getByTitle('Search requests').click(); + }); + + test('Sidebar search customized (Alt+F)', async ({ pageWithUserData: page }) => { + // Remap sidebarSearch to Alt+F + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-sidebarSearch'); + await row.hover(); + await page.getByTestId('keybinding-edit-sidebarSearch').click(); + await expect(page.getByTestId('keybinding-input-sidebarSearch')).toBeVisible(); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Alt', 'KeyF'); + + await closePreferencesTab(page); + + await pressShortcut(page, 'Alt', 'KeyF'); + + await expect(page.getByTestId('sidebar-search-input')).toBeVisible(); + await page.getByTitle('Search requests').click(); + }); + }); + + test.describe('SHORTCUT: New request (Cmd/Ctrl+N)', () => { + test('New request default (Cmd/Ctrl+N)', async ({ pageWithUserData: page }) => { + await page.locator('.collection-item-name').filter({ has: page.getByText('kb-folder', { exact: true }) }).click(); + + await pressShortcut(page, modifier, 'KeyN'); + + await page.getByTestId('request-name').fill('nr-folder'); + await page.getByTestId('new-request-url').locator('.CodeMirror').click(); + await page.keyboard.type('https://echo.usebruno.com'); + await page.getByTestId('create-new-request-button').click(); + + await expect(page.locator('.request-tab').filter({ has: page.getByText('nr-folder', { exact: true }) })).toBeVisible(); + }); + + test('New request customized (Alt+N)', async ({ pageWithUserData: page }) => { + // Remap newRequest to Alt+N + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-newRequest'); + await row.hover(); + await page.getByTestId('keybinding-edit-newRequest').click(); + await expect(page.getByTestId('keybinding-input-newRequest')).toBeVisible(); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Alt', 'KeyN'); + + await page.getByTestId('sidebar-collection-row').filter({ has: page.getByText('kb-collection', { exact: true }) }).click(); + + await pressShortcut(page, 'Alt', 'KeyN'); + + await page.getByTestId('request-name').fill('nr-collection'); + await page.getByTestId('new-request-url').locator('.CodeMirror').click(); + await page.keyboard.type('https://echo.usebruno.com'); + await page.getByTestId('create-new-request-button').click(); + + await expect(page.locator('.request-tab').filter({ has: page.getByText('nr-collection', { exact: true }) })).toBeVisible(); + }); + }); + + test.describe('SHORTCUT: Rename Item', () => { + test.describe('SHORTCUT: Rename Item for request (Cmd/Ctrl+R)', () => { + test('default Cmd/Ctrl+R open rename item modal for request', async ({ pageWithUserData: page }) => { + await page.getByTestId('sidebar-collection-row').filter({ has: page.getByText('kb-collection', { exact: true }) }).dblclick(); + await openRequest(page, 'kb-collection', 'req-1', { persist: true }); + await pressShortcut(page, modifier, 'KeyR'); + + // Verify rename modal opens + const renameModal = page.locator('.bruno-modal-card').filter({ hasText: /rename request/i }); + await expect(renameModal).toBeVisible(); + + // Fill in the rename req name + const requestNameInput = page.locator('#collection-item-name'); + await requestNameInput.fill('req-1-renamed'); + + // Click the rename button + await page.getByTestId('rename-item-button').click(); + + // Verify renamed request appears in sidebar + await expect(page.locator('.collection-item-name').filter({ has: page.getByText('req-1-renamed', { exact: true }) })).toBeVisible(); + }); + }); + + test.describe('SHORTCUT: Rename Item for folder (Cmd/Ctrl+R)', () => { + test('default Cmd/Ctrl+R open rename item modal for folder', async ({ pageWithUserData: page }) => { + await page.locator('.collection-item-name').filter({ has: page.getByText('kb-folder', { exact: true }) }).dblclick(); + await pressShortcut(page, modifier, 'KeyR'); + + // Verify rename modal opens + const renameModal = page.locator('.bruno-modal-card').filter({ hasText: /rename folder/i }); + await expect(renameModal).toBeVisible(); + + // Fill in the rename req name + const folderNameInput = page.locator('#collection-item-name'); + await folderNameInput.fill('kb-folder-renamed'); + + // Click the rename button + await page.getByTestId('rename-item-button').click(); + + // Verify renamed request appears in sidebar + await expect(page.locator('.collection-item-name').filter({ has: page.getByText('kb-folder-renamed', { exact: true }) })).toBeVisible(); + }); + }); + + test.describe('SHORTCUT: Rename Item for collection (Cmd/Ctrl+R)', () => { + test('default Cmd/Ctrl+R open rename item modal for collection', async ({ pageWithUserData: page }) => { + await page.getByTestId('sidebar-collection-row').filter({ has: page.getByText('kb-collection', { exact: true }) }).click(); + await pressShortcut(page, modifier, 'KeyR'); + + // Verify rename modal opens + const renameModal = page.locator('.bruno-modal-card').filter({ hasText: /rename collection/i }); + await expect(renameModal).toBeVisible(); + + // Fill in the rename req name + const collectionInput = page.locator('#collection-name'); + await collectionInput.fill('kb-collection-renamed'); + + // Click the rename button + await page.locator('.submit').click(); + + // Verify renamed request appears in sidebar + await expect(page.getByTestId('sidebar-collection-row').filter({ has: page.getByText('kb-collection-renamed', { exact: true }) })).toBeVisible(); + }); + }); + + test.describe('SHORTCUT: Rename Item for request (customized Alt+X)', () => { + test('customized Alt+X open rename item modal for request', async ({ pageWithUserData: page }) => { + // Remap renameItem to Alt+R + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-renameItem'); + await row.hover(); + await page.getByTestId('keybinding-edit-renameItem').click(); + await expect(page.getByTestId('keybinding-input-renameItem')).toBeVisible(); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Alt', 'KeyX'); + + await openRequest(page, collectionName, 'req-1', { persist: true }); + await pressShortcut(page, 'Alt', 'KeyX'); + + // Verify rename modal opens + const renameModal = page.locator('.bruno-modal-card').filter({ hasText: /rename request/i }); + await expect(renameModal).toBeVisible(); + + // Fill in the rename req name + const requestNameInput = page.locator('#collection-item-name'); + await requestNameInput.fill('req-1-renamed-altx'); + + // Click the rename button + await page.getByTestId('rename-item-button').click(); + + // Verify renamed request appears in sidebar + await expect(page.locator('.collection-item-name').filter({ has: page.getByText('req-1-renamed-altx', { exact: true }) })).toBeVisible(); + }); + }); + + test.describe('SHORTCUT: Rename Item for folder (customized Alt+X)', () => { + test('customized Alt+R open rename item modal for folder', async ({ pageWithUserData: page }) => { + await remapKeybinding(page, 'renameItem', 'Alt', 'KeyX'); + + await createFolder(page, 'kb-folder-rename-src', collectionName, true); + await openFolderSettingsTab(page, 'kb-folder-rename-src'); + await pressShortcut(page, 'Alt', 'KeyX'); + + // Verify rename modal opens + const renameModal = page.locator('.bruno-modal-card').filter({ hasText: /rename folder/i }); + await expect(renameModal).toBeVisible(); + + // Fill in the rename req name + const folderNameInput = page.locator('#collection-item-name'); + await folderNameInput.fill('kb-folder-renamed-altx-src'); + + // Click the rename button + await page.getByTestId('rename-item-button').click(); + + // Verify renamed request appears in sidebar + await expect(page.locator('.collection-item-name').filter({ has: page.getByText('kb-folder-renamed-altx-src', { exact: true }) })).toBeVisible(); + }); + }); + + test.describe('SHORTCUT: Rename Item for collection (customized Alt+X)', () => { + test('customized Alt+R open rename item modal for collection', async ({ pageWithUserData: page }) => { + await remapKeybinding(page, 'renameItem', 'Alt', 'KeyX'); + + await page.getByTestId('sidebar-collection-row').filter({ has: page.getByText(collectionName, { exact: true }) }).click(); + await pressShortcut(page, 'Alt', 'KeyX'); + + // Verify rename modal opens + const renameModal = page.locator('.bruno-modal-card').filter({ hasText: /rename collection/i }); + await expect(renameModal).toBeVisible(); + + // Fill in the rename req name + const collectionInput = page.locator('#collection-name'); + await collectionInput.fill('kb-collection-renamed-altx'); + + // Click the rename button + await page.locator('.submit').click(); + + // Verify renamed request appears in sidebar + await expect(page.getByTestId('sidebar-collection-row').filter({ has: page.getByText('kb-collection-renamed-altx', { exact: true }) })).toBeVisible(); + }); + }); + }); + + test.describe('SHORTCUT: Clone Item', () => { + test.describe('SHORTCUT: Clone Item for request (Cmd/Ctrl+D)', () => { + test('default Cmd/Ctrl+D open clone item modal for request', async ({ pageWithUserData: page }) => { + await openRequest(page, 'kb-collection', 'req-1', { persist: true }); + await pressShortcut(page, modifier, 'KeyD'); + + // Verify clone modal opens + const cloneModal = page.locator('.bruno-modal-card').filter({ hasText: /clone request/i }); + await expect(cloneModal).toBeVisible(); + + // Fill in the clone req name + const requestNameInput = page.locator('#collection-item-name'); + await requestNameInput.fill('req-1 clone 1'); + + // Click the clone button + await page.getByTestId('clone-item-button').click(); + + // Verify cloned request appears in sidebar + await expect(page.locator('.collection-item-name').filter({ has: page.getByText('req-1 clone 1', { exact: true }) })).toBeVisible(); + }); + }); + + test.describe('SHORTCUT: Clone Item for folder (Cmd/Ctrl+D)', () => { + test('default Cmd/Ctrl+D open clone item modal for folder', async ({ pageWithUserData: page }) => { + await page.locator('.collection-item-name').filter({ has: page.getByText('kb-folder', { exact: true }) }).dblclick(); + await pressShortcut(page, modifier, 'KeyD'); + + // Verify clone modal opens + const cloneModal = page.locator('.bruno-modal-card').filter({ hasText: /clone folder/i }); + await expect(cloneModal).toBeVisible(); + + // Fill in the clone kb-folder name + const folderNameInput = page.locator('#collection-item-name'); + await folderNameInput.fill('kb-folder clone 1'); + + // Click the clone button + await page.getByTestId('clone-item-button').click(); + + // Verify cloned request appears in sidebar + await expect(page.locator('.collection-item-name').filter({ has: page.getByText('kb-folder clone 1', { exact: true }) })).toBeVisible(); + }); + }); + + test.describe('SHORTCUT: Clone Item for request (customized Alt+D)', () => { + test('customized Alt+D open clone item modal for request', async ({ pageWithUserData: page }) => { + // Remap cloneItem to Alt+D + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-cloneItem'); + await row.hover(); + await page.getByTestId('keybinding-edit-cloneItem').click(); + await expect(page.getByTestId('keybinding-input-cloneItem')).toBeVisible(); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Alt', 'KeyD'); + + await openRequest(page, 'kb-collection', 'req-2', { persist: true }); + + await pressShortcut(page, 'Alt', 'KeyD'); + + // Verify clone modal opens + const cloneModal = page.locator('.bruno-modal-card').filter({ hasText: /clone request/i }); + await expect(cloneModal).toBeVisible(); + + // Fill in the clone req name + const requestNameInput = page.locator('#collection-item-name'); + await requestNameInput.fill('req-2 clone 1'); + + // Click the clone button + await page.getByTestId('clone-item-button').click(); + + // Verify renamed request appears in sidebar + await expect(page.locator('.collection-item-name').filter({ has: page.getByText('req-2 clone 1', { exact: true }) })).toBeVisible(); + }); + }); + + test.describe('SHORTCUT: Clone Item for folder (customized Alt+D)', () => { + test('customized Alt+D open clone item modal for folder', async ({ pageWithUserData: page }) => { + // Remap cloneItem to Alt+D (keybindings are reset after each test, so re-bind here). + await remapKeybinding(page, 'cloneItem', 'Alt', 'KeyD'); + await closePreferencesTab(page); + + await createFolder(page, 'kb-folder-clone-src', collectionName, true); + await openCollection(page, collectionName); + await page.locator('.collection-item-name').filter({ has: page.getByText('kb-folder-clone-src', { exact: true }) }).first().click(); + await pressShortcut(page, 'Alt', 'KeyD'); + + // Verify clone modal opens + const cloneModal = page.locator('.bruno-modal-card').filter({ hasText: /clone folder/i }); + await expect(cloneModal).toBeVisible(); + + // Fill in the clone req name + const folderNameInput = page.locator('#collection-item-name'); + await folderNameInput.fill('kb-folder-clone-src copy 1'); + + // Click the clone button + await page.getByTestId('clone-item-button').click(); + + // Verify renamed request appears in sidebar + await expect(page.locator('.collection-item-name').filter({ has: page.getByText('kb-folder-clone-src copy 1', { exact: true }) })).toBeVisible(); + }); + }); + }); + + test.describe('SHORTCUT: Copy Paste Item', () => { + test.describe('SHORTCUT: Copy Paste Item for request (Cmd/Ctrl+C/V)', () => { + test('default Cmd/Ctrl+C/V copy paste item for request', async ({ pageWithUserData: page }) => { + await openRequest(page, 'kb-collection', 'req-3', { persist: true }); + await pressShortcut(page, modifier, 'KeyC'); + await pressShortcut(page, modifier, 'KeyV'); + + // Verify cloned request appears in sidebar + await expect(page.locator('.collection-item-name').filter({ has: page.getByText('req-3 (1)', { exact: true }) })).toBeVisible(); + }); + }); + + test.describe('SHORTCUT: Copy Paste Item for folder (Cmd/Ctrl+C/V)', () => { + test('default Cmd/Ctrl+C/V copy paste item for folder', async ({ pageWithUserData: page }) => { + await openRequest(page, collectionName, 'kb-folder', { persist: true }); + await pressShortcut(page, modifier, 'KeyC'); + await pressShortcut(page, modifier, 'KeyV'); + + // Verify copied item appears in sidebar as child of folder + await expect(page.locator('.collection-item-name').filter({ has: page.getByText('kb-folder', { exact: true }) })).toHaveCount(2); + }); + }); + + test.describe('SHORTCUT: Copy Paste Item for request (customized Alt+C/V)', () => { + test('customized Alt+C/V copy paste item for request', async ({ pageWithUserData: page }) => { + // Remap copyItem to Alt+D + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-copyItem'); + await row.hover(); + await page.getByTestId('keybinding-edit-copyItem').click(); + await expect(page.getByTestId('keybinding-input-copyItem')).toBeVisible(); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Alt', 'KeyC'); + + // Remap pasteItem to Alt+V + await openKeybindingsTab(page); + const row2 = page.getByTestId('keybinding-row-pasteItem'); + await row2.hover(); + await page.getByTestId('keybinding-edit-pasteItem').click(); + await expect(page.getByTestId('keybinding-input-pasteItem')).toBeVisible(); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Alt', 'KeyV'); + + await openRequest(page, 'kb-collection', 'req-4', { persist: true }); + await pressShortcut(page, 'Alt', 'KeyC'); + + await pressShortcut(page, 'Alt', 'KeyV'); + + // Verify cloned request appears in sidebar + await expect(page.locator('.collection-item-name').filter({ has: page.getByText('req-4 (1)', { exact: true }) })).toBeVisible(); + }); + }); + + test.describe('SHORTCUT: Copy Paste Item for folder (Cmd/Ctrl+C/V)', () => { + test('customized Alt+C/V copy paste item for folder', async ({ pageWithUserData: page }) => { + await remapKeybinding(page, 'copyItem', 'Alt', 'KeyC'); + await remapKeybinding(page, 'pasteItem', 'Alt', 'KeyV'); + + await createFolder(page, 'kb-folder-copy-src', collectionName, true); + await openFolderSettingsTab(page, 'kb-folder-copy-src'); + await pressShortcut(page, 'Alt', 'KeyC'); + + await pressShortcut(page, 'Alt', 'KeyV'); + + // Verify copied item appears in sidebar as child of folder + await expect(page.locator('.collection-item-name').filter({ has: page.getByText('kb-folder-copy-src', { exact: true }) })).toHaveCount(2); + }); + }); + }); + + test.describe('SHORTCUT: Collapse Sidebar', () => { + test('Collapse sidebar & Expand using default Cmd/Ctrl+\\', async ({ pageWithUserData: page }) => { + await expect(page.getByTestId('collections')).toBeVisible(); + await page.locator('body').click({ position: { x: 1, y: 1 } }); + + // Press Cmd/Ctrl+\ to collapse sidebar + await pressShortcut(page, modifier, 'Backslash'); + + // Verify sidebar collapsed to 0px + await expect.poll( + () => page.locator('aside.sidebar').evaluate((el) => getComputedStyle(el).width) + ).toBe('0px'); + + // Press Cmd/Ctrl+\ to expand sidebar + await pressShortcut(page, modifier, 'Backslash'); + + // Verify sidebar expanded to 250px + await expect.poll( + () => page.locator('aside.sidebar').evaluate((el) => getComputedStyle(el).width) + ).toBe('250px'); + }); + + test('Collapse sidebar & Expand using customized (Shift+G)', async ({ pageWithUserData: page }) => { + // Remap collapseSidebar to Shift+G + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-collapseSidebar'); + await row.hover(); + await page.getByTestId('keybinding-edit-collapseSidebar').click(); + await expect(page.getByTestId('keybinding-input-collapseSidebar')).toBeVisible(); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Shift', 'KeyG'); + + await closePreferencesTab(page); + + // Trigger the remapped shortcut to collapse sidebar + await pressShortcut(page, 'Shift', 'KeyG'); + + // Verify sidebar collapsed to 0px + await expect.poll( + () => page.locator('aside.sidebar').evaluate((el) => getComputedStyle(el).width) + ).toBe('0px'); + + // Trigger the remapped shortcut to expand sidebar + await pressShortcut(page, 'Shift', 'KeyG'); + + // Verify sidebar expanded to 250px + await expect.poll( + () => page.locator('aside.sidebar').evaluate((el) => getComputedStyle(el).width) + ).toBe('250px'); + }); + }); + }); +}); diff --git a/tests/shortcuts/tabs-actions.spec.ts b/tests/shortcuts/tabs-actions.spec.ts new file mode 100644 index 000000000..1b831dd2e --- /dev/null +++ b/tests/shortcuts/tabs-actions.spec.ts @@ -0,0 +1,760 @@ +import { expect, test } from '../../playwright'; +import { closeAllCollections } from '../utils/page'; +import { + closePreferencesTab, + closeTabByName, + collectionName, + getTabIndex, + modifier, + openKeybindingsTab, + openRequest, + pressShortcut, + remapKeybinding, + reopenClosedTab, + resetKeybindings, + setupBoundActionsData +} from './helpers'; + +test.describe('Shortcut Keys - BOUND_ACTIONS', () => { + test.beforeEach(async ({ pageWithUserData: page, createTmpDir }) => { + await page.locator('[data-app-state="loaded"]').waitFor(); ; + await setupBoundActionsData(page, createTmpDir); + }); + + test.afterEach(async ({ pageWithUserData: page }) => { + await resetKeybindings(page); + await closeAllCollections(page); + }); + + test.describe('TABS', () => { + test.describe('SHORTCUT: Close Tab', () => { + test('Close active tab default (Cmd/Ctrl+W)', async ({ pageWithUserData: page }) => { + await openRequest(page, collectionName, 'req-1', { persist: true }); + const reqTab = page.locator('.request-tab').filter({ has: page.getByText('req-1', { exact: true }) }); + // Click the tab to guarantee it's the focused/active tab before firing the shortcut. + await reqTab.click(); + await expect(reqTab).toHaveClass(/active/, { timeout: 2000 }); + + await pressShortcut(page, modifier, 'KeyW'); + await expect(reqTab).not.toBeVisible({ timeout: 3000 }); + }); + + test('Close active tab customized (Shift+X)', async ({ pageWithUserData: page }) => { + // Remap closeTab to Cmd/Ctrl+Shift+X + await openKeybindingsTab(page); + const row = page.getByTestId(`keybinding-row-closeTab`); + await row.hover(); + await page.getByTestId(`keybinding-edit-closeTab`).click(); + // Wait for input to enter recording mode + await expect(page.getByTestId(`keybinding-input-closeTab`)).toBeVisible({ timeout: 2000 }); + + // Remove the old keybindings + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Shift', 'KeyX'); + + await closePreferencesTab(page); + + await openRequest(page, collectionName, 'req-1', { persist: true }); + await expect(page.locator('.request-tab').filter({ has: page.getByText('req-1', { exact: true }) })).toBeVisible({ timeout: 2000 }); + + await pressShortcut(page, 'Shift', 'KeyX'); + await expect(page.locator('.request-tab').filter({ has: page.getByText('req-1', { exact: true }) })).not.toBeVisible({ timeout: 3000 }); + }); + }); + + // Closing a tab that has unsaved changes must surface the entity-specific + // "Unsaved changes" confirmation modal (request / folder / collection). + test.describe('SHORTCUT: Close Tab (Unsaved changes modal)', () => { + // These tests trigger close with the customized Shift+X binding; set it up + // once for the group. The top-level afterEach resets it after each test. + test.beforeEach(async ({ pageWithUserData: page }) => { + await remapKeybinding(page, 'closeTab', 'Shift', 'KeyX'); + await closePreferencesTab(page); + }); + + test('Close active tab customized (Shift+X) shows unsaved changes modal for request', async ({ pageWithUserData: page }) => { + // Open a request and make an unsaved change (edit the URL → draft) + await openRequest(page, collectionName, 'req-1', { persist: true }); + const requestTab = page.locator('.request-tab').filter({ has: page.getByText('req-1', { exact: true }) }); + await expect(requestTab).toBeVisible({ timeout: 2000 }); + + await page.locator('#request-url .CodeMirror').click(); + await page.keyboard.type('/users'); + await expect(requestTab.locator('.has-changes-icon')).toBeVisible(); + + await pressShortcut(page, 'Shift', 'KeyX'); + + // The request "Unsaved changes" modal should appear + const modal = page.locator('.bruno-modal-card').filter({ has: page.getByText('Unsaved changes', { exact: true }) }); + await expect(modal).toBeVisible({ timeout: 3000 }); + await expect(modal).toContainText('You have unsaved changes in request'); + await expect(modal).toContainText('req-1'); + await expect(modal.getByRole('button', { name: 'Don\'t Save' })).toBeVisible(); + await expect(modal.getByRole('button', { name: 'Cancel' })).toBeVisible(); + await expect(modal.getByRole('button', { name: 'Save', exact: true })).toBeVisible(); + + // Cancel keeps the tab open with its draft intact + await modal.getByRole('button', { name: 'Cancel' }).click(); + await expect(modal).not.toBeVisible(); + await expect(requestTab.locator('.has-changes-icon')).toBeVisible(); + + // Trigger again and discard → the tab closes + await pressShortcut(page, 'Escape'); + + await pressShortcut(page, 'Shift', 'KeyX'); + + await expect(modal).toBeVisible({ timeout: 3000 }); + await modal.getByRole('button', { name: 'Don\'t Save' }).click(); + await expect(requestTab).not.toBeVisible({ timeout: 3000 }); + }); + + test('Close active tab customized (Shift+X) shows unsaved changes modal for folder', async ({ pageWithUserData: page }) => { + // Open folder settings and make an unsaved change (add a header → draft) + await page.locator('.collection-item-name').filter({ has: page.getByText('kb-draft-folder', { exact: true }) }).dblclick(); + const folderTab = page.locator('.request-tab').filter({ has: page.getByText('kb-draft-folder', { exact: true }) }); + await expect(folderTab).toBeVisible({ timeout: 3000 }); + + const headerRow = page.locator('table').first().locator('tbody tr').first(); + await headerRow.locator('.CodeMirror').first().click(); + await page.keyboard.type('X-Folder-Header'); + await headerRow.locator('.CodeMirror').nth(1).click(); + await page.keyboard.type('folder-value'); + await expect(folderTab.locator('.has-changes-icon')).toBeVisible(); + + await pressShortcut(page, 'Shift', 'KeyX'); + + // The folder "Unsaved changes" modal should appear + const modal = page.locator('.bruno-modal-card').filter({ has: page.getByText('Unsaved changes', { exact: true }) }); + await expect(modal).toBeVisible({ timeout: 3000 }); + await expect(modal).toContainText('folder settings'); + await expect(modal).toContainText('kb-draft-folder'); + await expect(modal.getByRole('button', { name: 'Don\'t Save' })).toBeVisible(); + await expect(modal.getByRole('button', { name: 'Cancel' })).toBeVisible(); + await expect(modal.getByRole('button', { name: 'Save', exact: true })).toBeVisible(); + + // Cancel keeps the tab open with its draft intact + await modal.getByRole('button', { name: 'Cancel' }).click(); + await expect(modal).not.toBeVisible(); + await expect(folderTab.locator('.has-changes-icon')).toBeVisible(); + + // Trigger again and discard → the tab closes + await pressShortcut(page, 'Escape'); + + await pressShortcut(page, 'Shift', 'KeyX'); + + await expect(modal).toBeVisible({ timeout: 3000 }); + await modal.getByRole('button', { name: 'Don\'t Save' }).click(); + await expect(folderTab).not.toBeVisible({ timeout: 3000 }); + }); + + test('Close active tab customized (Shift+X) shows unsaved changes modal for collection', async ({ pageWithUserData: page }) => { + // Open collection settings and make an unsaved change (add a header → draft) + await page.getByTestId('sidebar-collection-row').filter({ has: page.getByText('kb-collection', { exact: true }) }).dblclick(); + const collectionTab = page.locator('.request-tab').filter({ has: page.getByText('Collection', { exact: true }) }); + await expect(collectionTab).toBeVisible({ timeout: 3000 }); + + await page.locator('.tab.headers').click(); + const headerRow = page.locator('table').first().locator('tbody tr').first(); + await headerRow.locator('.CodeMirror').first().click(); + await page.keyboard.type('X-Custom-Header'); + await headerRow.locator('.CodeMirror').nth(1).click(); + await page.keyboard.type('custom-value'); + await expect(collectionTab.locator('.has-changes-icon')).toBeVisible(); + + await pressShortcut(page, 'Shift', 'KeyX'); + + // The collection "Unsaved changes" modal should appear + const modal = page.locator('.bruno-modal-card').filter({ has: page.getByText('Unsaved changes', { exact: true }) }); + await expect(modal).toBeVisible({ timeout: 3000 }); + await expect(modal).toContainText('collection settings'); + await expect(modal).toContainText(collectionName); + await expect(modal.getByRole('button', { name: 'Don\'t Save' })).toBeVisible(); + await expect(modal.getByRole('button', { name: 'Cancel' })).toBeVisible(); + await expect(modal.getByRole('button', { name: 'Save', exact: true })).toBeVisible(); + + // Cancel keeps the tab open with its draft intact + await modal.getByRole('button', { name: 'Cancel' }).click(); + await expect(modal).not.toBeVisible(); + await expect(collectionTab.locator('.has-changes-icon')).toBeVisible(); + + // Trigger again and discard → the tab closes + await pressShortcut(page, 'Escape'); + + await pressShortcut(page, 'Shift', 'KeyX'); + + await expect(modal).toBeVisible({ timeout: 3000 }); + await modal.getByRole('button', { name: 'Don\'t Save' }).click(); + await expect(collectionTab).not.toBeVisible({ timeout: 3000 }); + }); + }); + + test.describe('SHORTCUT: Close All Tabs', () => { + test('Close all tabs default (Cmd/Ctrl+Shift+W)', async ({ pageWithUserData: page }) => { + await openRequest(page, collectionName, 'req-1', { persist: true }); + await openRequest(page, collectionName, 'req-2', { persist: true }); + await openRequest(page, collectionName, 'req-3', { persist: true }); + await page.getByTestId('runner').click(); + await expect(page.locator('.request-tab').filter({ has: page.getByText('req-1', { exact: true }) })).toBeVisible({ timeout: 2000 }); + await expect(page.locator('.request-tab').filter({ has: page.getByText('req-2', { exact: true }) })).toBeVisible({ timeout: 2000 }); + await expect(page.locator('.request-tab').filter({ has: page.getByText('req-3', { exact: true }) })).toBeVisible({ timeout: 2000 }); + + await pressShortcut(page, modifier, 'Shift', 'KeyW'); + await expect(page.locator('.request-tab').filter({ has: page.getByText('req-1', { exact: true }) })).not.toBeVisible({ timeout: 3000 }); + await expect(page.locator('.request-tab').filter({ has: page.getByText('req-2', { exact: true }) })).not.toBeVisible({ timeout: 3000 }); + await expect(page.locator('.request-tab').filter({ has: page.getByText('req-3', { exact: true }) })).not.toBeVisible({ timeout: 3000 }); + }); + + test('Close all tabs customized (Alt+Y)', async ({ pageWithUserData: page }) => { + // Remap closeAllTabs to Alt+Y + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-closeAllTabs'); + await row.hover(); + await page.getByTestId('keybinding-row-closeAllTabs').click(); + await expect(page.getByTestId('keybinding-input-closeAllTabs')).toBeVisible({ timeout: 2000 }); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Alt', 'KeyY'); + + await closePreferencesTab(page); + + await openRequest(page, collectionName, 'req-1', { persist: true }); + await openRequest(page, collectionName, 'req-2', { persist: true }); + await openRequest(page, collectionName, 'req-3', { persist: true }); + await expect(page.locator('.request-tab').filter({ has: page.getByText('req-1', { exact: true }) })).toBeVisible({ timeout: 2000 }); + await expect(page.locator('.request-tab').filter({ has: page.getByText('req-2', { exact: true }) })).toBeVisible({ timeout: 2000 }); + await expect(page.locator('.request-tab').filter({ has: page.getByText('req-3', { exact: true }) })).toBeVisible({ timeout: 2000 }); + + await pressShortcut(page, 'Alt', 'KeyY'); + await expect(page.locator('.request-tab').filter({ has: page.getByText('req-1', { exact: true }) })).not.toBeVisible({ timeout: 3000 }); + await expect(page.locator('.request-tab').filter({ has: page.getByText('req-2', { exact: true }) })).not.toBeVisible({ timeout: 3000 }); + await expect(page.locator('.request-tab').filter({ has: page.getByText('req-3', { exact: true }) })).not.toBeVisible({ timeout: 3000 }); + }); + }); + + test.describe('SHORTCUT: Save', () => { + test('Save tab customized (Cmd/Ctrl+S)', async ({ pageWithUserData: page }) => { + await page.getByTestId('sidebar-collection-row').filter({ has: page.getByText('kb-collection', { exact: true }) }).dblclick(); + await expect(page.locator('.request-tab').filter({ has: page.getByText('Collection', { exact: true }) })).toBeVisible({ timeout: 2000 }); + + // Verify initially there is NO draft indicator (close icon is present) + const collectionTab = page.locator('.request-tab').filter({ has: page.getByText('Collection', { exact: true }) }); + await expect(collectionTab.locator('.close-icon')).toBeVisible(); + await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible(); + + await page.locator('.tab.headers').click(); + + const headerTable = page.locator('table').first(); + const headerRow = headerTable.locator('tbody tr').first(); + + const nameEditor = headerRow.locator('.CodeMirror').first(); + await nameEditor.click(); + await page.keyboard.type('X-Custom-Header'); + + const valueEditor = headerRow.locator('.CodeMirror').nth(1); + await valueEditor.click(); + await page.keyboard.type('custom-value'); + + // Verify draft indicator appears in the tab + await expect(collectionTab.locator('.has-changes-icon')).toBeVisible(); + await expect(collectionTab.locator('.close-icon')).not.toBeVisible(); + + // Save the changes + await pressShortcut(page, modifier, 'KeyS'); + + // Verify draft indicator is gone after saving + await expect(collectionTab.locator('.close-icon')).toBeVisible(); + await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible(); + }); + + test('Save tab customized (Alt+S)', async ({ pageWithUserData: page }) => { + // Remap save to Alt+S + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-save'); + await row.hover(); + await page.getByTestId('keybinding-edit-save').click(); + await expect(page.getByTestId('keybinding-input-save')).toBeVisible({ timeout: 2000 }); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Alt', 'KeyS'); + + await closePreferencesTab(page); + + await page.getByTestId('sidebar-collection-row').filter({ has: page.getByText('kb-collection', { exact: true }) }).dblclick(); + await expect(page.locator('.request-tab').filter({ has: page.getByText('Collection', { exact: true }) })).toBeVisible({ timeout: 2000 }); + + // Verify initially there is NO draft indicator (close icon is present) + const collectionTab = page.locator('.request-tab').filter({ has: page.getByText('Collection', { exact: true }) }); + await expect(collectionTab.locator('.close-icon')).toBeVisible(); + await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible(); + + await page.locator('.tab.headers').click(); + + const headerTable = page.locator('table').first(); + const headerRow = headerTable.locator('tbody tr').first(); + + const nameEditor = headerRow.locator('.CodeMirror').first(); + await nameEditor.click(); + await page.keyboard.type('X-Custom-Header'); + + const valueEditor = headerRow.locator('.CodeMirror').nth(1); + await valueEditor.click(); + await page.keyboard.type('custom-value'); + + // Verify draft indicator appears in the tab + await expect(collectionTab.locator('.has-changes-icon')).toBeVisible(); + await expect(collectionTab.locator('.close-icon')).not.toBeVisible(); + + await page.locator('body').click({ position: { x: 1, y: 1 } }); + + // Save the changes + await pressShortcut(page, 'Alt', 'KeyS'); + + // Verify draft indicator is gone after saving + await expect(collectionTab.locator('.close-icon')).toBeVisible(); + await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible(); + }); + }); + + test.describe('SHORTCUT: Save All Tabs', () => { + test('Save all tabs default (Cmd/Ctrl+Shift+S)', async ({ pageWithUserData: page }) => { + await page.getByTestId('sidebar-collection-row').filter({ has: page.getByText('kb-collection', { exact: true }) }).dblclick(); + await expect(page.locator('.request-tab').filter({ has: page.getByText('Collection', { exact: true }) })).toBeVisible({ timeout: 2000 }); + + // Verify initially there is NO draft indicator (close icon is present) + const collectionTab = page.locator('.request-tab').filter({ has: page.getByText('Collection', { exact: true }) }); + await expect(collectionTab.locator('.close-icon')).toBeVisible(); + await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible(); + + await page.locator('.tab.headers').click(); + + const headerTable = page.locator('table').first(); + const headerRow = headerTable.locator('tbody tr').first(); + + const nameEditor = headerRow.locator('.CodeMirror').first(); + await nameEditor.click(); + await page.keyboard.type('X-Custom-Header'); + + const valueEditor = headerRow.locator('.CodeMirror').nth(1); + await valueEditor.click(); + await page.keyboard.type('custom-value'); + + // Verify draft indicator appears in the tab + await expect(collectionTab.locator('.has-changes-icon')).toBeVisible(); + await expect(collectionTab.locator('.close-icon')).not.toBeVisible(); + + // Open Folder-Settings tab (create folder + double-click) + await page.locator('.collection-item-name').filter({ has: page.getByText('kb-draft-folder', { exact: true }) }).dblclick(); + + // Verify folder settings tab is open + const folderTab = page.locator('.request-tab').filter({ has: page.getByText('kb-draft-folder', { exact: true }) }); + await expect(folderTab).toBeVisible(); + + await expect(folderTab.locator('.close-icon')).toBeVisible(); + await expect(folderTab.locator('.has-changes-icon')).not.toBeVisible(); + + const folderHeaderTable = page.locator('table').first(); + const folderHeaderRow = folderHeaderTable.locator('tbody tr').first(); + + const folderNameEditor = folderHeaderRow.locator('.CodeMirror').first(); + await folderNameEditor.click(); + await page.keyboard.type('X-Folder-Header'); + + const folderValueEditor = folderHeaderRow.locator('.CodeMirror').nth(1); + await folderValueEditor.click(); + await page.keyboard.type('folder-value'); + + // Verify draft indicator appears in the folder tab + await expect(folderTab.locator('.has-changes-icon')).toBeVisible(); + await expect(folderTab.locator('.close-icon')).not.toBeVisible(); + + // Save the changes + await pressShortcut(page, modifier, 'Shift', 'KeyS'); + + // Verify draft indicator is gone after saving + await expect(folderTab.locator('.close-icon')).toBeVisible(); + await expect(folderTab.locator('.has-changes-icon')).not.toBeVisible(); + + // Verify draft indicator is gone after saving + await expect(collectionTab.locator('.close-icon')).toBeVisible(); + await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible(); + }); + + test('Save all tabs customized (Alt+Shift+S)', async ({ pageWithUserData: page }) => { + // Remap saveAllTabs to Alt+Shift+S + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-saveAllTabs'); + await row.hover(); + await page.getByTestId('keybinding-edit-saveAllTabs').click(); + await expect(page.getByTestId('keybinding-input-saveAllTabs')).toBeVisible({ timeout: 2000 }); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Alt', 'Shift', 'KeyS'); + + await closePreferencesTab(page); + + await page.getByTestId('sidebar-collection-row').filter({ has: page.getByText(collectionName, { exact: true }) }).dblclick(); + await expect(page.locator('.request-tab').filter({ has: page.getByText('Collection', { exact: true }) })).toBeVisible({ timeout: 2000 }); + + // Verify initially there is NO draft indicator (close icon is present) + const collectionTab = page.locator('.request-tab').filter({ has: page.getByText('Collection', { exact: true }) }); + await expect(collectionTab.locator('.close-icon')).toBeVisible(); + await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible(); + + await page.locator('.tab.headers').click(); + + const headerTable = page.locator('table').first(); + const headerRow = headerTable.locator('tbody tr').first(); + + const nameEditor = headerRow.locator('.CodeMirror').first(); + await nameEditor.click(); + await page.keyboard.type('X-Custom-Header'); + + const valueEditor = headerRow.locator('.CodeMirror').nth(1); + await valueEditor.click(); + await page.keyboard.type('custom-value'); + + // Verify draft indicator appears in the tab + await expect(collectionTab.locator('.has-changes-icon')).toBeVisible(); + await expect(collectionTab.locator('.close-icon')).not.toBeVisible(); + + // Open Folder-Settings tab (create folder + double-click) + await page.locator('.collection-item-name').filter({ has: page.getByText('kb-draft-folder', { exact: true }) }).dblclick(); + + // Verify folder settings tab is open + const folderTab = page.locator('.request-tab').filter({ has: page.getByText('kb-draft-folder', { exact: true }) }); + await expect(folderTab).toBeVisible(); + + await expect(folderTab.locator('.close-icon')).toBeVisible(); + await expect(folderTab.locator('.has-changes-icon')).not.toBeVisible(); + + const folderHeaderTable = page.locator('table').first(); + const folderHeaderRow = folderHeaderTable.locator('tbody tr').first(); + + const folderNameEditor = folderHeaderRow.locator('.CodeMirror').first(); + await folderNameEditor.click(); + await page.keyboard.type('X-Folder-Header'); + + const folderValueEditor = folderHeaderRow.locator('.CodeMirror').nth(1); + await folderValueEditor.click(); + await page.keyboard.type('folder-value'); + + // Verify draft indicator appears in the folder tab + await expect(folderTab.locator('.has-changes-icon')).toBeVisible(); + await expect(folderTab.locator('.close-icon')).not.toBeVisible(); + + // Save the changes + await pressShortcut(page, 'Alt', 'Shift', 'KeyS'); + + // Verify draft indicator is gone after saving + await expect(folderTab.locator('.close-icon')).toBeVisible(); + await expect(folderTab.locator('.has-changes-icon')).not.toBeVisible(); + + // Verify draft indicator is gone after saving + await expect(collectionTab.locator('.close-icon')).toBeVisible(); + await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible(); + }); + }); + + test.describe('SHORTCUT: Switch to Previous Tab', () => { + test('Switch to Previous Tab default (Cmd/Ctrl+Shift+[)', async ({ pageWithUserData: page }) => { + await openRequest(page, collectionName, 'req-4', { persist: true }); + await openRequest(page, collectionName, 'req-5', { persist: true }); + await openRequest(page, collectionName, 'req-6', { persist: true }); + await expect(page.locator('.request-tab').filter({ has: page.getByText('req-6', { exact: true }) })).toBeVisible({ timeout: 2000 }); + + // req-6 is active (last opened) - press previous → req-5 + await pressShortcut(page, modifier, 'Shift', 'BracketLeft'); + await expect(page.locator('li.request-tab.active')).toHaveText(/req-5/, { timeout: 3000 }); + + // Press again → req-4 + await pressShortcut(page, modifier, 'Shift', 'BracketLeft'); + await expect(page.locator('li.request-tab.active')).toHaveText(/req-4/, { timeout: 3000 }); + }); + + test('Switch to Previous Tab customized (Shift+P)', async ({ pageWithUserData: page }) => { + // Remap switchToPreviousTab to Shift+P + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-switchToPreviousTab'); + await row.hover(); + await page.getByTestId('keybinding-edit-switchToPreviousTab').click(); + await expect(page.getByTestId('keybinding-input-switchToPreviousTab')).toBeVisible({ timeout: 2000 }); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Shift', 'KeyP'); + + await closePreferencesTab(page); + + // Reuse the same requests opened in the default test + await openRequest(page, collectionName, 'req-4', { persist: true }); + await openRequest(page, collectionName, 'req-5', { persist: true }); + await openRequest(page, collectionName, 'req-6', { persist: true }); + await expect(page.locator('.request-tab').filter({ has: page.getByText('req-6', { exact: true }) })).toBeVisible({ timeout: 2000 }); + + // req-6 is active - press Shift+P → req-5 + await pressShortcut(page, 'Shift', 'KeyP'); + await expect(page.locator('li.request-tab.active')).toHaveText(/req-5/, { timeout: 3000 }); + }); + }); + + test.describe('SHORTCUT: Switch to Next Tab', () => { + test('Switch to Next Tab default (Cmd/Ctrl+Shift+])', async ({ pageWithUserData: page }) => { + await openRequest(page, collectionName, 'req-4', { persist: true }); + await openRequest(page, collectionName, 'req-5', { persist: true }); + await openRequest(page, collectionName, 'req-6', { persist: true }); + + // Go back to req-4 to start from the left + await openRequest(page, 'kb-collection', 'req-4', { persist: true }); + await expect(page.locator('li.request-tab.active')).toHaveText(/req-4/); + + // req-4 is active - press next → req-5 + await pressShortcut(page, modifier, 'Shift', 'BracketRight'); + await expect(page.locator('li.request-tab.active')).toHaveText(/req-5/, { timeout: 3000 }); + + // Press again → req-6 + await pressShortcut(page, modifier, 'Shift', 'BracketRight'); + await expect(page.locator('li.request-tab.active')).toHaveText(/req-6/, { timeout: 3000 }); + }); + + test('Switch to Next Tab customized (Shift+N)', async ({ pageWithUserData: page }) => { + // Remap switchToNextTab to Shift+N + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-switchToNextTab'); + await row.hover(); + await page.getByTestId('keybinding-edit-switchToNextTab').click(); + await expect(page.getByTestId('keybinding-input-switchToNextTab')).toBeVisible({ timeout: 2000 }); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Shift', 'KeyN'); + + await closePreferencesTab(page); + + await openRequest(page, collectionName, 'req-4', { persist: true }); + await openRequest(page, collectionName, 'req-5', { persist: true }); + await openRequest(page, collectionName, 'req-6', { persist: true }); + + // Go back to req-4 + await openRequest(page, 'kb-collection', 'req-4', { persist: true }); + await expect(page.locator('li.request-tab.active')).toHaveText(/req-4/); + + // req-4 is active - press Shift+N → req-5 + await pressShortcut(page, 'Shift', 'KeyN'); + await expect(page.locator('li.request-tab.active')).toHaveText(/req-5/, { timeout: 3000 }); + }); + }); + + test.describe('SHORTCUT: Move Tab Left', () => { + test('Move Tab Left default (Cmd/Ctrl+[)', async ({ pageWithUserData: page }) => { + await openRequest(page, collectionName, 'req-7', { persist: true }); + await openRequest(page, collectionName, 'req-8', { persist: true }); + await openRequest(page, collectionName, 'req-9', { persist: true }); + + // req-9 is active and last + const tabs = page.locator('.request-tab'); + const totalTabs = await tabs.count(); + await expect(tabs.nth(totalTabs - 1)).toHaveText(/req-9/); + + // Press Cmd/Ctrl+[ → req-9 moves left, req-8 becomes last + await pressShortcut(page, modifier, 'BracketLeft'); + await expect(tabs.nth(totalTabs - 1)).toHaveText(/req-8/, { timeout: 3000 }); + await expect(tabs.nth(totalTabs - 2)).toHaveText(/req-9/); + + // Press again → req-9 moves one more position left + await pressShortcut(page, modifier, 'BracketLeft'); + await expect(tabs.nth(totalTabs - 3)).toHaveText(/req-9/, { timeout: 3000 }); + }); + + test('Move Tab Left customized (Alt+L)', async ({ pageWithUserData: page }) => { + // Remap moveTabLeft to Alt+L + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-moveTabLeft'); + await row.hover(); + await page.getByTestId('keybinding-edit-moveTabLeft').click(); + await expect(page.getByTestId('keybinding-input-moveTabLeft')).toBeVisible({ timeout: 2000 }); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Alt', 'KeyL'); + + await closePreferencesTab(page); + + await openRequest(page, 'kb-collection', 'req-7', { persist: true }); + await openRequest(page, 'kb-collection', 'req-8', { persist: true }); + await openRequest(page, 'kb-collection', 'req-9', { persist: true }); + + // req-9 is active + const tabs = page.locator('.request-tab'); + + // Press Alt+L → req-9 moves left, req-8 becomes last + await pressShortcut(page, 'Alt', 'KeyL'); + + await pressShortcut(page, 'Alt', 'KeyL'); + + await pressShortcut(page, 'Alt', 'KeyL'); + await expect(tabs.nth(0)).toHaveText(/req-9/); + }); + }); + + test.describe('SHORTCUT: Move Tab Right', () => { + test('Move Tab Right default (Cmd/Ctrl+])', async ({ pageWithUserData: page }) => { + await openRequest(page, collectionName, 'req-6', { persist: true }); + await openRequest(page, collectionName, 'req-7', { persist: true }); + await openRequest(page, collectionName, 'req-8', { persist: true }); + await openRequest(page, collectionName, 'req-9', { persist: true }); + + // Move req-9 to first position first + await pressShortcut(page, modifier, 'BracketLeft'); + await pressShortcut(page, modifier, 'BracketLeft'); + await pressShortcut(page, modifier, 'BracketLeft'); + await expect(page.locator('li.request-tab.active')).toHaveText(/req-9/); + const startIndex = await getTabIndex(page, 'req-9'); + expect(startIndex).toBeGreaterThanOrEqual(0); + + await pressShortcut(page, modifier, 'BracketRight'); + const indexAfterOneMove = await getTabIndex(page, 'req-9'); + expect(indexAfterOneMove).toBeGreaterThanOrEqual(startIndex); + + await pressShortcut(page, modifier, 'BracketRight'); + const indexAfterTwoMoves = await getTabIndex(page, 'req-9'); + expect(indexAfterTwoMoves).toBeGreaterThanOrEqual(indexAfterOneMove); + + await pressShortcut(page, modifier, 'BracketRight'); + const indexAfterThreeMoves = await getTabIndex(page, 'req-9'); + expect(indexAfterThreeMoves).toBeGreaterThanOrEqual(indexAfterTwoMoves); + }); + + test('Move Tab Right customized (Alt+R)', async ({ pageWithUserData: page }) => { + // Remap moveTabRight to Alt+R + await openKeybindingsTab(page); + const row = page.getByTestId('keybinding-row-moveTabRight'); + await row.hover(); + await page.getByTestId('keybinding-edit-moveTabRight').click(); + await expect(page.getByTestId('keybinding-input-moveTabRight')).toBeVisible({ timeout: 2000 }); + + await page.keyboard.press('Backspace'); + + await pressShortcut(page, 'Alt', 'KeyR'); + + await closePreferencesTab(page); + + await openRequest(page, collectionName, 'req-6', { persist: true }); + await openRequest(page, collectionName, 'req-7', { persist: true }); + await openRequest(page, collectionName, 'req-8', { persist: true }); + await openRequest(page, collectionName, 'req-9', { persist: true }); + + const req7Tab = page.locator('.request-tab').filter({ has: page.getByText('req-7', { exact: true }) }).first(); + await req7Tab.click(); + await expect(req7Tab).toHaveClass(/active/); + + const startIndex = await getTabIndex(page, 'req-7'); + expect(startIndex).toBeGreaterThanOrEqual(0); + + // Press Alt+L → req-9 moves right, req-8 becomes last + await pressShortcut(page, 'Alt', 'KeyR'); + + const indexAfterOneMove = await getTabIndex(page, 'req-7'); + expect(indexAfterOneMove).toBeGreaterThan(startIndex); + + await pressShortcut(page, 'Alt', 'KeyR'); + + const indexAfterTwoMoves = await getTabIndex(page, 'req-7'); + expect(indexAfterTwoMoves).toBeGreaterThanOrEqual(indexAfterOneMove); + + await pressShortcut(page, 'Alt', 'KeyR'); + + const indexAfterThreeMoves = await getTabIndex(page, 'req-7'); + expect(indexAfterThreeMoves).toBeGreaterThanOrEqual(indexAfterTwoMoves); + }); + }); + + test.describe('SHORTCUT: Switch to Tab at Position', () => { + test('Switch to Tab at Position default (Cmd/Ctrl+1-8)', async ({ pageWithUserData: page }) => { + // Open req-1..req-9 as persisted tabs (persist: true keeps each tab open). + for (let i = 1; i <= 9; i++) { + await openRequest(page, 'kb-collection', `req-${i}`, { persist: true }); + } + + // Cmd/Ctrl+ activates the tab at position n. + for (let pos = 1; pos <= 8; pos++) { + await pressShortcut(page, modifier, `${pos}`); + await expect(page.locator('li.request-tab.active')).toHaveText(new RegExp(`req-${pos}`), { timeout: 3000 }); + } + }); + }); + + test.describe('SHORTCUT: Reopen Last Closed Tab', () => { + test('Reopen Last Closed Tab default (Cmd/Ctrl+Shift+T)', async ({ pageWithUserData: page }) => { + await openRequest(page, collectionName, 'req-2', { persist: true }); + await openRequest(page, collectionName, 'req-1', { persist: true }); + const req1Tab = page.locator('.request-tab').filter({ has: page.getByText('req-1', { exact: true }) }).first(); + await req1Tab.click(); + await expect(req1Tab).toHaveClass(/active/); + await closeTabByName(page, 'req-1'); + + await reopenClosedTab(page, async () => { + await pressShortcut(page, modifier, 'Shift', 'KeyT'); + }, 'req-1'); + }); + + test('Reopen Last Closed Tab customized (Cmd/Ctrl+Shift+T)', async ({ pageWithUserData: page }) => { + await openRequest(page, collectionName, 'req-2', { persist: true }); + + // Open Collection-Settings tab (double-click collection name) + await page.getByTestId('sidebar-collection-row').filter({ has: page.getByText('kb-collection', { exact: true }) }).dblclick(); + await expect(page.locator('.request-tab').filter({ has: page.getByText('Collection', { exact: true }) })).toBeVisible({ timeout: 2000 }); + + // Open Runner tab + await page.getByTestId('runner').click(); + await expect(page.locator('.request-tab').filter({ has: page.getByText('Runner', { exact: true }) })).toBeVisible({ timeout: 2000 }); + + // Open Variables tab + await page.getByTestId('more-actions').click(); + await page.getByTestId('more-actions-variables').click(); + await expect(page.locator('.request-tab').filter({ has: page.getByText('Variables', { exact: true }) })).toBeVisible({ timeout: 2000 }); + + // Open Folder-Settings tab (create folder + double-click) + await page.locator('.collection-item-name').filter({ has: page.getByText('kb-folder', { exact: true }) }).dblclick(); + + // Close in order: kb-folder (first closed) → Collection → Variables → Runner (last closed) + await closeTabByName(page, 'kb-folder'); + await closeTabByName(page, 'Collection'); + await closeTabByName(page, 'Variables'); + await closeTabByName(page, 'Runner'); + + // Reopen LIFO: Runner was closed last → reopens first + await reopenClosedTab(page, async () => { + await pressShortcut(page, modifier, 'Shift', 'KeyT'); + }, 'Runner'); + await reopenClosedTab(page, async () => { + await pressShortcut(page, modifier, 'Shift', 'KeyT'); + }, /variables/i); + await reopenClosedTab(page, async () => { + await pressShortcut(page, modifier, 'Shift', 'KeyT'); + }, 'Collection'); + await reopenClosedTab(page, async () => { + await pressShortcut(page, modifier, 'Shift', 'KeyT'); + }, 'kb-folder'); + }); + + test('Reopen Last Closed Tab customized (Alt+Z)', async ({ pageWithUserData: page }) => { + await remapKeybinding(page, 'reopenLastClosedTab', 'Alt', 'KeyZ'); + + await openRequest(page, collectionName, 'req-2', { persist: true }); + await openRequest(page, collectionName, 'req-1', { persist: true }); + const req1Tab = page.locator('.request-tab').filter({ has: page.getByText('req-1', { exact: true }) }).first(); + await req1Tab.click(); + await expect(req1Tab).toHaveClass(/active/); + await closeTabByName(page, 'req-1'); + + await reopenClosedTab(page, async () => { + await pressShortcut(page, 'Alt', 'KeyZ'); + }, 'req-1'); + }); + }); + }); +}); diff --git a/tests/utils/page/actions.ts b/tests/utils/page/actions.ts index e9f4ab858..0d1cf0184 100644 --- a/tests/utils/page/actions.ts +++ b/tests/utils/page/actions.ts @@ -48,9 +48,15 @@ const closeAllCollections = async (page) => { for (let i = 0; i < numberOfCollections; i++) { const firstCollection = page.locator('[data-testid="collections"] .collection-name').first(); - await firstCollection.hover(); - await firstCollection.locator('.collection-actions .icon').click(); - await page.locator('.dropdown-item').getByText('Remove').click(); + await firstCollection.scrollIntoViewIfNeeded(); + + const removeMenuItem = page.locator('.dropdown-item').getByText('Remove'); + await expect(async () => { + await firstCollection.hover(); + await firstCollection.locator('.collection-actions .icon').click({ force: true }); + await expect(removeMenuItem).toBeVisible({ timeout: 2000 }); + }).toPass({ timeout: 15000 }); + await removeMenuItem.click(); // Wait for modal to appear - could be either regular remove or drafts confirmation const removeModal = page.locator('.bruno-modal').filter({ hasText: 'Remove Collection' }); @@ -150,10 +156,18 @@ const createCollection = async ( await expect(nameInput).toHaveValue(collectionName, { timeout: 2000 }); if (format) { - await createCollectionModal.locator('.advanced-options .btn-advanced').click(); - await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Show File Format' }).click(); + const advancedBtn = createCollectionModal.locator('.advanced-options .btn-advanced'); + const showFileFormatToggle = page.getByTestId('show-file-format-toggle'); const formatSelect = createCollectionModal.locator('#format'); - await formatSelect.waitFor({ state: 'visible', timeout: 5000 }); + + await expect(async () => { + if (!(await formatSelect.isVisible())) { + await advancedBtn.click(); + await showFileFormatToggle.click({ timeout: 2000 }); + } + await expect(formatSelect).toBeVisible({ timeout: 2000 }); + }).toPass({ timeout: 15000 }); + await formatSelect.selectOption(format); }