diff --git a/packages/bruno-app/jest.config.js b/packages/bruno-app/jest.config.js index c4e9b8ac6..31cac61da 100644 --- a/packages/bruno-app/jest.config.js +++ b/packages/bruno-app/jest.config.js @@ -1,8 +1,7 @@ module.exports = { rootDir: '.', transform: { - '^.+\\.[jt]sx?$': '/jest/transformers/babel-with-esm-replacements.cjs' - // '^.+\\.[jt]sx?$': [require("./jest/transformers/with-replacements.cjs"),'babel-jest'] + '^.+\\.[jt]sx?$': 'babel-jest' }, transformIgnorePatterns: [ '/node_modules/(?!strip-json-comments|nanoid|xml-formatter)/' diff --git a/packages/bruno-app/jest/transformers/babel-with-esm-replacements.cjs b/packages/bruno-app/jest/transformers/babel-with-esm-replacements.cjs deleted file mode 100644 index d4d8a12ff..000000000 --- a/packages/bruno-app/jest/transformers/babel-with-esm-replacements.cjs +++ /dev/null @@ -1,8 +0,0 @@ -const babelJest = require('babel-jest') - -module.exports = { - process(sourceText, sourcePath, options) { - const transformer = babelJest.createTransformer(); - return transformer.process(sourceText.replace(`import.meta.env.MODE`, 'test'), sourcePath, options) - } -}; diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index 323be9a3c..98593967f 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -16,7 +16,6 @@ import stripJsonComments from 'strip-json-comments'; import { getAllVariables } from 'utils/collections'; import { setupLinkAware } from 'utils/codemirror/linkAware'; import { setupLintErrorTooltip } from 'utils/codemirror/lint-errors'; -import { setupShortcuts } from 'utils/codemirror/shortcuts'; import CodeMirrorSearch from 'components/CodeMirrorSearch/index'; const CodeMirror = require('codemirror'); @@ -47,9 +46,6 @@ export default class CodeEditor extends React.Component { this.state = { searchBarVisible: false }; - - // Shortcuts cleanup function - this._shortcutsCleanup = null; } componentDidMount() { @@ -221,9 +217,6 @@ export default class CodeEditor extends React.Component { // Setup lint error tooltip on line number hover this.cleanupLintErrorTooltip = setupLintErrorTooltip(editor); - - // Setup keyboard shortcuts - this._shortcutsCleanup = setupShortcuts(editor, this); } } @@ -295,12 +288,6 @@ export default class CodeEditor extends React.Component { } componentWillUnmount() { - // Cleanup shortcuts (keymap and store subscription) - if (this._shortcutsCleanup) { - this._shortcutsCleanup(); - this._shortcutsCleanup = null; - } - if (this.editor) { if (this.props.onScroll) { this.props.onScroll(this.editor); diff --git a/packages/bruno-app/src/components/MultiLineEditor/index.js b/packages/bruno-app/src/components/MultiLineEditor/index.js index 56e57d48c..25f1cf52f 100644 --- a/packages/bruno-app/src/components/MultiLineEditor/index.js +++ b/packages/bruno-app/src/components/MultiLineEditor/index.js @@ -6,7 +6,6 @@ import { setupAutoComplete } from 'utils/codemirror/autocomplete'; import { MaskedEditor } from 'utils/common/masked-editor'; import StyledWrapper from './StyledWrapper'; import { setupLinkAware } from 'utils/codemirror/linkAware'; -import { setupShortcuts } from 'utils/codemirror/shortcuts'; import { IconEye, IconEyeOff } from '@tabler/icons'; const CodeMirror = require('codemirror'); @@ -25,9 +24,6 @@ class MultiLineEditor extends Component { this.state = { maskInput: props.isSecret || false // Always mask the input by default (if it's a secret) }; - - // Shortcuts cleanup function - this._shortcutsCleanup = null; } componentDidMount() { @@ -49,16 +45,16 @@ class MultiLineEditor extends Component { readOnly: this.props.readOnly, tabindex: 0, extraKeys: { - // 'Ctrl-Enter': () => { - // if (this.props.onRun) { - // this.props.onRun(); - // } - // }, - // 'Cmd-Enter': () => { - // if (this.props.onRun) { - // this.props.onRun(); - // } - // }, + 'Ctrl-Enter': () => { + if (this.props.onRun) { + this.props.onRun(); + } + }, + 'Cmd-Enter': () => { + if (this.props.onRun) { + this.props.onRun(); + } + }, 'Cmd-S': () => { if (this.props.onSave) { this.props.onSave(); @@ -94,9 +90,6 @@ class MultiLineEditor extends Component { setupLinkAware(this.editor); - // Setup keyboard shortcuts - this._shortcutsCleanup = setupShortcuts(this.editor, this); - this.editor.setValue(String(this.props.value) || ''); this.editor.on('change', this._onEdit); this.addOverlay(variables); @@ -186,12 +179,6 @@ class MultiLineEditor extends Component { } componentWillUnmount() { - // Cleanup shortcuts (keymap and store subscription) - if (this._shortcutsCleanup) { - this._shortcutsCleanup(); - this._shortcutsCleanup = null; - } - if (this.brunoAutoCompleteCleanup) { this.brunoAutoCompleteCleanup(); } diff --git a/packages/bruno-app/src/components/Preferences/Keybindings/StyledWrapper.js b/packages/bruno-app/src/components/Preferences/Keybindings/StyledWrapper.js index 5b7ffcffc..01a80ba78 100644 --- a/packages/bruno-app/src/components/Preferences/Keybindings/StyledWrapper.js +++ b/packages/bruno-app/src/components/Preferences/Keybindings/StyledWrapper.js @@ -1,199 +1,53 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` - min-height: 0; - max-height: calc(100% - 30px); - display: flex; flex-direction: column; + gap: 1rem; + width: 100%; + + table { + width: 80%; + border-collapse: collapse; - &::-webkit-scrollbar { - display: none; - } - scrollbar-width: none; - -ms-overflow-style: none; - - .section-header { - display: flex; - align-items: center; - justify-content: space-between; - } - - .reset-all-btn { - display: flex; - align-items: center; - background: transparent; - border: 1px solid ${(props) => props.theme.table.border}; - border-radius: 6px; - padding: 4px 4px; - cursor: pointer; - color: ${(props) => props.theme.text}; - font-size: 12px; - font-weight: 500; - transition: all 0.2s ease; - - &:hover { - background: ${(props) => props.theme.button.secondary.hoverBg}; - border-color: ${(props) => props.theme.button.secondary.hoverBorder}; - } - } - - .keybinding-row { - display: flex; - align-items: center; - gap: 10px; - } - - .keybinding-row .edit-btn, - .keybinding-row .reset-btn { - flex-shrink: 0; - } - - .button-placeholder { - width: 20px; - height: 20px; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - } - - .keybinding-row:hover .edit-btn { - opacity: 0.9; - } - - .shortcut-wrap { - display: inline-flex; - align-items: center; - gap: 8px; - min-width: 260px; - flex: 1; - } - - .shortcut-input { - width: 200px; - max-width: 200px; - flex-shrink: 0; - - caret-color: ${(props) => props.theme.table.input.color}; - - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - border: none; - outline: none; - background: transparent; - - font-family: monospace; - color: ${(props) => props.theme.table.input.color}; - cursor: pointer; - - &:hover { - opacity: 0.85; + thead, + td { + border: 1px solid ${(props) => props.theme.table.border}; } - &:focus { - opacity: 1; + thead { + color: ${(props) => props.theme.table.thead.color}; + font-size: ${(props) => props.theme.font.size.base}; + user-select: none; } - &::placeholder { - opacity: 0.5; + td { + padding: 6px 10px; + font-size: ${(props) => props.theme.font.size.sm}; } - } - .edit-btn { - background: transparent; - border: none; - color: ${(props) => props.theme.table.thead.color}; - padding: 0; - cursor: pointer; - opacity: 0.6; - - &:hover { - opacity: 1; + thead th { + font-weight: 500; + padding: 10px; + text-align: left; + border: 1px solid ${(props) => props.theme.table.border}; } } - .reset-btn { - background: transparent; - border: none; - color: ${(props) => props.theme.table.thead.color}; - border-radius: 8px; - padding: 0px; - cursor: pointer; - user-select: none; - white-space: nowrap; - } - - .shortcut-input--error { - opacity: 1; - } - - .tooltip-mod.tooltip-mod--error{ - color: ${(props) => props.theme.status.danger.text} !important; - } - .table-container { - margin-bottom: 24px; - min-height: 0; overflow-y: auto; - - border-radius: 8px; - border-top: 1px solid ${(props) => props.theme.table.border}; - border-bottom: 1px solid ${(props) => props.theme.table.border}; - - &::-webkit-scrollbar { - width: 0; - height: 0; - } - scrollbar-width: none; - -ms-overflow-style: none; } - table { - width: 100%; - border-collapse: separate; - border-spacing: 0; - table-layout: fixed; - } - - thead th:first-child, - tbody td:first-child { - width: 35%; - } - - thead th:last-child, - tbody td:last-child { - width: 45%; - } - - thead th { - position: sticky; - top: 0; - z-index: 5; - - background: ${(props) => props.theme.background.base}; - - color: ${(props) => props.theme.table.thead.color}; - font-size: ${(props) => props.theme.font.size.base}; - user-select: none; - font-weight: 500; - padding: 10px; - text-align: left; - - border-left: 1px solid ${(props) => props.theme.table.border}; - border-right: 1px solid ${(props) => props.theme.table.border}; - border-bottom: 1px solid ${(props) => props.theme.table.border}; - box-shadow: 0 1px 0 ${(props) => props.theme.table.border}; - } - - td { - padding: 6px 10px; - font-size: ${(props) => props.theme.font.size.sm}; - border-top: 1px solid ${(props) => props.theme.table.border}; - border-left: 1px solid ${(props) => props.theme.table.border}; - border-right: 1px solid ${(props) => props.theme.table.border}; + .key-button { + display: inline-block; + color: ${(props) => props.theme.table.input.color}; + opacity: 0.7; + border-radius: 4px; + padding: 1px 5px; + font-family: monospace; + margin-right: 8px; + border: 1px solid #ccc; + border-bottom: 1.44px solid ${(props) => props.theme.table.input.border}; } `; diff --git a/packages/bruno-app/src/components/Preferences/Keybindings/index.js b/packages/bruno-app/src/components/Preferences/Keybindings/index.js index eeadda98e..599e6655c 100644 --- a/packages/bruno-app/src/components/Preferences/Keybindings/index.js +++ b/packages/bruno-app/src/components/Preferences/Keybindings/index.js @@ -1,516 +1,14 @@ -import React, { useMemo, useRef, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import toast from 'react-hot-toast'; - import StyledWrapper from './StyledWrapper'; -import { IconReload, IconPencil } from '@tabler/icons'; +import React from 'react'; +import { getKeyBindingsForOS } from 'providers/Hotkeys/keyMappings'; import { isMacOS } from 'utils/common/platform'; -import { savePreferences } from 'providers/ReduxStore/slices/app'; -import { DEFAULT_KEY_BINDINGS } from 'providers/Hotkeys/keyMappings.js'; -import { Tooltip } from 'react-tooltip'; - -const SEP = '+bind+'; -const getOS = () => (isMacOS() ? 'mac' : 'windows'); - -// Stored tokens must match your preferences defaults (lowercase) -const MODIFIERS = new Set(['ctrl', 'command', 'alt', 'shift']); - -const REQUIRED_MODIFIERS_BY_OS = { - mac: new Set(['command', 'alt', 'shift', 'ctrl']), - windows: new Set(['ctrl', 'alt', 'shift']) // command (Win key) should NOT count -}; - -const hasRequiredModifier = (os, arr) => arr.some((k) => REQUIRED_MODIFIERS_BY_OS[os]?.has(k)); -const isOnlyModifiers = (arr) => arr.length > 0 && arr.every((k) => MODIFIERS.has(k)); - -const sortCombo = (arr) => { - const order = ['ctrl', 'command', 'alt', 'shift']; - const modifiers = []; - const nonModifiers = []; - - // Separate modifiers from non-modifiers - arr.forEach((key) => { - if (order.includes(key)) { - modifiers.push(key); - } else { - nonModifiers.push(key); - } - }); - - // Sort modifiers by their order - modifiers.sort((a, b) => order.indexOf(a) - order.indexOf(b)); - - // Keep non-modifiers in the order they were pressed (don't sort them) - return [...modifiers, ...nonModifiers]; -}; - -const uniqSorted = (arr) => { - // Remove duplicates while preserving order - const unique = []; - const seen = new Set(); - arr.forEach((key) => { - if (!seen.has(key)) { - seen.add(key); - unique.push(key); - } - }); - return sortCombo(unique); -}; - -const fromKeysString = (keysStr) => (keysStr ? keysStr.split(SEP).filter(Boolean) : []); -const toKeysString = (keysArr) => uniqSorted(keysArr).join(SEP); - -// Signature MUST be stable: unique + sorted -const comboSignature = (arr) => toKeysString(arr); - -// OS reserved shortcuts in stored-token format -const RESERVED_BY_OS = { - mac: new Set([ - comboSignature(['command', 'q']), - comboSignature(['command', 'w']), - comboSignature(['command', 'h']), - comboSignature(['command', 'm']), - comboSignature(['command', 'tab']), - comboSignature(['command', 'space']), - comboSignature(['ctrl', 'command', 'q']), - comboSignature(['command', ',']), - comboSignature(['command', 'shift', '3']), - comboSignature(['command', 'shift', '4']), - comboSignature(['command', 'shift', '5']), - comboSignature(['command', 'alt', 'esc']) - ]), - windows: new Set([ - comboSignature(['alt', 'tab']), - comboSignature(['alt', 'f4']), - comboSignature(['ctrl', 'alt', 'delete']), - comboSignature(['command', 'l']), - comboSignature(['command', 'd']), - comboSignature(['command', 'e']), - comboSignature(['command', 'r']), - comboSignature(['command', 'tab']), - comboSignature(['ctrl', 'shift', 'esc']) - ]) -}; - -// normalize keyboard event -> stored tokens -const normalizeKey = (e) => { - const k = e.key; - - // ignore lock keys - if (k === 'CapsLock' || k === 'NumLock' || k === 'ScrollLock') return null; - - if (k === ' ') return 'space'; - if (k === 'Escape') return 'esc'; - if (k === 'Control') return 'ctrl'; - if (k === 'Alt') return 'alt'; - if (k === 'Shift') return 'shift'; - if (k === 'Enter') return 'enter'; - if (k === 'Backspace') return 'backspace'; - if (k === 'Tab') return 'tab'; - if (k === 'Delete') return 'delete'; - - // Meta -> command (matches your stored default format) - if (k === 'Meta') return 'command'; - - // single char (letters/punct) to lowercase - if (k.length === 1) return k.toLowerCase(); - - // ArrowUp -> arrowup, PageUp -> pageup, etc - return k.toLowerCase(); -}; - -const ERROR = { - EMPTY: 'EMPTY', - ONLY_MODIFIERS: 'ONLY_MODIFIERS', - MISSING_REQUIRED_MOD: 'MISSING_REQUIRED_MOD', - RESERVED: 'RESERVED', - DUPLICATE: 'DUPLICATE', - CONFLICT: 'CONFLICT' -}; - -const Keybindings = () => { - const dispatch = useDispatch(); - const preferences = useSelector((state) => state.app.preferences); - - const os = getOS(); - - // Source of truth: merge defaults with user preferences - const keyBindings = useMemo(() => { - const merged = {}; - - // Start with defaults - for (const [action, binding] of Object.entries(DEFAULT_KEY_BINDINGS)) { - merged[action] = { ...binding }; - } - - // Override with user preferences - const userBindings = preferences?.keyBindings || {}; - for (const [action, binding] of Object.entries(userBindings)) { - if (merged[action]) { - // Merge user's OS-specific overrides into defaults - merged[action] = { - ...merged[action], - ...binding - }; - } - } - - return merged; - }, [preferences?.keyBindings]); - - // Build table data (action -> { name, keys }) - const keyMapping = useMemo(() => { - const out = {}; - for (const [action, binding] of Object.entries(keyBindings)) { - if (binding?.[os]) out[action] = { name: binding.name, keys: binding[os] }; - } - return out; - }, [keyBindings, os]); - - // ✏️ which row is allowed to edit (pencil clicked) - const [editingAction, setEditingAction] = useState(null); - - // hover tracking (for showing pencil/reset only on hover row) - const [hoveredAction, setHoveredAction] = useState(null); - - // Recording state - const [recordingAction, setRecordingAction] = useState(null); - const pressedKeysRef = useRef(new Set()); - const inputRefs = useRef({}); - const [draftByAction, setDraftByAction] = useState({}); // action -> string[] - const [errorByAction, setErrorByAction] = useState({}); // action -> { code, message } - - const getCurrentRowKeysString = (action) => keyBindings?.[action]?.[os] || ''; - const getDefaultRowKeysString = (action) => DEFAULT_KEY_BINDINGS?.[action]?.[os] || ''; - - const isRowDirty = (action) => { - const current = getCurrentRowKeysString(action); - const def = getDefaultRowKeysString(action); - if (!DEFAULT_KEY_BINDINGS) return false; - return current !== def; - }; - - // Check if any keybinding is dirty (different from default) - const hasDirtyRows = useMemo(() => { - for (const action of Object.keys(DEFAULT_KEY_BINDINGS)) { - if (isRowDirty(action)) { - return true; - } - } - return false; - }, [keyBindings, os]); - - const buildUsedSignatures = (excludeAction) => { - const used = new Set(); - for (const [action, binding] of Object.entries(keyBindings)) { - if (action === excludeAction) continue; - const keysStr = binding?.[os]; - if (!keysStr) continue; - used.add(comboSignature(fromKeysString(keysStr))); - } - return used; - }; - - const validateCombo = (action, arrRaw) => { - const arr = uniqSorted(arrRaw); - const sig = comboSignature(arr); - - if (!sig) return { code: ERROR.EMPTY, message: `Shortcut can’t be empty.` }; - if (isOnlyModifiers(arr)) - return { code: ERROR.ONLY_MODIFIERS, message: 'Add a non-modifier key (e.g. Ctrl + K).' }; - - // OS-specific must-have modifier rule - if (!hasRequiredModifier(os, arr)) { - return { - code: ERROR.MISSING_REQUIRED_MOD, - message: - os === 'mac' - ? 'macOS shortcuts must include at least one modifier (command/alt/shift/ctrl).' - : 'Windows shortcuts must include at least one modifier (ctrl/alt/shift).' - }; - } - - // OS reserved - if (RESERVED_BY_OS[os]?.has(sig)) - return { code: ERROR.RESERVED, message: 'This shortcut is reserved by the OS.' }; - - // No duplicates (across all other actions) - if (buildUsedSignatures(action).has(sig)) - return { code: ERROR.DUPLICATE, message: 'That shortcut is already in use.' }; - - // Check for subset conflicts (e.g., Cmd+A conflicts with Cmd+Z+A) - for (const [otherAction, binding] of Object.entries(keyBindings)) { - if (otherAction === action) continue; - const otherKeysStr = binding?.[os]; - if (!otherKeysStr) continue; - - const otherKeys = fromKeysString(otherKeysStr); - - // Check if current is a subset of other (current is shorter) - if (arr.length < otherKeys.length) { - const isSubset = arr.every((k) => otherKeys.includes(k)); - if (isSubset) { - return { - code: ERROR.CONFLICT, - message: `Conflicts with "${binding.name}" (${otherKeys.join(' + ')}). Remove the longer shortcut first.` - }; - } - } - - // Check if other is a subset of current (current is longer) - if (arr.length > otherKeys.length) { - const isSubset = otherKeys.every((k) => arr.includes(k)); - if (isSubset) { - return { - code: ERROR.CONFLICT, - message: `Conflicts with "${binding.name}" (${otherKeys.join(' + ')}). Remove that shortcut first.` - }; - } - } - } - - return null; - }; - - const persistToPreferences = (action, nextKeys) => { - const updatedPreferences = { - ...preferences, - keyBindings: { - ...(preferences?.keyBindings || {}), - [action]: { - ...(preferences?.keyBindings?.[action] || {}), - name: preferences?.keyBindings?.[action]?.name || DEFAULT_KEY_BINDINGS?.[action]?.name || action, - [os]: nextKeys - } - } - }; - - dispatch(savePreferences(updatedPreferences)); - }; - - // Commit only if valid. Returns true if commit succeeded (or no-op), false if invalid. - const commitCombo = (action) => { - const draftArr = draftByAction[action] || []; - if (!draftArr.length) return; - - const arr = uniqSorted(draftArr); - const err = validateCombo(action, arr); - - if (err) { - setErrorByAction((prev) => ({ ...prev, [action]: err })); - return false; - } - - setErrorByAction((prev) => { - const next = { ...prev }; - delete next[action]; - return next; - }); - - const nextKeys = toKeysString(arr); - const currentKeys = getCurrentRowKeysString(action); - if (nextKeys === currentKeys) return true; - - persistToPreferences(action, nextKeys); - // toast success for 2s with Command name - const commandName = keyBindings?.[action]?.name || action; - toast.success(`"${commandName}" shortcut updated`, { autoClose: 2000 }); - return true; - }; - - const resetRowToDefault = (action) => { - const def = DEFAULT_KEY_BINDINGS?.[action]?.[os]; - if (!def) return; - - setErrorByAction((prev) => { - const next = { ...prev }; - delete next[action]; - return next; - }); - - setDraftByAction((prev) => { - const next = { ...prev }; - delete next[action]; - return next; - }); - - persistToPreferences(action, def); - }; - - const resetAllKeybindings = () => { - const updatedPreferences = { - ...preferences, - keyBindings: {} - }; - dispatch(savePreferences(updatedPreferences)); - }; - - const startEditing = (action) => { - // if another row is editing, commit/stop it first - if (editingAction && editingAction !== action) { - const ok = commitCombo(editingAction); - if (ok) { - setRecordingAction(null); - setEditingAction(null); - pressedKeysRef.current = new Set(); - } else { - // keep previous row editing if invalid - return; - } - } - - setEditingAction(action); - setRecordingAction(action); - pressedKeysRef.current = new Set(); - - // seed draft with current value - setDraftByAction((prev) => ({ - ...prev, - [action]: fromKeysString(getCurrentRowKeysString(action)) - })); - - // clear error on start edit - setErrorByAction((prev) => { - const next = { ...prev }; - delete next[action]; - return next; - }); - - requestAnimationFrame(() => { - inputRefs.current[action]?.focus?.(); - inputRefs.current[action]?.setSelectionRange?.( - inputRefs.current[action].value.length, - inputRefs.current[action].value.length - ); - }); - }; - - const stopEditing = (action) => { - const ok = commitCombo(action); - if (!ok) { - // If commit failed (validation error), reset to original value - cancelEditing(action); - return; - } - - setRecordingAction(null); - setEditingAction(null); - pressedKeysRef.current = new Set(); - }; - - // Reset draft to original value and clear error (used on blur with invalid state) - const cancelEditing = (action) => { - // Clear error for this action - setErrorByAction((prev) => { - const next = { ...prev }; - delete next[action]; - return next; - }); - - // Reset draft to current saved value - setDraftByAction((prev) => { - const next = { ...prev }; - delete next[action]; - return next; - }); - - setRecordingAction(null); - setEditingAction(null); - pressedKeysRef.current = new Set(); - }; - - const handleKeyDown = (action, e) => { - if (recordingAction !== action || editingAction !== action) return; - - e.preventDefault(); - e.stopPropagation(); - - // allow user to clear and keep editing (do NOT auto-stop) - if (e.key === 'Backspace' || e.key === 'Delete') { - pressedKeysRef.current = new Set(); - setDraftByAction((prev) => ({ ...prev, [action]: [] })); - setErrorByAction((prev) => ({ - ...prev, - [action]: { code: ERROR.EMPTY, message: `Shortcut can't be empty.` } - })); - return; - } - - if (e.repeat) return; - - const keyName = normalizeKey(e); - if (!keyName) return; - - pressedKeysRef.current.add(keyName); - - const currentDraft = uniqSorted(Array.from(pressedKeysRef.current)); - - setDraftByAction((prev) => ({ - ...prev, - [action]: currentDraft - })); - - const err = validateCombo(action, currentDraft); - if (err) { - setErrorByAction((prev) => ({ ...prev, [action]: err })); - } else { - setErrorByAction((prev) => { - const next = { ...prev }; - delete next[action]; - return next; - }); - } - }; - - const handleKeyUp = (action, e) => { - if (recordingAction !== action || editingAction !== action) return; - - e.preventDefault(); - e.stopPropagation(); - - const keyName = normalizeKey(e); - if (!keyName) return; - - pressedKeysRef.current.delete(keyName); - - // commit only when released AND currently valid - if (pressedKeysRef.current.size === 0) { - const currentDraft = draftByAction[action] || []; - - // if empty -> keep editing - if (currentDraft.length === 0) return; - - // if error -> keep editing - if (errorByAction[action]?.message) return; - - stopEditing(action); - } - }; - - const renderValue = (action) => { - const arr - = recordingAction === action ? draftByAction[action] : fromKeysString(getCurrentRowKeysString(action)); - - return (arr || []).join(' + '); - }; +const Keybindings = ({ close }) => { + const keyMapping = getKeyBindingsForOS(isMacOS() ? 'mac' : 'windows'); return ( -
- Keybindings - {hasDirtyRows && ( - - )} -
- +
Keybindings
@@ -521,89 +19,18 @@ const Keybindings = () => { {keyMapping ? ( - Object.entries(keyMapping).map(([action, row]) => { - const isEditing = editingAction === action; - const isHovered = hoveredAction === action; - const isDirty = isRowDirty(action); - - const showPencil = isHovered && !isEditing && !isDirty; - const showReset = isDirty && !isEditing; - const hasError = Boolean(errorByAction[action]?.message); - const errorMessage = errorByAction[action]?.message; - const inputId = `kb-input-${action}`; - - return ( - setHoveredAction(action)} - onMouseLeave={() => setHoveredAction((prev) => (prev === action ? null : prev))} - > - - + + - - ); - }) + ))} + + + )) ) : ( diff --git a/packages/bruno-app/src/components/Preferences/StyledWrapper.js b/packages/bruno-app/src/components/Preferences/StyledWrapper.js index d25406240..bde16f70b 100644 --- a/packages/bruno-app/src/components/Preferences/StyledWrapper.js +++ b/packages/bruno-app/src/components/Preferences/StyledWrapper.js @@ -3,7 +3,7 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` div.tabs { padding: 12px; - min-width: 180px; + min-width: 160px; div.tab { display: flex; @@ -38,7 +38,7 @@ const StyledWrapper = styled.div` } section.tab-panel { - max-height: calc(100% - 24px); + min-height: 70vh; overflow-y: auto; flex-grow: 1; padding: 12px; diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js index 6f109f22f..6c1a3644d 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js @@ -36,10 +36,6 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi const [showConfirmEnvironmentClose, setShowConfirmEnvironmentClose] = useState(false); const [showConfirmGlobalEnvironmentClose, setShowConfirmGlobalEnvironmentClose] = useState(false); - const tabs = useSelector((state) => state.tabs.tabs); - const activeTabUid = useSelector((state) => state.tabs.activeTabUid); - const activeTab = tabs.find((t) => t.uid === activeTabUid); - const menuDropdownRef = useRef(); const item = findItemInCollection(collection, tab.uid); @@ -90,62 +86,6 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi }; }, [item, item?.name, method, setHasOverflow]); - useEffect(() => { - const handleCloseTabFromHotkeys = () => { - if (!activeTabUid || !activeTab) return; - - // Only the active tab component should handle this - if (tab.uid !== activeTabUid) return; - - // Always compute item for the active tab - const activeItem = findItemInCollection(collection, activeTabUid); - - switch (activeTab.type) { - case 'request': - if (activeItem && hasRequestChanges(activeItem)) { - console.log('Item have changes'); - setShowConfirmClose(true); - } else { - console.log('Item dont have changes'); - dispatch(closeTabs({ tabUids: [activeTabUid] })); - } - break; - - case 'collection-settings': - if (collection?.draft) { - setShowConfirmCollectionClose(true); - } else { - dispatch(closeTabs({ tabUids: [activeTabUid] })); - } - break; - - case 'folder-settings': { - const folderItem = findItemInCollection(collection, activeTab.folderUid || tab.folderUid); - if (folderItem?.draft) { - setShowConfirmFolderClose(true); - } else { - dispatch(closeTabs({ tabUids: [activeTabUid] })); - } - break; - } - - case 'environment-settings': - if (collection?.environmentsDraft) { - setShowConfirmEnvironmentClose(true); - } else { - dispatch(closeTabs({ tabUids: [activeTabUid] })); - } - break; - - default: - break; - } - }; - - window.addEventListener('close-active-tab', handleCloseTabFromHotkeys); - return () => window.removeEventListener('close-active-tab', handleCloseTabFromHotkeys); - }, [dispatch, activeTab, activeTabUid, tab.uid, collection]); - const handleCloseClick = (event) => { event.stopPropagation(); event.preventDefault(); diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js index 427042fb4..5d0a537da 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js @@ -203,7 +203,7 @@ const CloneCollectionItem = ({ collectionUid, item, onClose }) => { - 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 abd876638..e4160593d 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 @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect, useCallback } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { getEmptyImage } from 'react-dnd-html5-backend'; import range from 'lodash/range'; import filter from 'lodash/filter'; @@ -69,21 +69,12 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) const isSidebarDragging = useSelector((state) => state.app.isDragging); const collection = useSelector((state) => state.collections.collections?.find((c) => c.uid === collectionUid)); const { hasCopiedItems } = useSelector((state) => state.app.clipboard); - const preferences = useSelector((state) => state.app.preferences); - const userKeyBindings = preferences?.keyBindings || {}; - const hasCustomCopyBinding = !!userKeyBindings?.copyItem; - const hasCustomPasteBinding = !!userKeyBindings?.pasteItem; - const hasCustomRenameBinding = !!userKeyBindings?.renameItem; const dispatch = useDispatch(); // We use a single ref for drag and drop. const ref = useRef(null); const menuDropdownRef = useRef(null); - // Refs to store current handler references for event listeners (avoid stale closures) - const copyHandlerRef = useRef(null); - const pasteHandlerRef = useRef(null); - const [renameItemModalOpen, setRenameItemModalOpen] = useState(false); const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false); const [deleteItemModalOpen, setDeleteItemModalOpen] = useState(false); @@ -130,52 +121,6 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) } }, [isTabForItemActive]); - // Listen for clone-item-open event from Hotkeys provider - const isFocusedRef = useRef(isKeyboardFocused); - isFocusedRef.current = isKeyboardFocused; - - useEffect(() => { - const handleCloneItemOpen = () => { - // Only open modal if this item is keyboard focused - if (isFocusedRef.current) { - setCloneItemModalOpen(true); - } - }; - - const handleCopyItemOpen = () => { - // Copy item to clipboard if this item is keyboard focused - if (isFocusedRef.current && copyHandlerRef.current) { - copyHandlerRef.current(); - } - }; - - const handlePasteItemOpen = () => { - // Paste item from clipboard if this item is keyboard focused - if (isFocusedRef.current && pasteHandlerRef.current) { - pasteHandlerRef.current(); - } - }; - - const handleRenameItemOpen = () => { - // Rename item if this item is keyboard focused - if (isFocusedRef.current) { - setRenameItemModalOpen(true); - } - }; - - window.addEventListener('clone-item-open', handleCloneItemOpen); - window.addEventListener('copy-item-open', handleCopyItemOpen); - window.addEventListener('paste-item-open', handlePasteItemOpen); - window.addEventListener('rename-item-open', handleRenameItemOpen); - - return () => { - window.removeEventListener('clone-item-open', handleCloneItemOpen); - window.removeEventListener('copy-item-open', handleCopyItemOpen); - window.removeEventListener('paste-item-open', handlePasteItemOpen); - window.removeEventListener('rename-item-open', handleRenameItemOpen); - }; - }, []); - const determineDropType = (monitor) => { const hoverBoundingRect = ref.current?.getBoundingClientRect(); const clientOffset = monitor.getClientOffset(); @@ -483,33 +428,6 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) return items; }; - const handleCopyItem = useCallback(() => { - dispatch(copyRequest(item)); - const itemType = isFolder ? 'Folder' : 'Request'; - toast.success(`${itemType} copied`); - }, [dispatch, item, isFolder]); - - const handlePasteItem = useCallback(() => { - // Determine target folder: if item is a folder, paste into it; otherwise paste into parent folder - let targetFolderUid = item.uid; - if (!isFolder) { - const parentFolder = findParentItemInCollection(collection, item.uid); - targetFolderUid = parentFolder ? parentFolder.uid : null; - } - - dispatch(pasteItem(collectionUid, targetFolderUid)) - .then(() => { - toast.success('Item pasted successfully'); - }) - .catch((err) => { - toast.error(err ? err.message : 'An error occurred while pasting the item'); - }); - }, [dispatch, collection, item, isFolder, collectionUid]); - - // Update refs whenever handlers change - copyHandlerRef.current = handleCopyItem; - pasteHandlerRef.current = handlePasteItem; - const className = classnames('flex flex-col w-full', { 'is-sidebar-dragging': isSidebarDragging }); @@ -619,25 +537,52 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) } }; + const handleCopyItem = () => { + dispatch(copyRequest(item)); + const itemType = isFolder ? 'Folder' : 'Request'; + toast.success(`${itemType} copied`); + }; + + const handlePasteItem = () => { + // Determine target folder: if item is a folder, paste into it; otherwise paste into parent folder + let targetFolderUid = item.uid; + if (!isFolder) { + const parentFolder = findParentItemInCollection(collection, item.uid); + targetFolderUid = parentFolder ? parentFolder.uid : null; + } + + dispatch(pasteItem(collectionUid, targetFolderUid)) + .then(() => { + toast.success('Item pasted successfully'); + }) + .catch((err) => { + toast.error(err ? err.message : 'An error occurred while pasting the item'); + }); + }; + // Keyboard shortcuts handler const handleKeyDown = (e) => { // Detect Mac by checking both metaKey and platform const isMac = navigator.userAgent?.includes('Mac') || navigator.platform?.startsWith('Mac'); const isModifierPressed = isMac ? e.metaKey : e.ctrlKey; - // Only use default handler if no custom keybinding is set for copy/paste - if (!hasCustomCopyBinding && isModifierPressed && e.key.toLowerCase() === 'c') { - e.preventDefault(); - e.stopPropagation(); - if (copyHandlerRef.current) copyHandlerRef.current(); - } else if (!hasCustomPasteBinding && isModifierPressed && e.key.toLowerCase() === 'v') { - e.preventDefault(); - e.stopPropagation(); - if (pasteHandlerRef.current) pasteHandlerRef.current(); - } else if (!hasCustomRenameBinding && e.key === 'F2') { + const [macRenameKey, winRenameKey] = getKeyBindingsForActionAllOS('renameItem'); + const renameKey = isMac ? macRenameKey : winRenameKey; + + // Only trigger rename if no modifier keys are pressed (allow Cmd+Enter for run request) + const hasModifier = e.metaKey || e.ctrlKey || e.shiftKey || e.altKey; + if (e.key.toLowerCase() === renameKey && !hasModifier) { e.preventDefault(); e.stopPropagation(); setRenameItemModalOpen(true); + } else if (isModifierPressed && e.key.toLowerCase() === 'c') { + e.preventDefault(); + e.stopPropagation(); + handleCopyItem(); + } else if (isModifierPressed && e.key.toLowerCase() === 'v') { + e.preventDefault(); + e.stopPropagation(); + handlePasteItem(); } }; 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 3a58b05d3..03392e170 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -278,34 +278,6 @@ const Collection = ({ collection, searchText }) => { } }, [isCollectionFocused]); - // Listen for clone-item-open event from Hotkeys provider - const isFocusedRef = useRef(isKeyboardFocused); - isFocusedRef.current = isKeyboardFocused; - - useEffect(() => { - const handleCloneItemOpen = () => { - // Only open modal if this collection is keyboard focused - if (isFocusedRef.current) { - setShowCloneCollectionModalOpen(true); - } - }; - - const handleRenameCollectionOpen = () => { - // Only open rename collection modal if this collection is keyboard focused - if (isFocusedRef.current) { - setShowRenameCollectionModal(true); - } - }; - - window.addEventListener('clone-item-open', handleCloneItemOpen); - window.addEventListener('rename-item-open', handleRenameCollectionOpen); - - return () => { - window.removeEventListener('clone-item-open', handleCloneItemOpen); - window.removeEventListener('rename-item-open', handleRenameCollectionOpen); - }; - }, []); - // Debounce showing empty state to prevent flicker // Race condition: isLoading can become false before items batch arrives from IPC useEffect(() => { diff --git a/packages/bruno-app/src/components/Sidebar/NewFolder/index.js b/packages/bruno-app/src/components/Sidebar/NewFolder/index.js index 00c868f41..ec6a36aa3 100644 --- a/packages/bruno-app/src/components/Sidebar/NewFolder/index.js +++ b/packages/bruno-app/src/components/Sidebar/NewFolder/index.js @@ -104,7 +104,6 @@ const NewFolder = ({ collectionUid, item, onClose }) => { formik.setFieldValue('folderName', e.target.value); !isEditing && formik.setFieldValue('directoryName', sanitizeName(e.target.value)); }} - data-testid="new-folder-input" value={formik.values.folderName || ''} /> {formik.touched.folderName && formik.errors.folderName ? ( diff --git a/packages/bruno-app/src/components/Sidebar/Sections/CollectionsSection/index.js b/packages/bruno-app/src/components/Sidebar/Sections/CollectionsSection/index.js index c04f41fa5..8e1544dcf 100644 --- a/packages/bruno-app/src/components/Sidebar/Sections/CollectionsSection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Sections/CollectionsSection/index.js @@ -1,5 +1,4 @@ -import { useState, useMemo, useEffect } from 'react'; -import { setIsCreatingCollection } from 'providers/ReduxStore/slices/app'; +import { useState, useMemo } from 'react'; import toast from 'react-hot-toast'; import get from 'lodash/get'; import { useDispatch, useSelector } from 'react-redux'; @@ -19,7 +18,7 @@ import { import { importCollection, openCollection, importCollectionFromZip, newHttpRequest } from 'providers/ReduxStore/slices/collections/actions'; import { sortCollections } from 'providers/ReduxStore/slices/collections/index'; -import { savePreferences } from 'providers/ReduxStore/slices/app'; +import { savePreferences, setIsCreatingCollection } from 'providers/ReduxStore/slices/app'; import { normalizePath } from 'utils/common/path'; import { isScratchCollection, flattenItems, isItemTransientRequest } from 'utils/collections'; import { sanitizeName } from 'utils/common/regex'; @@ -59,22 +58,6 @@ const CollectionsSection = () => { const [showCloneGitModal, setShowCloneGitModal] = useState(false); const [gitRepositoryUrl, setGitRepositoryUrl] = useState(null); - // Listen for sidebar-search-open hotkey event - useEffect(() => { - const handleSidebarSearch = () => { - setShowSearch(true); - // Focus the search input after it's rendered - setTimeout(() => { - const searchInput = document.querySelector('.collection-search-input'); - if (searchInput) { - searchInput.focus(); - } - }, 50); - }; - - window.addEventListener('sidebar-search-open', handleSidebarSearch); - return () => window.removeEventListener('sidebar-search-open', handleSidebarSearch); - }, []); // Default to true (don't show modal) so that: // 1. Existing users who upgrade (no hasSeenWelcomeModal in their prefs) don't see it // 2. The modal doesn't flash before preferences are loaded from the electron process diff --git a/packages/bruno-app/src/components/SingleLineEditor/index.js b/packages/bruno-app/src/components/SingleLineEditor/index.js index e08a1c073..ae8e89551 100644 --- a/packages/bruno-app/src/components/SingleLineEditor/index.js +++ b/packages/bruno-app/src/components/SingleLineEditor/index.js @@ -7,7 +7,6 @@ import { setupAutoComplete } from 'utils/codemirror/autocomplete'; import StyledWrapper from './StyledWrapper'; import { IconEye, IconEyeOff } from '@tabler/icons'; import { setupLinkAware } from 'utils/codemirror/linkAware'; -import { setupShortcuts } from 'utils/codemirror/shortcuts'; const CodeMirror = require('codemirror'); @@ -22,11 +21,8 @@ class SingleLineEditor extends Component { this.variables = {}; this.readOnly = props.readOnly || false; - // Shortcuts cleanup function - this._shortcutsCleanup = null; - this.state = { - maskInput: props.isSecret || false + maskInput: props.isSecret || false // Always mask the input by default (if it's a secret) }; } @@ -63,8 +59,8 @@ class SingleLineEditor extends Component { readOnly: this.props.readOnly, extraKeys: { 'Enter': runHandler, - // 'Ctrl-Enter': runHandler, - // 'Cmd-Enter': runHandler, + 'Ctrl-Enter': runHandler, + 'Cmd-Enter': runHandler, 'Alt-Enter': () => { if (this.props.allowNewlines) { this.editor.setValue(this.editor.getValue() + '\n'); @@ -73,7 +69,7 @@ class SingleLineEditor extends Component { this.props.onRun(); } }, - // 'Shift-Enter': runHandler, + 'Shift-Enter': runHandler, 'Cmd-S': saveHandler, 'Ctrl-S': saveHandler, 'Cmd-F': noopHandler, @@ -112,9 +108,6 @@ class SingleLineEditor extends Component { this._updateNewlineMarkers(); } setupLinkAware(this.editor); - - // Setup keyboard shortcuts using the dedicated utility - this._shortcutsCleanup = setupShortcuts(this.editor, this); } /** Enable or disable masking the rendered content of the editor */ @@ -209,12 +202,6 @@ class SingleLineEditor extends Component { } componentWillUnmount() { - // Cleanup shortcuts (keymap and store subscription) - if (this._shortcutsCleanup) { - this._shortcutsCleanup(); - this._shortcutsCleanup = null; - } - if (this.editor) { if (this.editor?._destroyLinkAware) { this.editor._destroyLinkAware(); diff --git a/packages/bruno-app/src/components/StatusBar/index.js b/packages/bruno-app/src/components/StatusBar/index.js index 29e9ab7d3..7a44ef1db 100644 --- a/packages/bruno-app/src/components/StatusBar/index.js +++ b/packages/bruno-app/src/components/StatusBar/index.js @@ -49,7 +49,10 @@ const StatusBar = () => { }; const openGlobalSearch = () => { - window.dispatchEvent(new CustomEvent('global-search-open')); + const bindings = getKeyBindingsForActionAllOS('globalSearch') || []; + bindings.forEach((binding) => { + Mousetrap.trigger(binding); + }); }; return ( diff --git a/packages/bruno-app/src/providers/Hotkeys/index.js b/packages/bruno-app/src/providers/Hotkeys/index.js index 6fb0750c6..a87b67e29 100644 --- a/packages/bruno-app/src/providers/Hotkeys/index.js +++ b/packages/bruno-app/src/providers/Hotkeys/index.js @@ -1,366 +1,290 @@ -import React, { createContext, useEffect, useContext, useRef, useState } from 'react'; +import React, { useState, useEffect } from 'react'; +import toast from 'react-hot-toast'; import find from 'lodash/find'; import Mousetrap from 'mousetrap'; -import toast from 'react-hot-toast'; -import { useSelector } from 'react-redux'; - -import NewRequest from 'components/Sidebar/NewRequest'; +import { useSelector, useDispatch } from 'react-redux'; import NetworkError from 'components/ResponsePane/NetworkError'; +import NewRequest from 'components/Sidebar/NewRequest'; import GlobalSearchModal from 'components/GlobalSearchModal'; -import ImportCollection from 'components/Sidebar/ImportCollection'; - -import store from 'providers/ReduxStore/index'; import { sendRequest, saveRequest, saveCollectionRoot, saveFolderRoot, saveCollectionSettings, - closeTabs, - cloneItem, - pasteItem + closeTabs } from 'providers/ReduxStore/slices/collections/actions'; import { findCollectionByUid, findItemInCollection } from 'utils/collections'; import { addTab, reorderTabs, switchTab } from 'providers/ReduxStore/slices/tabs'; -import { savePreferences, toggleSidebarCollapse, copyRequest } from 'providers/ReduxStore/slices/app'; +import { toggleSidebarCollapse } from 'providers/ReduxStore/slices/app'; import { getKeyBindingsForActionAllOS } from './keyMappings'; -export const HotkeysContext = createContext(null); +export const HotkeysContext = React.createContext(); -// List of all actions that are bound in this provider -const BOUND_ACTIONS = [ - 'save', - 'sendRequest', - 'editEnvironment', - 'newRequest', - 'globalSearch', - 'closeTab', - 'switchToPreviousTab', - 'switchToNextTab', - 'closeAllTabs', - 'collapseSidebar', - 'moveTabLeft', - 'moveTabRight', - 'changeLayout', - 'closeBruno', - 'openPreferences', - 'importCollection', - 'sidebarSearch', - 'zoomIn', - 'zoomOut', - 'resetZoom', - 'cloneItem', - 'copyItem', - 'pasteItem', - 'renameItem' -]; - -/** - * Bind a single hotkey action using Mousetrap. - * Reads from merged defaults + user preferences via getKeyBindingsForActionAllOS. - */ -function bindHotkey(action, handler, userKeyBindings) { - const combos = getKeyBindingsForActionAllOS(action, userKeyBindings); - if (!combos?.length) return; - - Mousetrap.bind([...combos], (e) => { - e?.preventDefault?.(); - handler(e); - return false; - }); -} - -/** - * Unbind a single hotkey action. - */ -function unbindHotkey(action, userKeyBindings) { - const combos = getKeyBindingsForActionAllOS(action, userKeyBindings); - if (!combos?.length) return; - Mousetrap.unbind([...combos]); -} - -/** - * Unbind all known actions for the given user key bindings. - */ -function unbindAllHotkeys(userKeyBindings) { - BOUND_ACTIONS.forEach((action) => unbindHotkey(action, userKeyBindings)); -} - -/** - * Bind all hotkey actions. - */ -function bindAllHotkeys(userKeyBindings) { - const { dispatch, getState } = store; - - // SAVE - bindHotkey('save', () => { - const state = getState(); - const tabs = state.tabs.tabs; - const collections = state.collections.collections; - const activeTabUid = state.tabs.activeTabUid; - - const activeTab = find(tabs, (t) => t.uid === activeTabUid); - if (!activeTab) return; - - if (activeTab.type === 'environment-settings' || activeTab.type === 'global-environment-settings') { - window.dispatchEvent(new CustomEvent('environment-save')); - return; - } - - const collection = findCollectionByUid(collections, activeTab.collectionUid); - if (!collection) return; - - const item = findItemInCollection(collection, activeTab.uid); - - if (item?.uid) { - if (activeTab.type === 'folder-settings') { - dispatch(saveFolderRoot(collection.uid, item.uid)); - } else { - dispatch(saveRequest(activeTab.uid, activeTab.collectionUid)); - } - } else if (activeTab.type === 'collection-settings') { - dispatch(saveCollectionSettings(collection.uid)); - } - }, userKeyBindings); - - // SEND REQUEST - bindHotkey('sendRequest', () => { - const state = getState(); - const tabs = state.tabs.tabs; - const collections = state.collections.collections; - const activeTabUid = state.tabs.activeTabUid; - - const activeTab = find(tabs, (t) => t.uid === activeTabUid); - if (!activeTab) return; - - const collection = findCollectionByUid(collections, activeTab.collectionUid); - if (!collection) return; - - const item = findItemInCollection(collection, activeTab.uid); - if (!item) return; - - if (item.type === 'grpc-request') { - const request = item.draft ? item.draft.request : item.request; - if (!request.url) return toast.error('Please enter a valid gRPC server URL'); - if (!request.method) return toast.error('Please select a gRPC method'); - } - - dispatch(sendRequest(item, collection.uid)).catch(() => - toast.custom((t) => toast.dismiss(t.id)} />, { duration: 5000 }) - ); - }, userKeyBindings); - - // EDIT ENV - bindHotkey('editEnvironment', () => { - const state = getState(); - const tabs = state.tabs.tabs; - const collections = state.collections.collections; - const activeTabUid = state.tabs.activeTabUid; - - const activeTab = find(tabs, (t) => t.uid === activeTabUid); - if (!activeTab) return; - - const collection = findCollectionByUid(collections, activeTab.collectionUid); - if (!collection) return; - - dispatch( - addTab({ - uid: `${collection.uid}-environment-settings`, - collectionUid: collection.uid, - type: 'environment-settings' - }) - ); - }, userKeyBindings); - - // NEW REQUEST -> trigger via event so the provider can open the modal - bindHotkey('newRequest', () => { - window.dispatchEvent(new CustomEvent('new-request-open')); - }, userKeyBindings); - - // GLOBAL SEARCH -> trigger via event so the provider can open the modal - bindHotkey('globalSearch', () => { - window.dispatchEvent(new CustomEvent('global-search-open')); - }, userKeyBindings); - - // CLOSE TAB - bindHotkey('closeTab', () => { - window.dispatchEvent(new CustomEvent('close-active-tab')); - }, userKeyBindings); - - // SWITCH PREV TAB - bindHotkey('switchToPreviousTab', () => { - dispatch(switchTab({ direction: 'pageup' })); - }, userKeyBindings); - - // SWITCH NEXT TAB - bindHotkey('switchToNextTab', () => { - dispatch(switchTab({ direction: 'pagedown' })); - }, userKeyBindings); - - // CLOSE ALL TABS - bindHotkey('closeAllTabs', () => { - window.dispatchEvent(new CustomEvent('close-active-tab')); - }, userKeyBindings); - - // COLLAPSE SIDEBAR - bindHotkey('collapseSidebar', () => { - dispatch(toggleSidebarCollapse()); - }, userKeyBindings); - - // MOVE TAB LEFT - bindHotkey('moveTabLeft', () => { - dispatch(reorderTabs({ direction: -1 })); - }, userKeyBindings); - - // MOVE TAB RIGHT - bindHotkey('moveTabRight', () => { - dispatch(reorderTabs({ direction: 1 })); - }, userKeyBindings); - - // CHANGE LAYOUT -> toggle response pane orientation - bindHotkey('changeLayout', () => { - const state = getState(); - const preferences = state.app.preferences; - const currentOrientation = preferences?.layout?.responsePaneOrientation || 'horizontal'; - const newOrientation = currentOrientation === 'horizontal' ? 'vertical' : 'horizontal'; - const updatedPreferences = { - ...preferences, - layout: { - ...preferences.layout, - responsePaneOrientation: newOrientation - } - }; - dispatch(savePreferences(updatedPreferences)); - }, userKeyBindings); - - // CLOSE BRUNO -> send IPC to close the window - bindHotkey('closeBruno', () => { - window.ipcRenderer?.send('renderer:window-close'); - }, userKeyBindings); - - // OPEN PREFERENCES -> open preferences tab - bindHotkey('openPreferences', () => { - const state = getState(); - const tabs = state.tabs.tabs; - const activeTabUid = state.tabs.activeTabUid; - const activeTab = tabs.find((t) => t.uid === activeTabUid); - - dispatch( - addTab({ - type: 'preferences', - uid: activeTab?.collectionUid ? `${activeTab.collectionUid}-preferences` : 'preferences', - collectionUid: activeTab?.collectionUid - }) - ); - }, userKeyBindings); - - // IMPORT COLLECTION -> trigger event to open import modal - bindHotkey('importCollection', () => { - window.dispatchEvent(new CustomEvent('import-collection-open')); - }, userKeyBindings); - - // SIDEBAR SEARCH -> trigger event to focus sidebar search - bindHotkey('sidebarSearch', () => { - window.dispatchEvent(new CustomEvent('sidebar-search-open')); - }, userKeyBindings); - - // ZOOM IN - bindHotkey('zoomIn', () => { - window.ipcRenderer?.invoke('renderer:zoom-in'); - }, userKeyBindings); - - // ZOOM OUT - bindHotkey('zoomOut', () => { - window.ipcRenderer?.invoke('renderer:zoom-out'); - }, userKeyBindings); - - // RESET ZOOM - bindHotkey('resetZoom', () => { - window.ipcRenderer?.invoke('renderer:reset-zoom'); - }, userKeyBindings); - - // CLONE ITEM -> trigger event so the sidebar can handle opening the clone modal - bindHotkey('cloneItem', () => { - window.dispatchEvent(new CustomEvent('clone-item-open')); - }, userKeyBindings); - - // COPY ITEM -> copy currently selected item to clipboard - bindHotkey('copyItem', () => { - window.dispatchEvent(new CustomEvent('copy-item-open')); - }, userKeyBindings); - - // PASTE ITEM -> paste from clipboard to current location - bindHotkey('pasteItem', () => { - window.dispatchEvent(new CustomEvent('paste-item-open')); - }, userKeyBindings); - - // RENAME ITEM -> trigger event so the sidebar can handle opening the rename modal - bindHotkey('renameItem', () => { - window.dispatchEvent(new CustomEvent('rename-item-open')); - }, userKeyBindings); -} - -// ----------------------- -// Provider (manages hotkey lifecycle + modal state) -// ----------------------- export const HotkeysProvider = (props) => { + const dispatch = useDispatch(); const tabs = useSelector((state) => state.tabs.tabs); const collections = useSelector((state) => state.collections.collections); const activeTabUid = useSelector((state) => state.tabs.activeTabUid); - const userKeyBindings = useSelector((state) => state.app.preferences?.keyBindings); - const [showNewRequestModal, setShowNewRequestModal] = useState(false); const [showGlobalSearchModal, setShowGlobalSearchModal] = useState(false); - const [showImportCollectionModal, setShowImportCollectionModal] = useState(false); - - // Keep a ref to the previous userKeyBindings so we can unbind old combos - const prevKeyBindingsRef = useRef(undefined); const getCurrentCollection = () => { const activeTab = find(tabs, (t) => t.uid === activeTabUid); - if (!activeTab) return undefined; - return findCollectionByUid(collections, activeTab.collectionUid); + if (activeTab) { + const collection = findCollectionByUid(collections, activeTab.collectionUid); + + return collection; + } }; - const currentCollection = getCurrentCollection(); - - // Bind/rebind hotkeys whenever user preferences change + // save hotkey useEffect(() => { - // Store previous bindings before updating - const prevBindings = prevKeyBindingsRef.current; + Mousetrap.bind([...getKeyBindingsForActionAllOS('save')], (e) => { + const activeTab = find(tabs, (t) => t.uid === activeTabUid); + if (activeTab) { + if (activeTab.type === 'environment-settings' || activeTab.type === 'global-environment-settings') { + window.dispatchEvent(new CustomEvent('environment-save')); + return false; + } - // Unbind previous bindings (if any) - if (prevBindings !== undefined) { - unbindAllHotkeys(prevBindings); - } + const collection = findCollectionByUid(collections, activeTab.collectionUid); + if (collection) { + const item = findItemInCollection(collection, activeTab.uid); + if (item && item.uid) { + if (activeTab.type === 'folder-settings') { + dispatch(saveFolderRoot(collection.uid, item.uid)); + } else { + dispatch(saveRequest(activeTab.uid, activeTab.collectionUid)); + } + } else if (activeTab.type === 'collection-settings') { + dispatch(saveCollectionSettings(collection.uid)); + } + } + } - // Bind with current preferences - bindAllHotkeys(userKeyBindings); - prevKeyBindingsRef.current = userKeyBindings; + return false; // this stops the event bubbling + }); return () => { - // Cleanup on unmount - unbindAllHotkeys(userKeyBindings); + Mousetrap.unbind([...getKeyBindingsForActionAllOS('save')]); }; - }, [userKeyBindings]); + }, [activeTabUid, tabs, saveRequest, collections, dispatch]); - // Listen for hotkey-triggered events for modals + // send request (ctrl/cmd + enter) useEffect(() => { - const openNewRequest = () => setShowNewRequestModal(true); - const openGlobalSearch = () => setShowGlobalSearchModal(true); - const openImportCollection = () => setShowImportCollectionModal(true); + Mousetrap.bind([...getKeyBindingsForActionAllOS('sendRequest')], (e) => { + const activeTab = find(tabs, (t) => t.uid === activeTabUid); + if (activeTab) { + const collection = findCollectionByUid(collections, activeTab.collectionUid); - window.addEventListener('new-request-open', openNewRequest); - window.addEventListener('global-search-open', openGlobalSearch); - window.addEventListener('import-collection-open', openImportCollection); + if (collection) { + const item = findItemInCollection(collection, activeTab.uid); + if (item) { + if (item.type === 'grpc-request') { + const request = item.draft ? item.draft.request : item.request; + if (!request.url) { + toast.error('Please enter a valid gRPC server URL'); + return; + } + if (!request.method) { + toast.error('Please select a gRPC method'); + return; + } + } + + dispatch(sendRequest(item, collection.uid)).catch((err) => + toast.custom((t) => toast.dismiss(t.id)} />, { + duration: 5000 + }) + ); + } + } + } + + return false; // this stops the event bubbling + }); return () => { - window.removeEventListener('new-request-open', openNewRequest); - window.removeEventListener('global-search-open', openGlobalSearch); - window.removeEventListener('import-collection-open', openImportCollection); + Mousetrap.unbind([...getKeyBindingsForActionAllOS('sendRequest')]); + }; + }, [activeTabUid, tabs, saveRequest, collections]); + + // edit environments (ctrl/cmd + e) + useEffect(() => { + Mousetrap.bind([...getKeyBindingsForActionAllOS('editEnvironment')], (e) => { + const activeTab = find(tabs, (t) => t.uid === activeTabUid); + if (activeTab) { + const collection = findCollectionByUid(collections, activeTab.collectionUid); + + if (collection) { + dispatch( + addTab({ + uid: `${collection.uid}-environment-settings`, + collectionUid: collection.uid, + type: 'environment-settings' + }) + ); + } + } + + return false; // this stops the event bubbling + }); + + return () => { + Mousetrap.unbind([...getKeyBindingsForActionAllOS('editEnvironment')]); + }; + }, [activeTabUid, tabs, collections, dispatch]); + + // new request (ctrl/cmd + b) + useEffect(() => { + Mousetrap.bind([...getKeyBindingsForActionAllOS('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 () => { + Mousetrap.unbind([...getKeyBindingsForActionAllOS('newRequest')]); + }; + }, [activeTabUid, tabs, collections, setShowNewRequestModal]); + + // global search (ctrl/cmd + k) + useEffect(() => { + Mousetrap.bind([...getKeyBindingsForActionAllOS('globalSearch')], (e) => { + setShowGlobalSearchModal(true); + + return false; // stop bubbling + }); + + return () => { + Mousetrap.unbind([...getKeyBindingsForActionAllOS('globalSearch')]); }; }, []); + // close tab hotkey + useEffect(() => { + Mousetrap.bind([...getKeyBindingsForActionAllOS('closeTab')], (e) => { + if (activeTabUid) { + dispatch( + closeTabs({ + tabUids: [activeTabUid] + }) + ); + } + + return false; // this stops the event bubbling + }); + + return () => { + Mousetrap.unbind([...getKeyBindingsForActionAllOS('closeTab')]); + }; + }, [activeTabUid]); + + // Switch to the previous tab + useEffect(() => { + Mousetrap.bind([...getKeyBindingsForActionAllOS('switchToPreviousTab')], (e) => { + dispatch( + switchTab({ + direction: 'pageup' + }) + ); + + return false; // this stops the event bubbling + }); + + return () => { + Mousetrap.unbind([...getKeyBindingsForActionAllOS('switchToPreviousTab')]); + }; + }, [dispatch]); + + // Switch to the next tab + useEffect(() => { + Mousetrap.bind([...getKeyBindingsForActionAllOS('switchToNextTab')], (e) => { + dispatch( + switchTab({ + direction: 'pagedown' + }) + ); + + return false; // this stops the event bubbling + }); + + return () => { + Mousetrap.unbind([...getKeyBindingsForActionAllOS('switchToNextTab')]); + }; + }, [dispatch]); + + // Close all tabs + useEffect(() => { + Mousetrap.bind([...getKeyBindingsForActionAllOS('closeAllTabs')], (e) => { + const activeTab = find(tabs, (t) => t.uid === activeTabUid); + if (activeTab) { + const collection = findCollectionByUid(collections, activeTab.collectionUid); + + if (collection) { + const tabUids = tabs.filter((tab) => tab.collectionUid === collection.uid).map((tab) => tab.uid); + dispatch( + closeTabs({ + tabUids: tabUids + }) + ); + } + } + + return false; // this stops the event bubbling + }); + + return () => { + Mousetrap.unbind([...getKeyBindingsForActionAllOS('closeAllTabs')]); + }; + }, [activeTabUid, tabs, collections, dispatch]); + + // Collapse sidebar (ctrl/cmd + \) + useEffect(() => { + Mousetrap.bind([...getKeyBindingsForActionAllOS('collapseSidebar')], (e) => { + dispatch(toggleSidebarCollapse()); + return false; + }); + + return () => { + Mousetrap.unbind([...getKeyBindingsForActionAllOS('collapseSidebar')]); + }; + }, [dispatch]); + + // Move tab left + useEffect(() => { + Mousetrap.bind([...getKeyBindingsForActionAllOS('moveTabLeft')], (e) => { + dispatch(reorderTabs({ direction: -1 })); + return false; // this stops the event bubbling + }); + + return () => { + Mousetrap.unbind([...getKeyBindingsForActionAllOS('moveTabLeft')]); + }; + }, [dispatch]); + + // Move tab right + useEffect(() => { + Mousetrap.bind([...getKeyBindingsForActionAllOS('moveTabRight')], (e) => { + dispatch(reorderTabs({ direction: 1 })); + return false; // this stops the event bubbling + }); + + return () => { + Mousetrap.unbind([...getKeyBindingsForActionAllOS('moveTabRight')]); + }; + }, [dispatch]); + + const currentCollection = getCurrentCollection(); + return ( {showNewRequestModal && ( @@ -369,16 +293,13 @@ export const HotkeysProvider = (props) => { {showGlobalSearchModal && ( setShowGlobalSearchModal(false)} /> )} - {showImportCollectionModal && ( - setShowImportCollectionModal(false)} /> - )}
{props.children}
); }; export const useHotkeys = () => { - const context = useContext(HotkeysContext); + const context = React.useContext(HotkeysContext); if (!context) { throw new Error(`useHotkeys must be used within a HotkeysProvider`); diff --git a/packages/bruno-app/src/providers/Hotkeys/keyMappings.js b/packages/bruno-app/src/providers/Hotkeys/keyMappings.js index 1415e1e9f..0c439baf2 100644 --- a/packages/bruno-app/src/providers/Hotkeys/keyMappings.js +++ b/packages/bruno-app/src/providers/Hotkeys/keyMappings.js @@ -1,76 +1,42 @@ -export const DEFAULT_KEY_BINDINGS = { - save: { mac: 'command+bind+s', windows: 'ctrl+bind+s', name: 'Save' }, - sendRequest: { mac: 'command+bind+enter', windows: 'ctrl+bind+enter', name: 'Send Request' }, - editEnvironment: { mac: 'command+bind+e', windows: 'ctrl+bind+e', name: 'Edit Environment' }, - newRequest: { mac: 'command+bind+n', windows: 'ctrl+bind+n', name: 'New Request' }, - importCollection: { mac: 'command+bind+o', windows: 'ctrl+bind+o', name: 'Import Collection' }, - globalSearch: { mac: 'command+bind+k', windows: 'ctrl+bind+k', name: 'Global Search' }, - sidebarSearch: { mac: 'command+bind+f', windows: 'ctrl+bind+f', name: 'Search Sidebar' }, - closeTab: { mac: 'command+bind+w', windows: 'ctrl+bind+w', name: 'Close Tab' }, - openPreferences: { mac: 'command+bind+,', windows: 'ctrl+bind+,', name: 'Open Preferences' }, - changeLayout: { mac: 'command+bind+j', windows: 'ctrl+bind+j', name: 'Change Orientation' }, +const KeyMapping = { + save: { mac: 'command+s', windows: 'ctrl+s', name: 'Save' }, + sendRequest: { mac: 'command+enter', windows: 'ctrl+enter', name: 'Send Request' }, + editEnvironment: { mac: 'command+e', windows: 'ctrl+e', name: 'Edit Environment' }, + newRequest: { mac: 'command+b', windows: 'ctrl+b', name: 'New Request' }, + globalSearch: { mac: 'command+k', windows: 'ctrl+k', name: 'Global Search' }, + closeTab: { mac: 'command+w', windows: 'ctrl+w', name: 'Close Tab' }, + openPreferences: { mac: 'command+,', windows: 'ctrl+,', name: 'Open Preferences' }, closeBruno: { - mac: 'command+bind+q', - windows: 'ctrl+bind+shift+bind+q', + mac: 'command+Q', + windows: 'ctrl+shift+q', name: 'Close Bruno' }, switchToPreviousTab: { - mac: 'command+bind+2', - windows: 'ctrl+bind+2', + mac: 'command+pageup', + windows: 'ctrl+pageup', name: 'Switch to Previous Tab' }, switchToNextTab: { - mac: 'command+bind+1', - windows: 'ctrl+bind+1', + mac: 'command+pagedown', + windows: 'ctrl+pagedown', name: 'Switch to Next Tab' }, moveTabLeft: { - mac: 'command+bind+[', - windows: 'ctrl+bind+[', + mac: 'command+shift+pageup', + windows: 'ctrl+shift+pageup', name: 'Move Tab Left' }, moveTabRight: { - mac: 'command+bind+]', - windows: 'ctrl+bind+]', + mac: 'command+shift+pagedown', + windows: 'ctrl+shift+pagedown', name: 'Move Tab Right' }, - closeAllTabs: { mac: 'command+bind+shift+bind+w', windows: 'ctrl+bind+shift+bind+w', name: 'Close All Tabs' }, - collapseSidebar: { mac: 'command+bind+\\', windows: 'ctrl+bind+\\', name: 'Collapse Sidebar' }, - zoomIn: { mac: 'command+bind+=', windows: 'ctrl+bind+=', name: 'Zoom In' }, - zoomOut: { mac: 'command+bind+-', windows: 'ctrl+bind+-', name: 'Zoom Out' }, - resetZoom: { mac: 'command+bind+0', windows: 'ctrl+bind+0', name: 'Reset Zoom' }, - cloneItem: { mac: 'command+bind+d', windows: 'ctrl+bind+d', name: 'Clone Item' }, - copyItem: { mac: 'command+bind+c', windows: 'ctrl+bind+c', name: 'Copy Item' }, - pasteItem: { mac: 'command+bind+v', windows: 'ctrl+bind+v', name: 'Paste Item' }, - renameItem: { mac: 'command+bind+r', windows: 'ctrl+bind+r', name: 'Rename Item' } -}; - -/** - * Converts keybindings from storage format (+bind+) to Mousetrap format (+) - * Storage format uses +bind+ as separator to avoid conflicts with the actual + key - * Mousetrap uses + as the separator - * Also converts arrow key names to Mousetrap format - * - * @param {string} keysStr - Keybinding string in storage format - * @returns {string|null} Keybinding string in Mousetrap format, or null if empty - */ -export const toMousetrapCombo = (keysStr) => { - if (!keysStr) return null; - - // Split by +bind+ separator - const parts = keysStr.split('+bind+').filter(Boolean); - - // Convert arrow key names from browser format to Mousetrap format - const converted = parts.map((part) => { - const lower = part.toLowerCase(); - if (lower === 'arrowup') return 'up'; - if (lower === 'arrowdown') return 'down'; - if (lower === 'arrowleft') return 'left'; - if (lower === 'arrowright') return 'right'; - return lower; - }); - - return converted.join('+'); + closeAllTabs: { mac: 'command+shift+w', windows: 'ctrl+shift+w', name: 'Close All Tabs' }, + collapseSidebar: { mac: 'command+\\', windows: 'ctrl+\\', name: 'Collapse Sidebar' }, + zoomIn: { mac: 'command+=', windows: 'ctrl+=', name: 'Zoom In' }, + zoomOut: { mac: 'command+-', windows: 'ctrl+-', name: 'Zoom Out' }, + resetZoom: { mac: 'command+0', windows: 'ctrl+0', name: 'Reset Zoom' }, + renameItem: { mac: 'enter', windows: 'f2', name: 'Rename Collection Item' } }; /** @@ -81,7 +47,7 @@ export const toMousetrapCombo = (keysStr) => { */ export const getKeyBindingsForOS = (os) => { const keyBindings = {}; - for (const [action, { name, ...keys }] of Object.entries(DEFAULT_KEY_BINDINGS)) { + for (const [action, { name, ...keys }] of Object.entries(KeyMapping)) { if (keys[os]) { keyBindings[action] = { keys: keys[os], @@ -93,57 +59,18 @@ export const getKeyBindingsForOS = (os) => { }; /** - * Merges default key bindings with user preferences. - * - * @param {Object} userKeyBindings - User's custom key bindings from preferences (preferences.keyBindings) - * @returns {Object} Merged key bindings object - */ -export const getMergedKeyBindings = (userKeyBindings) => { - const merged = {}; - - // Start with defaults - for (const [action, binding] of Object.entries(DEFAULT_KEY_BINDINGS)) { - merged[action] = { ...binding }; - } - - // Override with user preferences - if (userKeyBindings && typeof userKeyBindings === 'object') { - for (const [action, binding] of Object.entries(userKeyBindings)) { - if (merged[action]) { - merged[action] = { ...merged[action], ...binding }; - } - } - } - - return merged; -}; - -/** - * Retrieves the Mousetrap-compatible key combos for a specific action across all operating systems. - * Reads from merged defaults + user preferences. + * Retrieves the key bindings for a specific action across all operating systems. * * @param {string} action - The action for which to retrieve key bindings. - * @param {Object} [userKeyBindings] - User's custom key bindings from preferences - * @returns {string[]|null} Array of Mousetrap-compatible combo strings, or null if the action is not found. + * @returns {Object|null} An object containing the key bindings for macOS, Windows, or null if the action is not found. */ -export const getKeyBindingsForActionAllOS = (action, userKeyBindings) => { - const merged = getMergedKeyBindings(userKeyBindings); - const actionBindings = merged[action]; +export const getKeyBindingsForActionAllOS = (action) => { + const actionBindings = KeyMapping[action]; if (!actionBindings) { console.warn(`Action "${action}" not found in KeyMapping.`); return null; } - const combos = []; - if (actionBindings.mac) { - const combo = toMousetrapCombo(actionBindings.mac); - if (combo) combos.push(combo); - } - if (actionBindings.windows) { - const combo = toMousetrapCombo(actionBindings.windows); - if (combo) combos.push(combo); - } - - return combos.length > 0 ? combos : null; + return [actionBindings.mac, actionBindings.windows]; }; diff --git a/packages/bruno-app/src/utils/codemirror/shortcuts.js b/packages/bruno-app/src/utils/codemirror/shortcuts.js deleted file mode 100644 index 2d526501d..000000000 --- a/packages/bruno-app/src/utils/codemirror/shortcuts.js +++ /dev/null @@ -1,233 +0,0 @@ -import { getKeyBindingsForActionAllOS } from 'providers/Hotkeys/keyMappings'; -import store from 'providers/ReduxStore/index'; -import { reorderTabs, switchTab } from 'providers/ReduxStore/slices/tabs'; -import { savePreferences, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app'; - -const CodeMirror = require('codemirror'); - -const KEYBINDING_ACTIONS = [ - { - actionName: 'closeTab', - handler: () => { - window.dispatchEvent(new CustomEvent('close-active-tab')); - return true; - } - }, - { - actionName: 'sendRequest', - handler: (context) => { - if (context?.props?.onRun) context.props.onRun(); - return true; - } - }, - { - actionName: 'switchToPreviousTab', - handler: () => { - store.dispatch(switchTab({ direction: 'pageup' })); - return true; - } - }, - { - actionName: 'switchToNextTab', - handler: () => { - store.dispatch(switchTab({ direction: 'pagedown' })); - return true; - } - }, - { - actionName: 'moveTabLeft', - handler: () => { - store.dispatch(reorderTabs({ direction: -1 })); - return true; - } - }, - { - actionName: 'moveTabRight', - handler: () => { - store.dispatch(reorderTabs({ direction: 1 })); - return true; - } - }, - { - actionName: 'changeLayout', - handler: () => { - const state = store.getState(); - const preferences = state.app.preferences; - const currentOrientation = preferences?.layout?.responsePaneOrientation || 'horizontal'; - const newOrientation = currentOrientation === 'horizontal' ? 'vertical' : 'horizontal'; - const updatedPreferences = { - ...preferences, - layout: { - ...preferences.layout, - responsePaneOrientation: newOrientation - } - }; - store.dispatch(savePreferences(updatedPreferences)); - return true; - } - }, - { - actionName: 'collapseSidebar', - handler: () => { - store.dispatch(toggleSidebarCollapse()); - return true; - } - } -]; - -/** - * Converts user keybinding format to CodeMirror format - * e.g., "command+bind+enter" -> "Cmd-Enter" - * @param {string} combo - The keybinding combo string - * @returns {string|null} CodeMirror formatted combo or null - */ -function convertToCodeMirrorFormat(combo) { - if (!combo || typeof combo !== 'string') return null; - - const normalized = combo - .replace(/-/g, '+') - .split('+') - .map((p) => p.trim()) - .filter(Boolean) - .filter((p) => p.toLowerCase() !== 'bind') - .join('+'); - - const parts = normalized.split('+').map((p) => p.trim()).filter(Boolean); - - const out = parts.map((key) => { - const lower = key.toLowerCase(); - - if (lower === 'command' || lower === 'cmd') return 'Cmd'; - if (lower === 'control' || lower === 'ctrl') return 'Ctrl'; - if (lower === 'option' || lower === 'alt') return 'Alt'; - if (lower === 'shift') return 'Shift'; - if (lower === 'mod') return 'Mod'; - - if (lower === 'enter' || lower === 'return') return 'Enter'; - if (lower === 'esc' || lower === 'escape') return 'Esc'; - if (lower === 'space') return 'Space'; - if (lower === 'tab') return 'Tab'; - if (lower === 'backspace') return 'Backspace'; - if (lower === 'delete' || lower === 'del') return 'Delete'; - if (lower === 'up') return 'Up'; - if (lower === 'down') return 'Down'; - if (lower === 'left') return 'Left'; - if (lower === 'right') return 'Right'; - - if (key.length === 1) return key.toUpperCase(); - return key.charAt(0).toUpperCase() + key.slice(1); - }); - - return out.join('-'); -} - -/** - * Builds a consolidated CodeMirror keymap from all configured keybinding actions. - * Uses CodeMirror.Pass for non-matching keys to allow default behavior. - * @param {Object} context - Context object containing props and other editor context - * @returns {Object} CodeMirror keymap object - */ -function buildKeymap(context) { - let state; - try { - const reduxState = store.getState(); - state = reduxState; - } catch (e) { - state = { app: { preferences: {} } }; - } - - const userKeyBindings = state.app.preferences?.keyBindings || {}; - - // Create a comprehensive keymap with CodeMirror.Pass as fallthrough - // This allows non-matching keys to pass through to default CodeMirror behavior - const keyMap = { - name: 'singleLineEditor.custom', - // CodeMirror.Pass tells CodeMirror to pass this key event to the next keymap - // This is the key to making non-configured keys work normally - fallthrough: CodeMirror.Pass - }; - - // Build keymap entries for each configured action - KEYBINDING_ACTIONS.forEach(({ actionName, handler }) => { - const combos = getKeyBindingsForActionAllOS(actionName, userKeyBindings) || []; - const cmCombos = combos - .map((k) => convertToCodeMirrorFormat(k)) - .filter(Boolean); - - if (cmCombos.length > 0) { - cmCombos.forEach((cmKey) => { - // Create handler that passes context as argument - keyMap[cmKey] = () => handler(context); - }); - } - }); - - return keyMap; -} - -/** - * Sets up keyboard shortcuts for a CodeMirror editor instance. - * This enables custom keybindings with CodeMirror.Pass fallthrough support. - * @param {Object} editor - The CodeMirror editor instance - * @param {Object} context - Context object containing props and other editor context - * @returns {Object} Cleanup function to remove the keymap - */ -function setupShortcuts(editor, context = {}) { - if (!editor) { - return () => { }; - } - - let currentKeyMap = null; - let unsubscribeStore = null; - - /** - * Apply the consolidated custom keymap to the CodeMirror editor - */ - const applyKeyMap = () => { - if (!editor) return; - - // Remove existing custom keymap if any - if (currentKeyMap) { - try { - editor.removeKeyMap(currentKeyMap); - } catch (e) { - console.warn('[SingleLineEditor] Error removing keymap:', e); - } - } - - // Build and apply new consolidated keymap - currentKeyMap = buildKeymap(context); - editor.addKeyMap(currentKeyMap); - }; - - // Apply keymap on setup - applyKeyMap(); - - // Subscribe to store changes to rebuild keymap when preferences change - unsubscribeStore = store.subscribe(() => { - applyKeyMap(); - }); - - /** - * Cleanup function to remove the keymap and unsubscribe from store - */ - const cleanup = () => { - if (unsubscribeStore) { - unsubscribeStore(); - unsubscribeStore = null; - } - - if (editor && currentKeyMap) { - try { - editor.removeKeyMap(currentKeyMap); - } catch (e) { - console.warn('[SingleLineEditor] Error removing keymap on cleanup:', e); - } - currentKeyMap = null; - } - }; - - return cleanup; -} - -export { setupShortcuts, buildKeymap, convertToCodeMirrorFormat, KEYBINDING_ACTIONS }; diff --git a/packages/bruno-electron/src/app/menu-template.js b/packages/bruno-electron/src/app/menu-template.js index f04971877..fe6cbc716 100644 --- a/packages/bruno-electron/src/app/menu-template.js +++ b/packages/bruno-electron/src/app/menu-template.js @@ -27,6 +27,7 @@ const template = [ }, { label: 'Preferences', + accelerator: 'CommandOrControl+,', click() { ipcMain.emit('main:open-preferences'); } @@ -88,7 +89,7 @@ const template = [ }, { role: 'window', - submenu: [{ role: 'minimize' }, { role: 'close' }] + submenu: [{ role: 'minimize' }, { role: 'close', accelerator: 'CommandOrControl+Shift+Q' }] }, { role: 'help', diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js index 606127513..6c7de8518 100644 --- a/packages/bruno-electron/src/index.js +++ b/packages/bruno-electron/src/index.js @@ -216,8 +216,7 @@ app.on('ready', async () => { nodeIntegration: true, contextIsolation: true, preload: path.join(__dirname, 'preload.js'), - webviewTag: true, - zoomFactor: 1.0 + webviewTag: true }, title: 'Bruno', icon: path.join(__dirname, 'about/256x256.png'), @@ -247,29 +246,8 @@ app.on('ready', async () => { } }); - // Handle zoom shortcuts - ipcMain.on('main:zoom-in', () => { - if (mainWindow && mainWindow.webContents) { - const currentZoom = mainWindow.webContents.getZoomLevel(); - mainWindow.webContents.setZoomLevel(currentZoom + 0.5); - } - }); - - ipcMain.on('main:zoom-out', () => { - if (mainWindow && mainWindow.webContents) { - const currentZoom = mainWindow.webContents.getZoomLevel(); - mainWindow.webContents.setZoomLevel(currentZoom - 0.5); - } - }); - - ipcMain.on('main:zoom-reset', () => { - if (mainWindow && mainWindow.webContents) { - mainWindow.webContents.setZoomLevel(0); - } - }); - ipcMain.on('renderer:window-close', () => { - // if (!isWindows && !isLinux) return; + if (!isWindows && !isLinux) return; mainWindow.close(); }); @@ -505,6 +483,14 @@ app.on('open-file', (event, path) => { openCollection(mainWindow, collectionWatcher, path); }); +// Register the global shortcuts +app.on('browser-window-focus', () => { + // Quick fix for Electron issue #29996: https://github.com/electron/electron/issues/29996 + globalShortcut.register('Ctrl+=', () => { + incrementZoomAndPersist(10); + }); +}); + // Disable global shortcuts when not focused app.on('browser-window-blur', () => { globalShortcut.unregisterAll(); diff --git a/packages/bruno-electron/src/store/preferences.js b/packages/bruno-electron/src/store/preferences.js index 48ed9db2a..29cc9d24b 100644 --- a/packages/bruno-electron/src/store/preferences.js +++ b/packages/bruno-electron/src/store/preferences.js @@ -58,52 +58,6 @@ const defaultPreferences = { enabled: false, interval: 1000 }, - keyBindings: { - save: { mac: 'command+bind+s', windows: 'ctrl+bind+s', name: 'Save' }, - sendRequest: { mac: 'command+bind+enter', windows: 'ctrl+bind+enter', name: 'Send Request' }, - editEnvironment: { mac: 'command+bind+e', windows: 'ctrl+bind+e', name: 'Edit Environment' }, - newRequest: { mac: 'command+bind+n', windows: 'ctrl+bind+n', name: 'New Request' }, - importCollection: { mac: 'command+bind+o', windows: 'ctrl+bind+o', name: 'Import Collection' }, - globalSearch: { mac: 'command+bind+k', windows: 'ctrl+bind+k', name: 'Global Search' }, - sidebarSearch: { mac: 'command+bind+f', windows: 'ctrl+bind+f', name: 'Search Sidebar' }, - closeTab: { mac: 'command+bind+w', windows: 'ctrl+bind+w', name: 'Close Tab' }, - openPreferences: { mac: 'command+bind+,', windows: 'ctrl+bind+,', name: 'Open Preferences' }, - changeLayout: { mac: 'command+bind+j', windows: 'ctrl+bind+j', name: 'Change Orientation' }, - closeBruno: { - mac: 'command+bind+q', - windows: 'ctrl+bind+shift+bind+q', - name: 'Close Bruno' - }, - switchToPreviousTab: { - mac: 'command+bind+2', - windows: 'ctrl+bind+2', - name: 'Switch to Previous Tab' - }, - switchToNextTab: { - mac: 'command+bind+1', - windows: 'ctrl+bind+1', - name: 'Switch to Next Tab' - }, - moveTabLeft: { - mac: 'command+bind+[', - windows: 'ctrl+bind+[', - name: 'Move Tab Left' - }, - moveTabRight: { - mac: 'command+bind+]', - windows: 'ctrl+bind+]', - name: 'Move Tab Right' - }, - closeAllTabs: { mac: 'command+bind+shift+bind+w', windows: 'ctrl+bind+shift+bind+w', name: 'Close All Tabs' }, - collapseSidebar: { mac: 'command+bind+\\', windows: 'ctrl+bind+\\', name: 'Collapse Sidebar' }, - zoomIn: { mac: 'command+bind+=', windows: 'ctrl+bind+=', name: 'Zoom In' }, - zoomOut: { mac: 'command+bind+-', windows: 'ctrl+bind+-', name: 'Zoom Out' }, - resetZoom: { mac: 'command+bind+0', windows: 'ctrl+bind+0', name: 'Reset Zoom' }, - cloneItem: { mac: 'command+bind+d', windows: 'ctrl+bind+d', name: 'Clone Item' }, - copyItem: { mac: 'command+bind+c', windows: 'ctrl+bind+c', name: 'Copy Item' }, - pasteItem: { mac: 'command+bind+v', windows: 'ctrl+bind+v', name: 'Paste Item' }, - renameItem: { mac: 'command+bind+r', windows: 'ctrl+bind+r', name: 'Rename Item' } - }, display: { zoomPercentage: 100 }, diff --git a/tests/collection/moving-tabs/move-tabs.spec.ts b/tests/collection/moving-tabs/move-tabs.spec.ts index 1898f8663..63c8f0f9f 100644 --- a/tests/collection/moving-tabs/move-tabs.spec.ts +++ b/tests/collection/moving-tabs/move-tabs.spec.ts @@ -2,8 +2,6 @@ import { test, expect } from '../../../playwright'; import { closeAllCollections, createCollection } from '../../utils/page'; test.describe('Move tabs', () => { - const modifier = process.platform === 'darwin' ? 'Meta' : 'Control'; - test.afterEach(async ({ page }) => { // cleanup: close all collections await closeAllCollections(page); @@ -137,7 +135,7 @@ test.describe('Move tabs', () => { // Move the request tab before the folder tab using keyboard shortcut const source = page.locator('.request-tab .tab-label').filter({ hasText: 'test-request' }); await source.click(); - await page.keyboard.press(`${modifier}+BracketLeft`); + await page.keyboard.press('ControlOrMeta+Shift+PageUp'); await page.waitForTimeout(500); // Verify order of tabs after move @@ -146,7 +144,7 @@ test.describe('Move tabs', () => { // Move the request tab back to its original position using keyboard shortcut await source.click(); - await page.keyboard.press(`${modifier}+BracketRight`); + await page.keyboard.press('ControlOrMeta+Shift+PageDown'); await page.waitForTimeout(500); // Verify order of tabs after move diff --git a/tests/request/copy-request/keyboard-shortcuts.spec.ts b/tests/request/copy-request/keyboard-shortcuts.spec.ts new file mode 100644 index 000000000..cff366254 --- /dev/null +++ b/tests/request/copy-request/keyboard-shortcuts.spec.ts @@ -0,0 +1,93 @@ +import { test, expect } from '../../../playwright'; +import { closeAllCollections, createCollection } from '../../utils/page'; + +test.describe('Copy and Paste with Keyboard Shortcuts', () => { + test.afterAll(async ({ page }) => { + await closeAllCollections(page); + }); + + test('should copy and paste request using keyboard shortcuts', async ({ page, createTmpDir }) => { + await createCollection(page, 'keyboard-test', await createTmpDir('keyboard-test')); + const collection = page.locator('.collection-name').filter({ hasText: 'keyboard-test' }); + + // Create a request + await collection.hover(); + await collection.locator('.collection-actions .icon').click(); + await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click(); + await page.getByPlaceholder('Request Name').fill('test-request'); + await page.locator('#new-request-url .CodeMirror').click(); + await page.locator('textarea').fill('https://echo.usebruno.com'); + await page.getByRole('button', { name: 'Create' }).click(); + + const requestItem = page.locator('.collection-item-name').filter({ hasText: 'test-request' }); + await expect(requestItem).toBeVisible(); + + // Focus the request item + await requestItem.click(); + await requestItem.focus(); + + // Wait for keyboard focus indicator + await expect(requestItem).toHaveClass(/item-keyboard-focused/); + + // Use Cmd+C on Mac, Ctrl+C on Windows/Linux + const modifier = process.platform === 'darwin' ? 'Meta' : 'Control'; + await page.keyboard.press(`${modifier}+KeyC`); + + // Verify copy success (toast message) + await expect(page.getByText(/Request copied/i).first()).toBeVisible(); + + // Focus the collection to paste + await collection.click(); + await collection.focus(); + + // Use Cmd+V on Mac, Ctrl+V on Windows/Linux + await page.keyboard.press(`${modifier}+KeyV`); + + // Verify paste success + await expect(page.getByText(/pasted successfully/i).first()).toBeVisible(); + + // Verify the pasted request appears + await expect(page.locator('.collection-item-name').filter({ hasText: 'test-request' })).toHaveCount(2); + }); + + test('should copy and paste folder using keyboard shortcuts', async ({ page }) => { + const collection = page.locator('.collection-name').filter({ hasText: 'keyboard-test' }); + + // Create a folder + await collection.hover(); + await collection.locator('.collection-actions .icon').click(); + await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click(); + await page.locator('#folder-name').fill('test-folder'); + await page.getByRole('button', { name: 'Create' }).click(); + + const folder = page.locator('.collection-item-name').filter({ hasText: 'test-folder' }); + await expect(folder).toBeVisible(); + + // Focus the folder + await folder.click(); + await folder.focus(); + + // Wait for keyboard focus indicator + await expect(folder).toHaveClass(/item-keyboard-focused/); + + // Use keyboard shortcut to copy + const modifier = process.platform === 'darwin' ? 'Meta' : 'Control'; + await page.keyboard.press(`${modifier}+KeyC`); + + // Verify copy success + await expect(page.getByText(/Folder copied/i).first()).toBeVisible(); + + // Focus the collection to paste + await collection.click(); + await collection.focus(); + + // Use keyboard shortcut to paste + await page.keyboard.press(`${modifier}+KeyV`); + + // Verify paste success + await expect(page.getByText(/pasted successfully/i).first()).toBeVisible(); + + // Verify the pasted folder appears + await expect(page.locator('.collection-item-name').filter({ hasText: 'test-folder' })).toHaveCount(2); + }); +}); diff --git a/tests/shortcuts/preference-shortcuts-edit.spec.js b/tests/shortcuts/preference-shortcuts-edit.spec.js deleted file mode 100644 index 574f192a0..000000000 --- a/tests/shortcuts/preference-shortcuts-edit.spec.js +++ /dev/null @@ -1,2140 +0,0 @@ -import { test, expect } from '../../playwright'; -import { createCollection, createRequest, createFolder, openRequest, closeAllCollections } from '../utils/page'; - -test.describe('Preferences - Keybindings Editor', () => { - const modifier = process.platform === 'darwin' ? 'Meta' : 'Control'; - const modifierName = process.platform === 'darwin' ? 'command' : 'ctrl'; - - test.beforeEach(async ({ page }) => { - // Wait for app to be fully loaded - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 5000 }); - }); - - const openKeybindingsTab = async (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(); - }; - - const getInput = (page, action) => page.getByTestId(`keybinding-input-${action}`); - const getResetBtn = (page, action) => page.getByTestId(`keybinding-reset-${action}`); - const getEditBtn = (page, action) => page.getByTestId(`keybinding-edit-${action}`); - const getError = (page, action) => page.getByTestId(`keybinding-error-${action}`); - - test('should open Preferences tab', async ({ 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(); - }); - - test.describe('FUNCTIONAL: Global Search', () => { - test('should open global search using default Cmd/Ctrl+K', async ({ page, createTmpDir }) => { - // Close all collections first for clean state - await closeAllCollections(page); - - const collectionPath = await createTmpDir('global-search-default'); - await createCollection(page, 'test-collection-global-search-default', collectionPath); - - // Create a request - await createRequest(page, 'request-1', 'test-collection-global-search-default'); - - // Wait for collection to be ready - await page.waitForTimeout(500); - - // Press Cmd/Ctrl+K to open global search - await page.keyboard.press(`${modifier}+KeyK`); - await page.waitForTimeout(500); - - // Verify global search modal is visible - const searchModal = page.locator('.command-k-modal'); - await expect(searchModal).toBeVisible(); - - // Close the modal - await page.keyboard.press('Escape'); - await page.waitForTimeout(300); - - // Verify modal is closed - await expect(searchModal).not.toBeVisible(); - }); - - test('should open global search using customized-1 Cmd/Ctrl+Shift+G', async ({ page, createTmpDir }) => { - // Close all collections, tabs, and preferences first - await closeAllCollections(page); - - // Close any open preference tabs - const preferenceTabs = page.locator('.request-tab').filter({ hasText: 'Preferences' }); - const prefTabCount = await preferenceTabs.count(); - for (let i = 0; i < prefTabCount; i++) { - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - } - - // Open Keybindings preferences and customize globalSearch FIRST - await openKeybindingsTab(page); - - const row = page.getByTestId('keybinding-row-globalSearch'); - await row.hover(); - - // Start recording - await getEditBtn(page, 'globalSearch').click(); - await page.waitForTimeout(300); - - // Press new combo: Cmd/Ctrl+Shift+G (G is not used in any existing keybinding) - // Use down/up to ensure proper key recording - await page.keyboard.down(modifier); - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyG'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyG'); - await page.keyboard.up('Shift'); - await page.keyboard.up(modifier); - await page.waitForTimeout(200); - - // Verify the keybinding was saved - const input = getInput(page, 'globalSearch'); - const newValue = await input.inputValue(); - expect(newValue).toContain('shift'); - expect(newValue).toContain('g'); - - // Close preferences - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - - // Now create collection and request - const collectionPath = await createTmpDir('global-search-customized-1'); - await createCollection(page, 'test-collection-global-search-customized-1', collectionPath); - - // Create a request - await createRequest(page, 'request-1', 'test-collection-global-search-customized-1'); - - // Wait for collection to be ready - await page.waitForTimeout(500); - - // Press Cmd/Ctrl+Shift+G to open global search - await page.keyboard.press(`${modifier}+Shift+KeyG`); - await page.waitForTimeout(500); - - // Verify global search modal is visible - const searchModal = page.locator('.command-k-modal'); - await expect(searchModal).toBeVisible(); - - // Close the modal - await page.keyboard.press('Escape'); - await page.waitForTimeout(300); - - // Verify modal is closed - await expect(searchModal).not.toBeVisible(); - }); - }); - - test.describe('FUNCTIONAL: Sidebar Search', () => { - test('should open sidebar search using default Cmd/Ctrl+F', async ({ page, createTmpDir }) => { - const collectionPath = await createTmpDir('sidebar-search-default'); - await createCollection(page, 'test-collection-sidebar-search-default', collectionPath); - - // Create a request - await createRequest(page, 'request-1', 'test-collection-sidebar-search-default'); - - // Wait for collection to be ready - await page.waitForTimeout(500); - - // Press Cmd/Ctrl+F to open sidebar search - await page.keyboard.press(`${modifier}+KeyF`); - await page.waitForTimeout(500); - - // // Verify sidebar search appears (look for search input in sidebar) - // await expect(page.get('Search requests...').toBeVisible()); - await page.locator('body').click({ position: { x: 1, y: 1 } }); - - // Clear the search - await page.keyboard.press(`${modifier}+KeyF`); - await page.waitForTimeout(300); - }); - - test('should open sidebar search using customized-1 Cmd/Ctrl+Shift+H', async ({ page, createTmpDir }) => { - // Close all collections, tabs, and preferences first - await closeAllCollections(page); - - // Close any open preference tabs - const preferenceTabs = page.locator('.request-tab').filter({ hasText: 'Preferences' }); - const prefTabCount = await preferenceTabs.count(); - for (let i = 0; i < prefTabCount; i++) { - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - } - - // Open Keybindings preferences and customize sidebarSearch FIRST - await openKeybindingsTab(page); - - const row = page.getByTestId('keybinding-row-sidebarSearch'); - await row.hover(); - - // Start recording - await getEditBtn(page, 'sidebarSearch').click(); - await page.waitForTimeout(300); - - // Press new combo: Cmd/Ctrl+Shift+H (H is not used in any existing keybinding) - // Use down/up to ensure proper key recording - await page.keyboard.down(modifier); - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyH'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyH'); - await page.keyboard.up('Shift'); - await page.keyboard.up(modifier); - await page.waitForTimeout(200); - - // Verify the keybinding was saved - const input = getInput(page, 'sidebarSearch'); - const newValue = await input.inputValue(); - expect(newValue).toContain('shift'); - expect(newValue).toContain('h'); - - // Close preferences - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - - // Now create collection and request - const collectionPath = await createTmpDir('sidebar-search-customized-1'); - await createCollection(page, 'test-collection-sidebar-search-customized-1', collectionPath); - - // Create a request - await createRequest(page, 'request-1', 'test-collection-sidebar-search-customized-1'); - - // Wait for collection to be ready - await page.waitForTimeout(500); - - // Press Cmd/Ctrl+Shift+H to open sidebar search - await page.keyboard.press(`${modifier}+Shift+KeyH`); - await page.waitForTimeout(500); - - // Verify sidebar search input is visible and focused - // const searchInput = page.locator('#sidebar-search-input'); - - // Clear the search - await page.locator('body').click({ position: { x: 1, y: 1 } }); - - // Verify sidebar search appears (look for search input in sidebar) - await page.keyboard.press(`${modifier}+Shift+KeyH`); - await page.waitForTimeout(300); - }); - }); - - test.describe('FUNCTIONAL: Import Collection', () => { - test('should open import collection modal using default Cmd/Ctrl+O', async ({ page, createTmpDir }) => { - const collectionPath = await createTmpDir('import-collection-default'); - await createCollection(page, 'test-collection-import-default', collectionPath); - - // Wait for collection to be ready - await page.waitForTimeout(500); - - // Press Cmd/Ctrl+O to open import collection modal - await page.keyboard.press(`${modifier}+KeyO`); - await page.waitForTimeout(500); - - // Verify import collection modal is visible - const importModal = page.locator('.bruno-modal-card').filter({ hasText: 'Import Collection' }); - await expect(importModal).toBeVisible(); - - // Close the modal - await page.keyboard.press('Escape'); - await page.waitForTimeout(300); - - // Verify modal is closed - await expect(importModal).not.toBeVisible(); - }); - - test('should open import collection modal using customized-1 Cmd/Ctrl+Shift+I', async ({ page, createTmpDir }) => { - // Close all collections, tabs, and preferences first - await closeAllCollections(page); - - // Close any open preference tabs - const preferenceTabs = page.locator('.request-tab').filter({ hasText: 'Preferences' }); - const prefTabCount = await preferenceTabs.count(); - for (let i = 0; i < prefTabCount; i++) { - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - } - - // Open Keybindings preferences and customize importCollection FIRST - await openKeybindingsTab(page); - - const row = page.getByTestId('keybinding-row-importCollection'); - await row.hover(); - - // Start recording - await getEditBtn(page, 'importCollection').click(); - await page.waitForTimeout(300); - - // Press new combo: Cmd/Ctrl+Shift+I (I is not used in any existing keybinding) - // Use down/up to ensure proper key recording - await page.keyboard.down(modifier); - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyI'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyI'); - await page.keyboard.up('Shift'); - await page.keyboard.up(modifier); - await page.waitForTimeout(200); - - // Verify the keybinding was saved - const input = getInput(page, 'importCollection'); - const newValue = await input.inputValue(); - expect(newValue).toContain('shift'); - expect(newValue).toContain('i'); - - // Close preferences - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - - // Now create collection - const collectionPath = await createTmpDir('import-collection-customized-1'); - await createCollection(page, 'test-collection-import-customized-1', collectionPath); - - // Wait for collection to be ready - await page.waitForTimeout(500); - - // Press Cmd/Ctrl+Shift+I to open import collection modal - await page.keyboard.press(`${modifier}+Shift+KeyI`); - await page.waitForTimeout(500); - - // Verify import collection modal is visible - const importModal = page.locator('.bruno-modal-card').filter({ hasText: 'Import Collection' }); - await expect(importModal).toBeVisible(); - - // Close the modal - await page.keyboard.press('Escape'); - await page.waitForTimeout(300); - - // Verify modal is closed - await expect(importModal).not.toBeVisible(); - }); - }); - - test.describe('FUNCTIONAL: Change Layout', () => { - test('should change layout orientation using default Cmd/Ctrl+J', async ({ page, createTmpDir }) => { - const collectionPath = await createTmpDir('change-layout-default'); - await createCollection(page, 'test-collection-layout-default', collectionPath); - - // Create and open a request - await createRequest(page, 'request-1', 'test-collection-layout-default'); - await openRequest(page, 'test-collection-layout-default', 'request-1'); - - // Press Cmd/Ctrl+J to change layout - await page.keyboard.press(`${modifier}+KeyJ`); - await page.waitForTimeout(500); - - 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 page.waitForTimeout(500); - - await expect( - page.getByTestId('response-layout-toggle-btn') - ).toHaveAttribute('title', 'Switch to vertical layout'); - }); - - test('should change layout orientation using customized-1 Cmd/Ctrl+Shift+Y', async ({ page, createTmpDir }) => { - // Close all collections, tabs, and preferences first - await closeAllCollections(page); - - // Close any open preference tabs - const preferenceTabs = page.locator('.request-tab').filter({ hasText: 'Preferences' }); - const prefTabCount = await preferenceTabs.count(); - for (let i = 0; i < prefTabCount; i++) { - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - } - - // Open Keybindings preferences and customize changeLayout FIRST - await openKeybindingsTab(page); - - const row = page.getByTestId('keybinding-row-changeLayout'); - await row.hover(); - - // Start recording - await getEditBtn(page, 'changeLayout').click(); - await page.waitForTimeout(300); - - // Press new combo: Cmd/Ctrl+Shift+Y (Y is not used in any existing keybinding) - // Use down/up to ensure proper key recording - await page.keyboard.down(modifier); - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyY'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyY'); - await page.keyboard.up('Shift'); - await page.keyboard.up(modifier); - await page.waitForTimeout(200); - - // Verify the keybinding was saved - const input = getInput(page, 'changeLayout'); - const newValue = await input.inputValue(); - expect(newValue).toContain('shift'); - expect(newValue).toContain('y'); - - // Close preferences - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(500); - - // Now create collection and request - const collectionPath = await createTmpDir('change-layout-customized-1'); - await createCollection(page, 'test-collection-layout-customized-1', collectionPath); - - // Create and open a request - await createRequest(page, 'request-1', 'test-collection-layout-customized-1'); - await openRequest(page, 'test-collection-layout-customized-1', 'request-1'); - - // Press Cmd/Ctrl+Shift+J to change layout - await page.keyboard.press(`${modifier}+Shift+KeyY`); - await page.waitForTimeout(500); - - await expect( - page.getByTestId('response-layout-toggle-btn') - ).toHaveAttribute('title', 'Switch to horizontal layout'); - - // Press Cmd/Ctrl+Shift+J to change layout - await page.keyboard.press(`${modifier}+Shift+KeyY`); - await page.waitForTimeout(500); - - await expect( - page.getByTestId('response-layout-toggle-btn') - ).toHaveAttribute('title', 'Switch to vertical layout'); - }); - }); - - test.describe('FUNCTIONAL: Collapse Sidebar', () => { - test('should collapse/expand sidebar using default Cmd/Ctrl+\\', async ({ page, createTmpDir }) => { - const collectionPath = await createTmpDir('collapse-sidebar-default'); - await createCollection(page, 'test-collection-sidebar-default', collectionPath); - - // Wait for collection to be ready - await page.waitForTimeout(500); - - // // Get initial sidebar state - const width = await page.locator('aside.sidebar').evaluate((el) => - getComputedStyle(el).width - ); - - expect(width).not.toBe('0px'); - - await expect(page.getByTestId('collections')).toBeVisible(); - - // Press Cmd/Ctrl+\ to collapse sidebar - await page.keyboard.press(`${modifier}+Backslash`); - await page.waitForTimeout(500); - - // await expect(page.getByTestId('collections')).not.toBeVisible(); - - const width2 = await page.locator('aside.sidebar').evaluate((el) => - getComputedStyle(el).width - ); - - expect(width2).toBe('0px'); - - // Press again to expand - await page.keyboard.press(`${modifier}+Backslash`); - await page.waitForTimeout(500); - }); - - test('should collapse/expand sidebar using customized-1 Cmd/Ctrl+Shift+B', async ({ page, createTmpDir }) => { - // Close all collections, tabs, and preferences first - await closeAllCollections(page); - - // Close any open preference tabs - const preferenceTabs = page.locator('.request-tab').filter({ hasText: 'Preferences' }); - const prefTabCount = await preferenceTabs.count(); - for (let i = 0; i < prefTabCount; i++) { - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - } - - // Open Keybindings preferences and customize collapseSidebar FIRST - await openKeybindingsTab(page); - - const row = page.getByTestId('keybinding-row-collapseSidebar'); - await row.hover(); - - // Start recording - await getEditBtn(page, 'collapseSidebar').click(); - await page.waitForTimeout(300); - - // Press new combo: Cmd/Ctrl+Shift+B (B is not used in any existing keybinding) - // Use down/up to ensure proper key recording - await page.keyboard.down(modifier); - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyB'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyB'); - await page.keyboard.up('Shift'); - await page.keyboard.up(modifier); - await page.waitForTimeout(200); - - // Verify the keybinding was saved - const input = getInput(page, 'collapseSidebar'); - const newValue = await input.inputValue(); - expect(newValue).toContain('shift'); - expect(newValue).toContain('b'); - - // Close preferences - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - - // Now create collection - const collectionPath = await createTmpDir('collapse-sidebar-customized-1'); - await createCollection(page, 'test-collection-sidebar-customized-1', collectionPath); - - // Wait for collection to be ready - await page.waitForTimeout(500); - - // // Get initial sidebar state - const width = await page.locator('aside.sidebar').evaluate((el) => - getComputedStyle(el).width - ); - - expect(width).not.toBe('0px'); - - await expect(page.getByTestId('collections')).toBeVisible(); - - // Press Cmd/Ctrl+\ to collapse sidebar - await page.keyboard.press(`${modifier}+Shift+KeyB`); - await page.waitForTimeout(500); - - // await expect(page.getByTestId('collections')).not.toBeVisible(); - - const width2 = await page.locator('aside.sidebar').evaluate((el) => - getComputedStyle(el).width - ); - - expect(width2).toBe('0px'); - - // Press again to expand - await page.keyboard.press(`${modifier}+Shift+KeyB`); - await page.waitForTimeout(500); - }); - }); - - test.describe('FUNCTIONAL: Close Tab', () => { - test('should close single tab using default Cmd/Ctrl+W', async ({ page, createTmpDir }) => { - // Close all collections first for clean state - await closeAllCollections(page); - - const collectionPath = await createTmpDir('close-tab-default'); - await createCollection(page, 'test-collection-close-tab-default', collectionPath); - - // Create multiple requests - await createRequest(page, 'request-1', 'test-collection-close-tab-default'); - await createRequest(page, 'request-2', 'test-collection-close-tab-default'); - await createRequest(page, 'request-3', 'test-collection-close-tab-default'); - - // Open and pin all requests - await openRequest(page, 'test-collection-close-tab-default', 'request-1', { persist: true }); - await openRequest(page, 'test-collection-close-tab-default', 'request-2', { persist: true }); - await openRequest(page, 'test-collection-close-tab-default', 'request-3', { persist: true }); - - // Wait for tabs to be ready - await page.waitForTimeout(500); - - // Verify all 3 tabs are open - const tabs = page.locator('.request-tab'); - await expect(tabs).toHaveCount(3); - - // Verify request-3 is active - const activeTab = page.locator('li.request-tab.active'); - await expect(activeTab).toHaveText(/request-3/); - - // Press Cmd/Ctrl+W to close active tab - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(500); - - // Verify only 2 tabs remain and request-3 is closed - await expect(tabs).toHaveCount(2); - await expect(page.locator('.request-tab').filter({ hasText: 'request-3' })).not.toBeVisible(); - - // Verify request-2 is now active - await expect(activeTab).toHaveText(/request-2/); - - // Close another tab - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(500); - - // Verify only 1 tab remains - await expect(tabs).toHaveCount(1); - await expect(page.locator('.request-tab').filter({ hasText: 'request-2' })).not.toBeVisible(); - - // Verify request-1 is now active - await expect(activeTab).toHaveText(/request-1/); - }); - - test('should close single tab using customized Alt+T+C', async ({ page, createTmpDir }) => { - // Close all collections first - await closeAllCollections(page); - - // Open Keybindings preferences and customize closeTab FIRST - await openKeybindingsTab(page); - - const row = page.getByTestId('keybinding-row-closeTab'); - await row.hover(); - - // Start recording - await getEditBtn(page, 'closeTab').click(); - await page.waitForTimeout(300); - - // Press new combo: Alt+T+C - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyT'); - await page.keyboard.down('KeyC'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyC'); - await page.keyboard.up('KeyT'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(1000); - - // Verify the keybinding was saved - const input = getInput(page, 'closeTab'); - const newValue = await input.inputValue(); - expect(newValue).toContain('alt'); - expect(newValue).toContain('t'); - expect(newValue).toContain('c'); - - await page.keyboard.press(`${modifier}+KeyT+KeyC`); - await page.waitForTimeout(500); - - // Now create collection and requests - const collectionPath = await createTmpDir('close-tab-customized'); - await createCollection(page, 'test-collection-close-tab-customized', collectionPath); - - // Create multiple requests - await createRequest(page, 'request-1', 'test-collection-close-tab-customized'); - await createRequest(page, 'request-2', 'test-collection-close-tab-customized'); - await createRequest(page, 'request-3', 'test-collection-close-tab-customized'); - - // Open and pin all requests - await openRequest(page, 'test-collection-close-tab-customized', 'request-1', { persist: true }); - await openRequest(page, 'test-collection-close-tab-customized', 'request-2', { persist: true }); - await openRequest(page, 'test-collection-close-tab-customized', 'request-3', { persist: true }); - - // Wait for tabs to be ready - await page.waitForTimeout(500); - - // Verify all 3 tabs are open - const tabs = page.locator('.request-tab'); - await expect(tabs).toHaveCount(3); - - // Verify request-3 is active - const activeTab = page.locator('li.request-tab.active'); - await expect(activeTab).toHaveText(/request-3/); - - // Press Alt+T+C to close active tab - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyT'); - await page.keyboard.down('KeyC'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyC'); - await page.keyboard.up('KeyT'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(500); - - // Verify only 2 tabs remain and request-3 is closed - await expect(tabs).toHaveCount(2); - await expect(page.locator('.request-tab').filter({ hasText: 'request-3' })).not.toBeVisible(); - - // Verify request-2 is now active - await expect(activeTab).toHaveText(/request-2/); - - // Close another tab - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyT'); - await page.keyboard.down('KeyC'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyC'); - await page.keyboard.up('KeyT'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(500); - - // Verify only 1 tab remains - await expect(tabs).toHaveCount(1); - await expect(page.locator('.request-tab').filter({ hasText: 'request-2' })).not.toBeVisible(); - - // Verify request-1 is now active - await expect(activeTab).toHaveText(/request-1/); - }); - }); - - test.describe('FUNCTIONAL: Close All Tabs', () => { - test('should close all tabs using default Cmd/Ctrl+Shift+W', async ({ page, createTmpDir }) => { - const collectionPath = await createTmpDir('close-all-tabs-default'); - await createCollection(page, 'test-collection-close-all-default', collectionPath); - - // Create multiple requests - await createRequest(page, 'request-1', 'test-collection-close-all-default'); - await createRequest(page, 'request-2', 'test-collection-close-all-default'); - await createRequest(page, 'request-3', 'test-collection-close-all-default'); - - // Open and pin all requests - await openRequest(page, 'test-collection-close-all-default', 'request-1', { persist: true }); - await openRequest(page, 'test-collection-close-all-default', 'request-2', { persist: true }); - await openRequest(page, 'test-collection-close-all-default', 'request-3', { persist: true }); - - // Wait for tabs to be ready - await page.waitForTimeout(500); - - // Verify all 3 tabs are open - await expect(page.locator('.request-tab')).toHaveCount(3); // 'request-1/2/3' - - // Press Cmd/Ctrl+Shift+W to close all tabs - await page.keyboard.press(`${modifier}+Shift+KeyW`); - await page.waitForTimeout(500); - - // Verify all tabs are closed - await expect(page.locator('.request-tab')).toHaveCount(2); // Overview / Global Environments - }); - - test('should close all tabs using customized-1 Alt+W+A', async ({ page, createTmpDir }) => { - // Close all collections, tabs, and preferences first - await closeAllCollections(page); - - // Close any open preference tabs - const preferenceTabs = page.locator('.request-tab').filter({ hasText: 'Preferences' }); - const prefTabCount = await preferenceTabs.count(); - for (let i = 0; i < prefTabCount; i++) { - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - } - - // Open Keybindings preferences and customize closeAllTabs FIRST - await openKeybindingsTab(page); - - const row = page.getByTestId('keybinding-row-closeAllTabs'); - await row.hover(); - - // Start recording - await getEditBtn(page, 'closeAllTabs').click(); - await page.waitForTimeout(300); - - // Press new combo: Alt+W+A (multi-key sequence) - // Use down/up to ensure proper key recording - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyW'); - await page.keyboard.down('KeyA'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyA'); - await page.keyboard.up('KeyW'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(500); - - // Verify the keybinding was saved - const input = getInput(page, 'closeAllTabs'); - const newValue = await input.inputValue(); - expect(newValue).toContain('alt'); - expect(newValue).toContain('w'); - expect(newValue).toContain('a'); - - // Close preferences - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(500); - - // Now create collection and requests - const collectionPath = await createTmpDir('close-all-tabs-customized-1'); - await createCollection(page, 'test-collection-close-all-customized-1', collectionPath); - - // Create multiple requests - await createRequest(page, 'request-1', 'test-collection-close-all-customized-1'); - await createRequest(page, 'request-2', 'test-collection-close-all-customized-1'); - await createRequest(page, 'request-3', 'test-collection-close-all-customized-1'); - - // Open and pin all requests - await openRequest(page, 'test-collection-close-all-customized-1', 'request-1', { persist: true }); - await openRequest(page, 'test-collection-close-all-customized-1', 'request-2', { persist: true }); - await openRequest(page, 'test-collection-close-all-customized-1', 'request-3', { persist: true }); - - // Wait for tabs to be ready - await page.waitForTimeout(500); - - // Verify all tabs are closed - await expect(page.locator('.request-tab')).toHaveCount(3); // Overview / Global Environments / Preferences - }); - }); - - test.describe('FUNCTIONAL: Switch to Previous Tab', () => { - test('should switch to previous tab using default Cmd/Ctrl+2', async ({ page, createTmpDir }) => { - // Close all collections first for clean state - await closeAllCollections(page); - - const collectionPath = await createTmpDir('switch-tabs-default'); - await createCollection(page, 'test-collection-switching-default', collectionPath); - - // Create multiple requests - await createRequest(page, 'request-1', 'test-collection-switching-default'); - await createRequest(page, 'request-2', 'test-collection-switching-default'); - await createRequest(page, 'request-3', 'test-collection-switching-default'); - - // Open and pin all requests (persist: true means double-click to pin) - await openRequest(page, 'test-collection-switching-default', 'request-1', { persist: true }); - await openRequest(page, 'test-collection-switching-default', 'request-2', { persist: true }); - await openRequest(page, 'test-collection-switching-default', 'request-3', { persist: true }); - - // Wait for tabs to be ready - request-3 should be active - await page.waitForTimeout(500); - - // Click on request-1 to make it active - await page.locator('.request-tab').filter({ hasText: 'request-3' }).click(); - await page.waitForTimeout(300); - - // Press Cmd/Ctrl+1 to switch to next tab - await page.keyboard.press(`${modifier}+Digit2`); - await page.waitForTimeout(500); - - // Verify next tab is active (request-2) - const activeTab = page.locator('li.request-tab.active'); - await expect(activeTab).toHaveText(/request-2/); - - // Press again - await page.keyboard.press(`${modifier}+Digit2`); - await page.waitForTimeout(500); - - // Verify next tab is active (request-3) - await expect(activeTab).toHaveText(/request-1/); - }); - - test('should switch to previous tab using customized-1 Cmd/Ctrl+Shift+P', async ({ page, createTmpDir }) => { - // Close all collections, tabs, and preferences first - await closeAllCollections(page); - - // Close any open preference tabs - const preferenceTabs = page.locator('.request-tab').filter({ hasText: 'Preferences' }); - const prefTabCount = await preferenceTabs.count(); - for (let i = 0; i < prefTabCount; i++) { - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - } - - // Open Keybindings preferences and customize switchToPreviousTab FIRST - await openKeybindingsTab(page); - - const row = page.getByTestId('keybinding-row-switchToPreviousTab'); - await row.hover(); - - // Start recording - await getEditBtn(page, 'switchToPreviousTab').click(); - await page.waitForTimeout(300); - - // Press new combo: Cmd/Ctrl+Shift+P (P is not used in any existing keybinding) - // Use down/up to ensure proper key recording - await page.keyboard.down(modifier); - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyP'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyP'); - await page.keyboard.up('Shift'); - await page.keyboard.up(modifier); - await page.waitForTimeout(200); - - // Verify the keybinding was saved - const input = getInput(page, 'switchToPreviousTab'); - const newValue = await input.inputValue(); - expect(newValue).toContain('shift'); - expect(newValue).toContain('p'); - - // Close preferences - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - - // Now create collection and requests - const collectionPath = await createTmpDir('switch-tabs-customized-1'); - await createCollection(page, 'test-collection-switching-customized-1', collectionPath); - - // Create multiple requests - await createRequest(page, 'request-1', 'test-collection-switching-customized-1'); - await createRequest(page, 'request-2', 'test-collection-switching-customized-1'); - await createRequest(page, 'request-3', 'test-collection-switching-customized-1'); - - // Open and pin all requests (persist: true means double-click to pin) - await openRequest(page, 'test-collection-switching-customized-1', 'request-1', { persist: true }); - await openRequest(page, 'test-collection-switching-customized-1', 'request-2', { persist: true }); - await openRequest(page, 'test-collection-switching-customized-1', 'request-3', { persist: true }); - - // Wait for tabs to be ready - await page.waitForTimeout(500); - - // Press Cmd/Ctrl+2 to switch to previous tab - await page.keyboard.press(`${modifier}+Shift+KeyP`); - await page.waitForTimeout(500); - - // Verify previous tab is active (request-2) - const activeTab = page.locator('li.request-tab.active'); - await expect(activeTab).toHaveText(/request-2/); - - // Press again - await page.keyboard.press(`${modifier}+Shift+KeyP`); - await page.waitForTimeout(200); - - // Verify previous tab is active (request-1) - await expect(activeTab).toHaveText(/request-1/); - }); - }); - - test.describe('FUNCTIONAL: Switch to Next Tab', () => { - test('should switch to next tab using default Cmd/Ctrl+1', async ({ page, createTmpDir }) => { - // Close all collections first for clean state - await closeAllCollections(page); - - const collectionPath = await createTmpDir('switch-tabs-next-default'); - await createCollection(page, 'test-collection-switching-next-default', collectionPath); - - // Create multiple requests - await createRequest(page, 'request-1', 'test-collection-switching-next-default'); - await createRequest(page, 'request-2', 'test-collection-switching-next-default'); - await createRequest(page, 'request-3', 'test-collection-switching-next-default'); - - // Open and pin all requests (persist: true means double-click to pin) - await openRequest(page, 'test-collection-switching-next-default', 'request-1', { persist: true }); - await openRequest(page, 'test-collection-switching-next-default', 'request-2', { persist: true }); - await openRequest(page, 'test-collection-switching-next-default', 'request-3', { persist: true }); - - // Wait for tabs to be ready - request-3 should be active - await page.waitForTimeout(500); - - // Click on request-1 to make it active - await page.locator('.request-tab').filter({ hasText: 'request-1' }).click(); - await page.waitForTimeout(300); - - // Press Cmd/Ctrl+1 to switch to next tab - await page.keyboard.press(`${modifier}+Digit1`); - await page.waitForTimeout(500); - - // Verify next tab is active (request-2) - const activeTab = page.locator('li.request-tab.active'); - await expect(activeTab).toHaveText(/request-2/); - - // Press again - await page.keyboard.press(`${modifier}+Digit1`); - await page.waitForTimeout(500); - - // Verify next tab is active (request-3) - await expect(activeTab).toHaveText(/request-3/); - }); - - test('should switch to next tab using customized-1 Cmd/Ctrl+Shift+L', async ({ page, createTmpDir }) => { - // Close all collections, tabs, and preferences first - await closeAllCollections(page); - - // Close any open preference tabs - const preferenceTabs = page.locator('.request-tab').filter({ hasText: 'Preferences' }); - const prefTabCount = await preferenceTabs.count(); - for (let i = 0; i < prefTabCount; i++) { - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - } - - // Open Keybindings preferences and customize switchToNextTab FIRST - await openKeybindingsTab(page); - - const row = page.getByTestId('keybinding-row-switchToNextTab'); - await row.hover(); - - // Start recording - await getEditBtn(page, 'switchToNextTab').click(); - await page.waitForTimeout(300); - - // Press new combo: Cmd/Ctrl+Shift+L (L is not used in any existing keybinding) - // Use down/up to ensure proper key recording - await page.keyboard.down(modifier); - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyL'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyL'); - await page.keyboard.up('Shift'); - await page.keyboard.up(modifier); - await page.waitForTimeout(200); - - // Verify the keybinding was saved - const input = getInput(page, 'switchToNextTab'); - const newValue = await input.inputValue(); - expect(newValue).toContain('shift'); - expect(newValue).toContain('l'); - - // Close preferences - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - - // Now create collection and requests - const collectionPath = await createTmpDir('switch-tabs-next-customized-1'); - await createCollection(page, 'test-collection-switching-next-customized-1', collectionPath); - - // Create multiple requests - await createRequest(page, 'request-1', 'test-collection-switching-next-customized-1'); - await createRequest(page, 'request-2', 'test-collection-switching-next-customized-1'); - await createRequest(page, 'request-3', 'test-collection-switching-next-customized-1'); - - // Open and pin all requests (persist: true means double-click to pin) - await openRequest(page, 'test-collection-switching-next-customized-1', 'request-1', { persist: true }); - await openRequest(page, 'test-collection-switching-next-customized-1', 'request-2', { persist: true }); - await openRequest(page, 'test-collection-switching-next-customized-1', 'request-3', { persist: true }); - - // Wait for tabs to be ready - request-3 should be active - await page.waitForTimeout(500); - - // Click on request-1 to make it active - await page.locator('.request-tab').filter({ hasText: 'request-1' }).click(); - await page.waitForTimeout(300); - - // Press Cmd/Ctrl+Shift+L to switch to next tab - await page.keyboard.press(`${modifier}+Shift+KeyL`); - await page.waitForTimeout(500); - - // Verify next tab is active (request-2) - const activeTab = page.locator('li.request-tab.active'); - await expect(activeTab).toHaveText(/request-2/); - - // Press again - await page.keyboard.press(`${modifier}+Shift+KeyL`); - await page.waitForTimeout(200); - - // Verify next tab is active (request-3) - await expect(activeTab).toHaveText(/request-3/); - }); - }); - - test.describe('FUNCTIONAL: Edit Environment', () => { - test('should open environment editor using default Cmd/Ctrl+E', async ({ page, createTmpDir }) => { - const collectionPath = await createTmpDir('edit-environment-default'); - await createCollection(page, 'test-collection-environment-default', collectionPath); - - // Wait for collection to be ready - await page.waitForTimeout(500); - - // Press Cmd/Ctrl+E to open environment editor - await page.keyboard.press(`${modifier}+KeyE`); - await page.waitForTimeout(500); - - // Verify environment editor tab is visible - const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' }); - await expect(envTab).toBeVisible(); - }); - - test('should open environment editor using customized-1 Alt+E+G', async ({ page, createTmpDir }) => { - // Close all collections, tabs, and preferences first - await closeAllCollections(page); - - // Close any open preference tabs - const preferenceTabs = page.locator('.request-tab').filter({ hasText: 'Preferences' }); - const prefTabCount = await preferenceTabs.count(); - for (let i = 0; i < prefTabCount; i++) { - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - } - - // Open Keybindings preferences and customize editEnvironment FIRST - await openKeybindingsTab(page); - - const row = page.getByTestId('keybinding-row-editEnvironment'); - await row.hover(); - - // Start recording - await getEditBtn(page, 'editEnvironment').click(); - await page.waitForTimeout(300); - - // Press new combo: Alt+E+G (multi-key sequence) - // Use down/up to ensure proper key recording - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyE'); - await page.keyboard.down('KeyG'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyG'); - await page.keyboard.up('KeyE'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(500); - - // Verify the keybinding was saved - const input = getInput(page, 'editEnvironment'); - const newValue = await input.inputValue(); - expect(newValue).toContain('alt'); - expect(newValue).toContain('e'); - expect(newValue).toContain('g'); - - // Close preferences - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(500); - - // Now create collection - const collectionPath = await createTmpDir('edit-environment-customized-1'); - await createCollection(page, 'test-collection-environment-customized-1', collectionPath); - - // Wait for collection to be ready - await page.waitForTimeout(500); - - // Close the environment tab - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(300); - }); - }); - - test.describe('FUNCTIONAL: Zoom In', () => { - test('should zoom in using default Cmd/Ctrl+=', async ({ page, createTmpDir }) => { - // Close all collections first for clean state - await closeAllCollections(page); - - const collectionPath = await createTmpDir('zoom-in-default'); - await createCollection(page, 'test-collection-zoom-in-default', collectionPath); - - // Wait for collection to be ready - await page.waitForTimeout(500); - - await page.keyboard.press(`${modifier}+Shift+Equal`); - await page.waitForTimeout(500); - }); - - test('should zoom in using customized-1 Alt+Z+ArrowUp', async ({ page, createTmpDir }) => { - // Close all collections, tabs, and preferences first - await closeAllCollections(page); - - // Close any open preference tabs - const preferenceTabs = page.locator('.request-tab').filter({ hasText: 'Preferences' }); - const prefTabCount = await preferenceTabs.count(); - for (let i = 0; i < prefTabCount; i++) { - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - } - - // Open Keybindings preferences and customize zoomIn FIRST - await openKeybindingsTab(page); - - const row = page.getByTestId('keybinding-row-zoomIn'); - await row.hover(); - - // Start recording - await getEditBtn(page, 'zoomIn').click(); - await page.waitForTimeout(300); - - // Press new combo: Alt+Z+ArrowUp - // Use down/up to ensure proper key recording - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyZ'); - await page.keyboard.down('ArrowUp'); - await page.waitForTimeout(200); - await page.keyboard.up('ArrowUp'); - await page.keyboard.up('KeyZ'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(1000); - - // Verify the keybinding was saved - const input = getInput(page, 'zoomIn'); - const newValue = await input.inputValue(); - expect(newValue).toContain('alt'); - expect(newValue).toContain('z'); - expect(newValue).toContain('arrowup'); - - // Close preferences - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(1000); - - // Wait for collection to be ready - await page.waitForTimeout(500); - - // Press Alt+Z+ArrowUp to zoom in - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyZ'); - await page.keyboard.down('ArrowUp'); - await page.waitForTimeout(200); - await page.keyboard.up('ArrowUp'); - await page.keyboard.up('KeyZ'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(500); - }); - }); - - test.describe('FUNCTIONAL: Zoom Out', () => { - test('should zoom out using default Cmd/Ctrl+-', async ({ page, createTmpDir }) => { - // Close all collections first for clean state - await closeAllCollections(page); - - const collectionPath = await createTmpDir('zoom-out-default'); - await createCollection(page, 'test-collection-zoom-out-default', collectionPath); - - // Wait for collection to be ready - await page.waitForTimeout(500); - - // Press Cmd/Ctrl+- to zoom out - await page.keyboard.press(`${modifier}+Minus`); - await page.waitForTimeout(500); - }); - - test('should zoom out using customized-1 Alt+Z+ArrowDown', async ({ page, createTmpDir }) => { - // Close all collections, tabs, and preferences first - await closeAllCollections(page); - - // Close any open preference tabs - const preferenceTabs = page.locator('.request-tab').filter({ hasText: 'Preferences' }); - const prefTabCount = await preferenceTabs.count(); - for (let i = 0; i < prefTabCount; i++) { - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - } - - // Open Keybindings preferences and customize zoomOut FIRST - await openKeybindingsTab(page); - - const row = page.getByTestId('keybinding-row-zoomOut'); - await row.hover(); - - // Start recording - await getEditBtn(page, 'zoomOut').click(); - await page.waitForTimeout(300); - - // Press new combo: Alt+Z+ArrowDown - // Use down/up to ensure proper key recording - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyZ'); - await page.keyboard.down('ArrowDown'); - await page.waitForTimeout(200); - await page.keyboard.up('ArrowDown'); - await page.keyboard.up('KeyZ'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(1000); - - // Verify the keybinding was saved - const input = getInput(page, 'zoomOut'); - const newValue = await input.inputValue(); - expect(newValue).toContain('alt'); - expect(newValue).toContain('z'); - expect(newValue).toContain('arrowdown'); - - // Close preferences - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(1000); - - // Now create collection - const collectionPath = await createTmpDir('zoom-out-customized-1'); - await createCollection(page, 'test-collection-zoom-out-customized-1', collectionPath); - - // Wait for collection to be ready - await page.waitForTimeout(500); - - // Press Alt+Z+ArrowDown to zoom out - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyZ'); - await page.keyboard.down('ArrowDown'); - await page.waitForTimeout(200); - await page.keyboard.up('ArrowDown'); - await page.keyboard.up('KeyZ'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(500); - }); - }); - - test.describe('FUNCTIONAL: Reset Zoom', () => { - test('should reset zoom using default Cmd/Ctrl+0', async ({ page, createTmpDir }) => { - // Close all collections first for clean state - await closeAllCollections(page); - - const collectionPath = await createTmpDir('reset-zoom-default'); - await createCollection(page, 'test-collection-reset-zoom-default', collectionPath); - - // Wait for collection to be ready - await page.waitForTimeout(500); - - // Zoom in first - await page.keyboard.press(`${modifier}+Shift+Equal`); - await page.waitForTimeout(300); - - // Press Cmd/Ctrl+0 to reset zoom - await page.keyboard.press(`${modifier}+Digit0`); - await page.waitForTimeout(500); - }); - - test('should reset zoom using customized-1 Alt+Z+0', async ({ page, createTmpDir }) => { - // Close all collections, tabs, and preferences first - await closeAllCollections(page); - - // Close any open preference tabs - const preferenceTabs = page.locator('.request-tab').filter({ hasText: 'Preferences' }); - const prefTabCount = await preferenceTabs.count(); - for (let i = 0; i < prefTabCount; i++) { - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - } - - // Open Keybindings preferences and customize resetZoom FIRST - await openKeybindingsTab(page); - - const row = page.getByTestId('keybinding-row-resetZoom'); - await row.hover(); - - // Start recording - await getEditBtn(page, 'resetZoom').click(); - await page.waitForTimeout(300); - - // Press new combo: Alt+Z+0 - // Use down/up to ensure proper key recording - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyZ'); - await page.keyboard.down('Digit0'); - await page.waitForTimeout(200); - await page.keyboard.up('Digit0'); - await page.keyboard.up('KeyZ'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(1000); - - // Verify the keybinding was saved - const input = getInput(page, 'resetZoom'); - const newValue = await input.inputValue(); - expect(newValue).toContain('alt'); - expect(newValue).toContain('z'); - expect(newValue).toContain('0'); - - // Close preferences - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(1000); - - // Wait for collection to be ready - await page.waitForTimeout(500); - - // Zoom in first using default shortcut - await page.keyboard.press(`${modifier}+Shift+Equal`); - await page.waitForTimeout(300); - - // Press Alt+Z+0 to reset zoom - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyZ'); - await page.keyboard.down('Digit0'); - await page.waitForTimeout(200); - await page.keyboard.up('Digit0'); - await page.keyboard.up('KeyZ'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(500); - }); - }); - - test.describe('FUNCTIONAL: Clone Item', () => { - test('should clone request using default Cmd/Ctrl+D', async ({ page, createTmpDir }) => { - // Close all collections first for clean state - await closeAllCollections(page); - - const collectionPath = await createTmpDir('clone-request-default'); - await createCollection(page, 'test-collection-clone-request-default', collectionPath); - - // Create a request - await createRequest(page, 'original-request', 'test-collection-clone-request-default'); - - // Open the request - await openRequest(page, 'test-collection-clone-request-default', 'original-request'); - await page.waitForTimeout(500); - - // Press Cmd/Ctrl+D to clone - 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({ timeout: 3000 }); - - // Fill in the clone request name - const requestNameInput = page.locator('#collection-item-name'); - await requestNameInput.fill('cloned-request'); - - // Click the clone button - await page.getByTestId('collection-item-clone').click(); - - // Wait for clone operation to complete - await page.waitForTimeout(500); - - // Verify cloned request appears in sidebar - await expect(page.locator('.collection-item-name').filter({ hasText: 'cloned-request' })).toBeVisible(); - await expect(page.locator('.collection-item-name').filter({ hasText: 'original-request' })).toBeVisible(); - }); - - test('should clone folder using default Cmd/Ctrl+D', async ({ page, createTmpDir }) => { - // Close all collections first for clean state - await closeAllCollections(page); - - // Close any open preference tabs - const preferenceTabs = page.locator('.request-tab').filter({ hasText: 'Preferences' }); - const prefTabCount = await preferenceTabs.count(); - for (let i = 0; i < prefTabCount; i++) { - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - } - - // Reset cloneItem keybinding to default Cmd/Ctrl+D first - await openKeybindingsTab(page); - - const row = page.getByTestId('keybinding-row-cloneItem'); - await row.hover(); - - // Close preferences - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(500); - - // Now create collection and folder - const collectionPath = await createTmpDir('clone-folder-default'); - await createCollection(page, 'test-collection-clone-folder-default', collectionPath); - - // Create a folder - await createFolder(page, 'original-folder', 'test-collection-clone-folder-default', true); - await page.waitForTimeout(500); - - // Open folder settings - const folder = page.locator('.collection-item-name').filter({ hasText: 'original-folder' }); - await folder.click(); - - // Verify folder settings tab is open - await expect(page.locator('.request-tab').filter({ hasText: 'original-folder' })).toBeVisible(); - await page.waitForTimeout(500); - - // Press Cmd/Ctrl+D to clone folder - 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({ timeout: 3000 }); - - // Fill in the clone folder name - const folderNameInput = page.locator('#collection-item-name'); - await folderNameInput.fill('cloned-folder'); - - // Click the clone button - await page.getByTestId('collection-item-clone').click(); - - // Wait for clone operation to complete - await page.waitForTimeout(500); - - // Verify cloned folder appears in sidebar - await expect(page.locator('.collection-item-name').filter({ hasText: 'cloned-folder' })).toBeVisible(); - await expect(page.locator('.collection-item-name').filter({ hasText: 'original-folder' })).toBeVisible(); - }); - - test('should clone request using customized Cmd/Ctrl+Shift+D', async ({ page, createTmpDir }) => { - // Close all collections, tabs, and preferences first - await closeAllCollections(page); - - // Close any open preference tabs - const preferenceTabs = page.locator('.request-tab').filter({ hasText: 'Preferences' }); - const prefTabCount = await preferenceTabs.count(); - for (let i = 0; i < prefTabCount; i++) { - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - } - - // Open Keybindings preferences and customize cloneItem FIRST - await openKeybindingsTab(page); - - const row = page.getByTestId('keybinding-row-cloneItem'); - await row.hover(); - - // Start recording - await getEditBtn(page, 'cloneItem').click(); - await page.waitForTimeout(300); - - // Press new combo: Cmd/Ctrl+Shift+D - await page.keyboard.down(modifier === 'Meta' ? 'Meta' : 'Control'); - await page.keyboard.down('Shift'); - await page.keyboard.down('KeyD'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyD'); - await page.keyboard.up('Shift'); - await page.keyboard.up(modifier === 'Meta' ? 'Meta' : 'Control'); - await page.waitForTimeout(1000); - - // Verify the keybinding was saved - const input = getInput(page, 'cloneItem'); - const newValue = await input.inputValue(); - expect(newValue).toContain('shift'); - expect(newValue).toContain('d'); - - // Close preferences - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(1000); - - // Now create collection and request - const collectionPath = await createTmpDir('clone-request-customized'); - await createCollection(page, 'test-collection-clone-request-customized', collectionPath); - - // Create a request - await createRequest(page, 'original-request', 'test-collection-clone-request-customized'); - - // Open the request - await openRequest(page, 'test-collection-clone-request-customized', 'original-request'); - await page.waitForTimeout(500); - - // Press Cmd/Ctrl+Shift+D to clone - await page.keyboard.press(`${modifier}+Shift+KeyD`); - - // Verify clone modal opens - const cloneModal = page.locator('.bruno-modal-card').filter({ hasText: /clone request/i }); - await expect(cloneModal).toBeVisible({ timeout: 3000 }); - - // Fill in the clone request name - const requestNameInput = page.locator('#collection-item-name'); - await requestNameInput.fill('cloned-request'); - - // Click the clone button - await page.getByTestId('collection-item-clone').click(); - - // Wait for clone operation to complete - await page.waitForTimeout(500); - - // Verify cloned request appears in sidebar - await expect(page.locator('.collection-item-name').filter({ hasText: 'cloned-request' })).toBeVisible(); - await expect(page.locator('.collection-item-name').filter({ hasText: 'original-request' })).toBeVisible(); - }); - - test('should clone folder using customized Cmd/Ctrl+Shift+D', async ({ page, createTmpDir }) => { - // Close all collections, tabs, and preferences first - await closeAllCollections(page); - - // Close any open preference tabs - const preferenceTabs = page.locator('.request-tab').filter({ hasText: 'Preferences' }); - const prefTabCount = await preferenceTabs.count(); - for (let i = 0; i < prefTabCount; i++) { - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - } - - // Open Keybindings preferences and customize cloneItem FIRST - await openKeybindingsTab(page); - - const row = page.getByTestId('keybinding-row-cloneItem'); - await row.hover(); - - // Verify the keybinding was saved - const input = getInput(page, 'cloneItem'); - const newValue = await input.inputValue(); - expect(newValue).toContain('shift'); - expect(newValue).toContain('d'); - - // Close preferences - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(1000); - - // Now create collection - const collectionPath = await createTmpDir('clone-folder-customized'); - await createCollection(page, 'test-collection-clone-folder-customized', collectionPath); - - // Create a folder - await createFolder(page, 'original-folder', 'test-collection-clone-folder-customized', true); - await page.waitForTimeout(500); - - // Open folder settings - const folder = page.locator('.collection-item-name').filter({ hasText: 'original-folder' }); - await folder.click(); - // await folder.locator('.menu-icon').click(); - // await page.locator('.dropdown-item').filter({ hasText: 'Settings' }).click(); - - // Verify folder settings tab is open - await expect(page.locator('.request-tab').filter({ hasText: 'original-folder' })).toBeVisible(); - await page.waitForTimeout(500); - - // Press Cmd/Ctrl+Shift+D to clone folder - await page.keyboard.press(`${modifier}+Shift+KeyD`); - - // Verify clone modal opens - const cloneModal = page.locator('.bruno-modal-card').filter({ hasText: /clone folder/i }); - await expect(cloneModal).toBeVisible({ timeout: 3000 }); - - // Fill in the clone folder name - const folderNameInput = page.locator('#collection-item-name'); - await folderNameInput.fill('cloned-folder'); - - // Click the clone button - await page.getByTestId('collection-item-clone').click(); - - // Wait for clone operation to complete - await page.waitForTimeout(500); - - // Verify cloned folder appears in sidebar - await expect(page.locator('.collection-item-name').filter({ hasText: 'cloned-folder' })).toBeVisible(); - await expect(page.locator('.collection-item-name').filter({ hasText: 'original-folder' })).toBeVisible(); - }); - }); - - test.describe('FUNCTIONAL: Copy and Paste Item', () => { - test('should copy and paste request using default Cmd/Ctrl+C and Cmd/Ctrl+V', async ({ page, createTmpDir }) => { - // Close all collections first for clean state - await closeAllCollections(page); - - const collectionPath = await createTmpDir('copy-paste-request-default'); - await createCollection(page, 'test-collection-copy-paste-request-default', collectionPath); - - // Create a request - await createRequest(page, 'original-request', 'test-collection-copy-paste-request-default'); - - // Open the request - await openRequest(page, 'test-collection-copy-paste-request-default', 'original-request'); - await page.waitForTimeout(500); - - // Press Cmd/Ctrl+C to copy - await page.keyboard.press(`${modifier}+KeyC`); - await page.waitForTimeout(300); - - // Press Cmd/Ctrl+V to paste - await page.keyboard.press(`${modifier}+KeyV`); - await page.waitForTimeout(500); - - // Verify pasted request appears in sidebar (should be named "original-request (1)") - await expect(page.locator('.collection-item-name').filter({ hasText: 'original-request (1)' })).toBeVisible(); - await expect(page.locator('.collection-item-name').filter({ hasText: 'original-request' }).first()).toBeVisible(); - }); - - test('should copy and paste request using customized Alt+C+R and Alt+V+R', async ({ page, createTmpDir }) => { - // Close all collections, tabs, and preferences first - await closeAllCollections(page); - - // Close any open preference tabs - let preferenceTabs = page.locator('.request-tab').filter({ hasText: 'Preferences' }); - let prefTabCount = await preferenceTabs.count(); - for (let i = 0; i < prefTabCount; i++) { - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - } - - // Open Keybindings preferences and customize copyItem FIRST - await openKeybindingsTab(page); - - let copyRow = page.getByTestId('keybinding-row-copyItem'); - await copyRow.hover(); - - // Start recording for copyItem - await getEditBtn(page, 'copyItem').click(); - await page.waitForTimeout(300); - - // Press new combo: Alt+C+R - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyC'); - await page.keyboard.down('KeyR'); - await page.waitForTimeout(300); - await page.keyboard.up('KeyR'); - await page.keyboard.up('KeyC'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(500); - - // Verify the keybinding was saved - let input = getInput(page, 'copyItem'); - let newValue = await input.inputValue(); - expect(newValue).toContain('alt'); - expect(newValue).toContain('c'); - expect(newValue).toContain('r'); - - // Now customize pasteItem - const pasteRow = page.getByTestId('keybinding-row-pasteItem'); - await pasteRow.hover(); - - // Start recording for pasteItem - await getEditBtn(page, 'pasteItem').click(); - await page.waitForTimeout(300); - - // Press new combo: Alt+V+R - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyV'); - await page.keyboard.down('KeyR'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyR'); - await page.keyboard.up('KeyV'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(1000); - - // Verify the keybinding was saved - input = getInput(page, 'pasteItem'); - newValue = await input.inputValue(); - expect(newValue).toContain('alt'); - expect(newValue).toContain('v'); - expect(newValue).toContain('r'); - - // Close preferences - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(500); - - // Now create collection and request - const collectionPath = await createTmpDir('copy-paste-request-customized'); - await createCollection(page, 'test-collection-copy-paste-request-customized', collectionPath); - - // Create a request - await createRequest(page, 'original-request', 'test-collection-copy-paste-request-customized'); - - // Open the request - await openRequest(page, 'test-collection-copy-paste-request-customized', 'original-request'); - await page.waitForTimeout(500); - - // Press Alt+C+R to copy - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyC'); - await page.keyboard.down('KeyR'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyR'); - await page.keyboard.up('KeyC'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(300); - - // Press Alt+V+R to paste - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyV'); - await page.keyboard.down('KeyR'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyR'); - await page.keyboard.up('KeyV'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(1000); - - // Verify pasted request appears in sidebar (should be named "original-request (1)") - // Wait for the new item to appear - await page.waitForTimeout(500); - const allRequests = page.locator('.collection-item-name'); - const requestCount = await allRequests.count(); - await page.waitForTimeout(500); - expect(requestCount).toBeGreaterThanOrEqual(1); // Should have at least original and pasted - - // Verify both original and pasted exist - await expect(page.locator('.collection-item-name').filter({ hasText: 'original-request' }).first()).toBeVisible(); - }); - - test('should copy and paste folder using customized Alt+C+F and Alt+V+F', async ({ page, createTmpDir }) => { - // Close all collections, tabs, and preferences first - await closeAllCollections(page); - - // Close any open preference tabs - let preferenceTabs = page.locator('.request-tab').filter({ hasText: 'Preferences' }); - let prefTabCount = await preferenceTabs.count(); - for (let i = 0; i < prefTabCount; i++) { - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - } - - // Open Keybindings preferences and customize copyItem FIRST - await openKeybindingsTab(page); - - // Reset first to remove old keybindings - await openKeybindingsTab(page); - const resetCopyRow = page.getByTestId('keybinding-reset-copyItem'); - await resetCopyRow.hover(); - await getResetBtn(page, 'copyItem').click(); - await page.waitForTimeout(500); - - // Start recording for copyItem - await getEditBtn(page, 'copyItem').click(); - await page.waitForTimeout(300); - - // Press new combo: Alt+C+F - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyC'); - await page.keyboard.down('KeyF'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyF'); - await page.keyboard.up('KeyC'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(1000); - - // Verify the keybinding was saved - let input = getInput(page, 'copyItem'); - let newValue = await input.inputValue(); - expect(newValue).toContain('alt'); - expect(newValue).toContain('c'); - expect(newValue).toContain('f'); - - await openKeybindingsTab(page); - const resetPasteRow = page.getByTestId('keybinding-reset-pasteItem'); - await resetPasteRow.hover(); - await getResetBtn(page, 'pasteItem').click(); - await page.waitForTimeout(500); - - // Start recording for copyItem - await getEditBtn(page, 'pasteItem').click(); - await page.waitForTimeout(300); - - // Press new combo: Alt+V+F - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyV'); - await page.keyboard.down('KeyF'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyF'); - await page.keyboard.up('KeyV'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(1000); - - // Verify the keybinding was saved - input = getInput(page, 'pasteItem'); - newValue = await input.inputValue(); - expect(newValue).toContain('alt'); - expect(newValue).toContain('v'); - expect(newValue).toContain('f'); - - // Close preferences - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(1000); - - // Now create collection - const collectionPath = await createTmpDir('copy-paste-folder-customized'); - await createCollection(page, 'test-collection-copy-paste-folder-customized', collectionPath); - - // Create a folder - await createFolder(page, 'original-folder', 'test-collection-copy-paste-folder-customized', true); - await page.waitForTimeout(500); - - // Open folder settings - const folder = page.locator('.collection-item-name').filter({ hasText: 'original-folder' }); - await folder.hover(); - await folder.locator('.menu-icon').click(); - await page.locator('.dropdown-item').filter({ hasText: 'Settings' }).click(); - - // Verify folder settings tab is open - await expect(page.locator('.request-tab').filter({ hasText: 'original-folder' })).toBeVisible(); - await page.waitForTimeout(500); - - // Press Alt+C+F to copy folder - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyC'); - await page.keyboard.down('KeyF'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyF'); - await page.keyboard.up('KeyC'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(300); - - // Press Alt+V+F to paste folder - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyV'); - await page.keyboard.down('KeyF'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyF'); - await page.keyboard.up('KeyV'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(1000); - - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(1000); - - // Verify pasted folder appears in sidebar - // Wait for the new item to appear - await page.waitForTimeout(500); - const allFolders = page.locator('.collection-item-name'); - const folderCount = await allFolders.count(); - await page.waitForTimeout(500); - expect(folderCount).toBeGreaterThanOrEqual(1); // Should have at least original and pasted - - // Verify both original and pasted exist - await expect(page.locator('.collection-item-name').filter({ hasText: 'original-folder' }).first()).toBeVisible(); - }); - }); - - test.describe('FUNCTIONAL: Move Tab Left', () => { - test('should move tab left using default Cmd/Ctrl+[', async ({ page, createTmpDir }) => { - // Close all collections first for clean state - await closeAllCollections(page); - - const collectionPath = await createTmpDir('move-tab-left-default'); - await createCollection(page, 'test-collection-move-tab-left-default', collectionPath); - - // Create multiple requests - await createRequest(page, 'request-1', 'test-collection-move-tab-left-default'); - await createRequest(page, 'request-2', 'test-collection-move-tab-left-default'); - await createRequest(page, 'request-3', 'test-collection-move-tab-left-default'); - - // Open and pin all requests (persist: true means double-click to pin) - await openRequest(page, 'test-collection-move-tab-left-default', 'request-1', { persist: true }); - await openRequest(page, 'test-collection-move-tab-left-default', 'request-2', { persist: true }); - await openRequest(page, 'test-collection-move-tab-left-default', 'request-3', { persist: true }); - - // Wait for tabs to be ready - request-3 should be active and last - await page.waitForTimeout(500); - - // Get initial tab order - const tabs = page.locator('.request-tab'); - const initialCount = await tabs.count(); - expect(initialCount).toBe(3); - - // Verify request-3 is active and last - const lastTab = tabs.last(); - const activeTab = page.locator('li.request-tab.active'); - await expect(lastTab).toHaveText(/request-3/); - - // Press Cmd/Ctrl+[ to move tab left - await page.keyboard.press(`${modifier}+BracketLeft`); - await page.waitForTimeout(500); - - // Verify request-3 is still active but moved left (no longer last tab) - await expect(activeTab).toHaveText(/request-3/); - await expect(activeTab).not.toHaveClass(/last-tab/); - - // Press again to move further left - await page.keyboard.press(`${modifier}+BracketLeft`); - await page.waitForTimeout(500); - - // Verify request-3 is now first tab - const firstTab = tabs.first(); - await expect(firstTab).toHaveText(/request-3/); - await expect(activeTab).toHaveText(/request-3/); - }); - - test('should move tab left using customized Alt+M+L', async ({ page, createTmpDir }) => { - // Close all collections, tabs, and preferences first - await closeAllCollections(page); - - // Close any open preference tabs - const preferenceTabs = page.locator('.request-tab').filter({ hasText: 'Preferences' }); - const prefTabCount = await preferenceTabs.count(); - for (let i = 0; i < prefTabCount; i++) { - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - } - - // Open Keybindings preferences and customize moveTabLeft FIRST - await openKeybindingsTab(page); - - const row = page.getByTestId('keybinding-row-moveTabLeft'); - await row.hover(); - - // Start recording - await getEditBtn(page, 'moveTabLeft').click(); - await page.waitForTimeout(300); - - // Press new combo: Alt+M+L - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyM'); - await page.keyboard.down('KeyL'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyL'); - await page.keyboard.up('KeyM'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(1000); - - // Verify the keybinding was saved - const input = getInput(page, 'moveTabLeft'); - const newValue = await input.inputValue(); - expect(newValue).toContain('alt'); - expect(newValue).toContain('m'); - expect(newValue).toContain('l'); - - // Close preferences - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(1000); - - // Now create collection and requests - const collectionPath = await createTmpDir('move-tab-left-customized'); - await createCollection(page, 'test-collection-move-tab-left-customized', collectionPath); - - // Create multiple requests - await createRequest(page, 'request-1', 'test-collection-move-tab-left-customized'); - await createRequest(page, 'request-2', 'test-collection-move-tab-left-customized'); - await createRequest(page, 'request-3', 'test-collection-move-tab-left-customized'); - - // Open and pin all requests - await openRequest(page, 'test-collection-move-tab-left-customized', 'request-1', { persist: true }); - await openRequest(page, 'test-collection-move-tab-left-customized', 'request-2', { persist: true }); - await openRequest(page, 'test-collection-move-tab-left-customized', 'request-3', { persist: true }); - - // Wait for tabs to be ready - await page.waitForTimeout(500); - - // Get initial tab order - const tabs = page.locator('.request-tab'); - const initialCount = await tabs.count(); - expect(initialCount).toBe(3); - - // Verify request-3 is active and last - const lastTab = tabs.last(); - const activeTab = page.locator('li.request-tab.active'); - await expect(lastTab).toHaveText(/request-3/); - - // Press Alt+M+L to move tab left - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyM'); - await page.keyboard.down('KeyL'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyL'); - await page.keyboard.up('KeyM'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(500); - - // Verify request-3 is still active but moved left - await expect(activeTab).toHaveText(/request-3/); - await expect(activeTab).not.toHaveClass(/last-tab/); - - // Press again to move further left - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyM'); - await page.keyboard.down('KeyL'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyL'); - await page.keyboard.up('KeyM'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(500); - - // Verify request-3 is now first tab - const firstTab = tabs.first(); - await expect(firstTab).toHaveText(/request-3/); - await expect(activeTab).toHaveText(/request-3/); - }); - }); - - test.describe('FUNCTIONAL: Move Tab Right', () => { - test('should move tab right using default Cmd/Ctrl+]', async ({ page, createTmpDir }) => { - // Close all collections first for clean state - await closeAllCollections(page); - - const collectionPath = await createTmpDir('move-tab-right-default'); - await createCollection(page, 'test-collection-move-tab-right-default', collectionPath); - - // Create multiple requests - await createRequest(page, 'request-1', 'test-collection-move-tab-right-default'); - await createRequest(page, 'request-2', 'test-collection-move-tab-right-default'); - await createRequest(page, 'request-3', 'test-collection-move-tab-right-default'); - - // Open and pin all requests (persist: true means double-click to pin) - await openRequest(page, 'test-collection-move-tab-right-default', 'request-1', { persist: true }); - await openRequest(page, 'test-collection-move-tab-right-default', 'request-2', { persist: true }); - await openRequest(page, 'test-collection-move-tab-right-default', 'request-3', { persist: true }); - await openRequest(page, 'test-collection-move-tab-right-default', 'request-1', { persist: true }); - - // Wait for tabs to be ready - request-3 should be active and last - await page.waitForTimeout(500); - - // Get initial tab order - const tabs = page.locator('.request-tab'); - const initialCount = await tabs.count(); - expect(initialCount).toBe(4); - - // Verify request-3 is active and last - const firstTab = tabs.first(); - await expect(firstTab).toHaveText(/request-1/); - const activeTab = page.locator('li.request-tab.active'); - await expect(activeTab).toHaveText(/request-1/); - - // Press Cmd/Ctrl+] to move tab right - await page.keyboard.press(`${modifier}+BracketRight`); - await page.waitForTimeout(500); - - // Verify request-1 is still active but moved right (no longer last tab) - await expect(activeTab).toHaveText(/request-1/); - - // Press again to move further right - await page.keyboard.press(`${modifier}+BracketRight`); - await page.waitForTimeout(500); - - await expect(activeTab).toHaveText(/request-1/); - - // Press again to move further right - await page.keyboard.press(`${modifier}+BracketRight`); - await page.waitForTimeout(500); - - // Verify request-3 is now first tab - const lastTab = tabs.last(); - await expect(lastTab).toHaveText(/request-1/); - await expect(activeTab).toHaveText(/request-1/); - }); - - test('should move tab right using customized Alt+M+R', async ({ page, createTmpDir }) => { - // Close all collections, tabs, and preferences first - await closeAllCollections(page); - - // Close any open preference tabs - const preferenceTabs = page.locator('.request-tab').filter({ hasText: 'Preferences' }); - const prefTabCount = await preferenceTabs.count(); - for (let i = 0; i < prefTabCount; i++) { - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(200); - } - - // Open Keybindings preferences and customize moveTabRight FIRST - await openKeybindingsTab(page); - - const row = page.getByTestId('keybinding-row-moveTabRight'); - await row.hover(); - - // Start recording - await getEditBtn(page, 'moveTabRight').click(); - await page.waitForTimeout(300); - - // Press new combo: Alt+M+R - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyM'); - await page.keyboard.down('KeyR'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyR'); - await page.keyboard.up('KeyM'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(1000); - - // Verify the keybinding was saved - const input = getInput(page, 'moveTabRight'); - const newValue = await input.inputValue(); - expect(newValue).toContain('alt'); - expect(newValue).toContain('m'); - expect(newValue).toContain('r'); - - // Close preferences - await page.keyboard.press(`${modifier}+KeyW`); - await page.waitForTimeout(1000); - - // Now create collection and requests - const collectionPath = await createTmpDir('move-tab-right-customized'); - await createCollection(page, 'test-collection-move-tab-right-customized', collectionPath); - - // Create multiple requests - await createRequest(page, 'request-1', 'test-collection-move-tab-right-customized'); - await createRequest(page, 'request-2', 'test-collection-move-tab-right-customized'); - await createRequest(page, 'request-3', 'test-collection-move-tab-right-customized'); - - // Open and pin all requests (persist: true means double-click to pin) - await openRequest(page, 'test-collection-move-tab-right-customized', 'request-1', { persist: true }); - await openRequest(page, 'test-collection-move-tab-right-customized', 'request-2', { persist: true }); - await openRequest(page, 'test-collection-move-tab-right-customized', 'request-3', { persist: true }); - await openRequest(page, 'test-collection-move-tab-right-customized', 'request-1', { persist: true }); - - // Wait for tabs to be ready - request-3 should be active and last - await page.waitForTimeout(500); - - // Get initial tab order - const tabs = page.locator('.request-tab'); - const initialCount = await tabs.count(); - expect(initialCount).toBe(4); - - // Verify request-3 is active and last - const firstTab = tabs.first(); - await expect(firstTab).toHaveText(/request-1/); - const activeTab = page.locator('li.request-tab.active'); - await expect(activeTab).toHaveText(/request-1/); - - // Press again to move further right - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyM'); - await page.keyboard.down('KeyR'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyR'); - await page.keyboard.up('KeyM'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(500); - - // Verify request-1 is still active but moved right (no longer last tab) - await expect(activeTab).toHaveText(/request-1/); - - // Press again to move further right - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyM'); - await page.keyboard.down('KeyR'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyR'); - await page.keyboard.up('KeyM'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(500); - - // Verify request-1 is still active but moved right (no longer last tab) - await expect(activeTab).toHaveText(/request-1/); - - // Press again to move further right - await page.keyboard.down('Alt'); - await page.keyboard.down('KeyM'); - await page.keyboard.down('KeyR'); - await page.waitForTimeout(200); - await page.keyboard.up('KeyR'); - await page.keyboard.up('KeyM'); - await page.keyboard.up('Alt'); - await page.waitForTimeout(500); - - // Verify request-3 is now first tab - const lastTab = tabs.last(); - await expect(lastTab).toHaveText(/request-1/); - await expect(activeTab).toHaveText(/request-1/); - }); - }); -}); diff --git a/tests/utils/page/actions.ts b/tests/utils/page/actions.ts index 295fe1dc9..30fd5ca4c 100644 --- a/tests/utils/page/actions.ts +++ b/tests/utils/page/actions.ts @@ -450,7 +450,7 @@ const createFolder = async ( } await locators.dropdown.item('New Folder').click(); - await page.getByTestId('new-folder-input').fill(folderName); + await page.getByPlaceholder('Folder Name').fill(folderName); await locators.modal.button('Create').click(); await expect(locators.sidebar.folder(folderName)).toBeVisible(); });
{row.name} -
-
- { - if (el) inputRefs.current[action] = el; - }} - data-testid={`keybinding-input-${action}`} - className={`shortcut-input ${hasError ? 'shortcut-input--error' : ''}`} - value={renderValue(action)} - readOnly={!isEditing} - onKeyDown={(e) => handleKeyDown(action, e)} - onKeyUp={(e) => { handleKeyUp(action, e); }} - onBlur={() => { - // If there's an error, reset to original value instead of keeping invalid state - if (isEditing && hasError) { - cancelEditing(action); - } else if (isEditing) { - stopEditing(action); - } - }} - spellCheck={false} - /> - {isEditing && hasError && ( - - )} -
- - {showReset ? ( - - ) : null} - {showPencil ? ( - - ) : null} + Object.entries(keyMapping).map(([action, { name, keys }], index) => ( +
{name} + {keys.split('+').map((key, i) => ( +
+ {key}
-
No key bindings available