mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-23 20:55:41 +00:00
Autocomplete random variables (#4695)
* Feature: adding dynamic variable support (#3609) Co-authored-by: Raghav Sethi <109696225+rsxc@users.noreply.github.com> Co-authored-by: sanjai0py <sanjailucifer666@gmail.com>
This commit is contained in:
@@ -25,6 +25,19 @@ module.exports = defineConfig([
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
// It prevents lint errors when using CommonJS exports (module.exports) in Jest mocks.
|
||||
files: ["packages/bruno-app/src/test-utils/mocks/codemirror.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-electron/**/*.{js}"],
|
||||
ignores: ["**/*.config.js"],
|
||||
|
||||
235
package-lock.json
generated
235
package-lock.json
generated
@@ -8209,78 +8209,6 @@
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "9.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz",
|
||||
"integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/aria-query": "^5.0.1",
|
||||
"aria-query": "5.1.3",
|
||||
"chalk": "^4.1.0",
|
||||
"dom-accessibility-api": "^0.5.9",
|
||||
"lz-string": "^1.5.0",
|
||||
"pretty-format": "^27.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom/node_modules/pretty-format": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"react-is": "^17.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom/node_modules/pretty-format/node_modules/ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom/node_modules/react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@testing-library/jest-dom": {
|
||||
"version": "6.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz",
|
||||
@@ -8309,25 +8237,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@testing-library/react": {
|
||||
"version": "14.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.3.1.tgz",
|
||||
"integrity": "sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@testing-library/dom": "^9.0.0",
|
||||
"@types/react-dom": "^18.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tippyjs/react": {
|
||||
"version": "4.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz",
|
||||
@@ -8700,16 +8609,6 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-redux": {
|
||||
"version": "7.1.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz",
|
||||
@@ -28148,9 +28047,9 @@
|
||||
"@rsbuild/plugin-react": "^1.0.7",
|
||||
"@rsbuild/plugin-sass": "^1.1.0",
|
||||
"@rsbuild/plugin-styled-components": "1.1.0",
|
||||
"@testing-library/dom": "^9.3.3",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"autoprefixer": "10.4.20",
|
||||
"babel-jest": "^29.7.0",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
|
||||
@@ -29478,6 +29377,64 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/@testing-library/dom": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
|
||||
"integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/aria-query": "^5.0.1",
|
||||
"aria-query": "5.3.0",
|
||||
"chalk": "^4.1.0",
|
||||
"dom-accessibility-api": "^0.5.9",
|
||||
"lz-string": "^1.5.0",
|
||||
"pretty-format": "^27.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/@testing-library/react": {
|
||||
"version": "16.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz",
|
||||
"integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@testing-library/dom": "^10.0.0",
|
||||
"@types/react": "^18.0.0 || ^19.0.0",
|
||||
"@types/react-dom": "^18.0.0 || ^19.0.0",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/aria-query": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
|
||||
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/babel-plugin-polyfill-corejs3": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz",
|
||||
@@ -29546,6 +29503,23 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"packages/bruno-app/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/cookie": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
@@ -29647,6 +29621,41 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/pretty-format": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"react-is": "^17.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/pretty-format/node_modules/ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/bruno-app/node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
@@ -31461,7 +31470,6 @@
|
||||
"version": "2.0.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.750.0",
|
||||
"@faker-js/faker": "^9.5.1",
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/converters": "^0.1.0",
|
||||
"@usebruno/js": "0.12.0",
|
||||
@@ -31971,23 +31979,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/@faker-js/faker": {
|
||||
"version": "9.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.5.1.tgz",
|
||||
"integrity": "sha512-0fzMEDxkExR2cn731kpDaCCnBGBUOIXEi2S1N5l8Hltp6aPf4soTMJ+g4k8r2sI5oB+rpwIW8Uy/6jkwGpnWPg==",
|
||||
"deprecated": "Please update to a newer version",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fakerjs"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=9.0.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/@smithy/abort-controller": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.1.tgz",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"],
|
||||
"plugins": [["styled-components", { "ssr": true }]]
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
module.exports = {
|
||||
rootDir: '.',
|
||||
transform: {
|
||||
'^.+\\.[jt]sx?$': 'babel-jest',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
"/node_modules/(?!strip-json-comments|nanoid|xml-formatter)/",
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'^assets/(.*)$': '<rootDir>/src/assets/$1',
|
||||
'^components/(.*)$': '<rootDir>/src/components/$1',
|
||||
@@ -8,18 +14,17 @@ module.exports = {
|
||||
'^api/(.*)$': '<rootDir>/src/api/$1',
|
||||
'^pageComponents/(.*)$': '<rootDir>/src/pageComponents/$1',
|
||||
'^providers/(.*)$': '<rootDir>/src/providers/$1',
|
||||
'^utils/(.*)$': '<rootDir>/src/utils/$1'
|
||||
'^utils/(.*)$': '<rootDir>/src/utils/$1',
|
||||
'^test-utils/(.*)$': '<rootDir>/src/test-utils/$1'
|
||||
},
|
||||
clearMocks: true,
|
||||
moduleDirectories: ['node_modules', 'src'],
|
||||
testEnvironment: 'jsdom',
|
||||
transform: {
|
||||
'^.+\\.[jt]sx?$': 'babel-jest'
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!(nanoid|xml-formatter)/)'
|
||||
setupFilesAfterEnv: ['@testing-library/jest-dom'],
|
||||
setupFiles: [
|
||||
'<rootDir>/jest.setup.js',
|
||||
],
|
||||
testMatch: [
|
||||
'<rootDir>/src/**/*.spec.[jt]s?(x)'
|
||||
]
|
||||
};
|
||||
};
|
||||
11
packages/bruno-app/jest.setup.js
Normal file
11
packages/bruno-app/jest.setup.js
Normal file
@@ -0,0 +1,11 @@
|
||||
jest.mock('nanoid', () => {
|
||||
return {
|
||||
nanoid: () => {}
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('strip-json-comments', () => {
|
||||
return {
|
||||
stripJsonComments: (str) => str
|
||||
};
|
||||
});
|
||||
@@ -91,9 +91,9 @@
|
||||
"@rsbuild/plugin-react": "^1.0.7",
|
||||
"@rsbuild/plugin-sass": "^1.1.0",
|
||||
"@rsbuild/plugin-styled-components": "1.1.0",
|
||||
"@testing-library/dom": "^9.3.3",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"autoprefixer": "10.4.20",
|
||||
"babel-jest": "^29.7.0",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
|
||||
|
||||
@@ -20,6 +20,11 @@ export default defineConfig({
|
||||
],
|
||||
source: {
|
||||
tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file,
|
||||
exclude: [
|
||||
'**/test-utils/**',
|
||||
'**/*.test.*',
|
||||
'**/*.spec.*'
|
||||
]
|
||||
},
|
||||
html: {
|
||||
title: 'Bruno'
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import React from 'react';
|
||||
import { isEqual, escapeRegExp } from 'lodash';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import { getMockDataHints } from 'utils/codemirror/mock-data-hints';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import * as jsonlint from '@prantlf/jsonlint';
|
||||
import { JSHINT } from 'jshint';
|
||||
@@ -90,6 +91,7 @@ if (!SERVER_RENDERED) {
|
||||
'bru.runner.stopExecution()',
|
||||
'bru.interpolate(str)'
|
||||
];
|
||||
|
||||
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
|
||||
const cursor = editor.getCursor();
|
||||
const currentLine = editor.getLine(cursor.line);
|
||||
@@ -117,6 +119,7 @@ if (!SERVER_RENDERED) {
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
CodeMirror.commands.autocomplete = (cm, hint, options) => {
|
||||
cm.showHint({ hint, ...options });
|
||||
};
|
||||
@@ -278,11 +281,14 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
return found;
|
||||
});
|
||||
|
||||
if (editor) {
|
||||
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
editor.on('change', this._onEdit);
|
||||
editor.on('keyup', this._onKeyUpMockDataHints);
|
||||
this.addOverlay();
|
||||
}
|
||||
|
||||
if (this.props.mode == 'javascript') {
|
||||
editor.on('keyup', function (cm, event) {
|
||||
const cursor = editor.getCursor();
|
||||
@@ -305,6 +311,28 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
_onKeyUpMockDataHints(cm, event) {
|
||||
// This prevents triggering hints for non-character keys (e.g., Arrow keys, Meta).
|
||||
if (
|
||||
!/^(?!Shift|Tab|Enter|Escape|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Meta|Alt|Home|End\s)\w*/.test(event?.key)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hints = getMockDataHints(cm);
|
||||
if (!hints) {
|
||||
if (cm.state.completionActive) {
|
||||
cm.state.completionActive.close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
cm.showHint({
|
||||
hint: () => hints,
|
||||
completeSingle: false
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
// Ensure the changes caused by this update are not interpreted as
|
||||
// user-input changes which could otherwise result in an infinite
|
||||
@@ -338,6 +366,7 @@ export default class CodeEditor extends React.Component {
|
||||
componentWillUnmount() {
|
||||
if (this.editor) {
|
||||
this.editor.off('change', this._onEdit);
|
||||
this.editor.off('keyup', this._onKeyUpMockDataHints);
|
||||
this.editor = null;
|
||||
}
|
||||
|
||||
|
||||
105
packages/bruno-app/src/components/CodeEditor/index.spec.js
Normal file
105
packages/bruno-app/src/components/CodeEditor/index.spec.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import React from 'react';
|
||||
import { render, act } from '@testing-library/react';
|
||||
import CodeEditor from './index';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
|
||||
jest.mock('codemirror', () => {
|
||||
const codemirror = require('test-utils/mocks/codemirror');
|
||||
return codemirror;
|
||||
});
|
||||
|
||||
const MOCK_THEME = {
|
||||
codemirror: {
|
||||
bg: "#1e1e1e",
|
||||
border: "#333",
|
||||
},
|
||||
textLink: "#007acc",
|
||||
};
|
||||
|
||||
const setupEditorState = (editor, { value, cursorPosition }) => {
|
||||
editor._currentValue = value;
|
||||
editor.getCursor.mockReturnValue({ line: 0, ch: cursorPosition });
|
||||
editor.getRange.mockImplementation((from, to) => {
|
||||
if (from.line === 0 && from.ch === 0 && to.line === 0 && to.ch === cursorPosition) {
|
||||
return value;
|
||||
}
|
||||
return editor._currentValue.slice(from.ch, to.ch);
|
||||
});
|
||||
|
||||
editor.state = {
|
||||
completionActive: null,
|
||||
}
|
||||
};
|
||||
|
||||
const setupEditorWithRef = () => {
|
||||
const ref = React.createRef();
|
||||
const { rerender } = render(
|
||||
<ThemeProvider theme={MOCK_THEME}>
|
||||
<CodeEditor ref={ref} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
return { ref, rerender };
|
||||
};
|
||||
|
||||
describe('CodeEditor Autocomplete', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
it('shows autocomplete suggestions when typing {{$ra', () => {
|
||||
// Setup
|
||||
const { ref } = setupEditorWithRef();
|
||||
const editorInstance = ref.current;
|
||||
expect(editorInstance).toBeTruthy();
|
||||
|
||||
const editor = editorInstance.editor;
|
||||
expect(editor).toBeTruthy();
|
||||
|
||||
// Configure editor state
|
||||
setupEditorState(editor, {
|
||||
value: '{{$r',
|
||||
cursorPosition: 4
|
||||
});
|
||||
|
||||
// Trigger autocomplete
|
||||
const _onKeyUpMockDataHints = editor._onKeyUpMockDataHints;
|
||||
expect(typeof _onKeyUpMockDataHints).toBe('function');
|
||||
|
||||
act(() => {
|
||||
_onKeyUpMockDataHints(editor, { text: ['a'], origin: '+input' });
|
||||
});
|
||||
|
||||
// Assertions
|
||||
expect(editor.showHint).toHaveBeenCalled();
|
||||
const call = editor.showHint.mock.calls[0][0];
|
||||
expect(typeof call.hint).toBe('function');
|
||||
|
||||
const hints = call.hint();
|
||||
expect(Array.isArray(hints.list)).toBe(true);
|
||||
expect(hints.list.some((s) => s.startsWith('$'))).toBe(true);
|
||||
expect(hints.list.every((match) => match.startsWith('$ra'))).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show hints for regular text input', () => {
|
||||
// Setup
|
||||
const { ref } = setupEditorWithRef();
|
||||
const editor = ref.current.editor;
|
||||
|
||||
// Configure editor state
|
||||
setupEditorState(editor, {
|
||||
value: 'regular text',
|
||||
cursorPosition: 11
|
||||
});
|
||||
|
||||
// Trigger input
|
||||
const _onKeyUpMockDataHints = editor._onKeyUpMockDataHints;
|
||||
|
||||
act(() => {
|
||||
_onKeyUpMockDataHints(editor, { text: ['x'], origin: '+input' });
|
||||
});
|
||||
|
||||
// Assert no hints shown for regular text
|
||||
expect(editor.showHint).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import React, { Component } from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { getAllVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode, MaskedEditor } from 'utils/common/codemirror';
|
||||
import { getMockDataHints } from 'utils/codemirror/mock-data-hints';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconEye, IconEyeOff } from '@tabler/icons';
|
||||
|
||||
@@ -26,6 +27,7 @@ class SingleLineEditor extends Component {
|
||||
maskInput: props.isSecret || false // Always mask the input by default (if it's a secret)
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Initialize CodeMirror as a single line editor
|
||||
/** @type {import("codemirror").Editor} */
|
||||
@@ -75,6 +77,7 @@ class SingleLineEditor extends Component {
|
||||
'Shift-Tab': false
|
||||
}
|
||||
});
|
||||
|
||||
if (this.props.autocomplete) {
|
||||
this.editor.on('keyup', (cm, event) => {
|
||||
if (!cm.state.completionActive /*Enables keyboard navigation in autocomplete list*/ && event.key !== 'Enter') {
|
||||
@@ -83,6 +86,8 @@ class SingleLineEditor extends Component {
|
||||
}
|
||||
});
|
||||
}
|
||||
this.editor.on('keyup', this._onKeyUpMockDataHints);
|
||||
|
||||
this.editor.setValue(String(this.props.value ?? ''));
|
||||
this.editor.on('change', this._onEdit);
|
||||
this.addOverlay(variables);
|
||||
@@ -94,7 +99,6 @@ class SingleLineEditor extends Component {
|
||||
_enableMaskedEditor = (enabled) => {
|
||||
if (typeof enabled !== 'boolean') return;
|
||||
|
||||
console.log('Enabling masked editor: ' + enabled);
|
||||
if (enabled == true) {
|
||||
if (!this.maskedEditor) this.maskedEditor = new MaskedEditor(this.editor, '*');
|
||||
this.maskedEditor.enable();
|
||||
@@ -113,6 +117,26 @@ class SingleLineEditor extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
_onKeyUpMockDataHints(cm, event) {
|
||||
// This prevents triggering hints for non-character keys (e.g., Arrow keys, Meta).
|
||||
if (!/^(?!Shift|Tab|Enter|Escape|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Meta|Alt|Home|End\s)\w*/.test(event?.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hints = getMockDataHints(cm);
|
||||
if (!hints) {
|
||||
if (cm.state.completionActive) {
|
||||
cm.state.completionActive.close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
cm.showHint({
|
||||
hint: () => hints,
|
||||
completeSingle: false
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
// Ensure the changes caused by this update are not interpreted as
|
||||
// user-input changes which could otherwise result in an infinite
|
||||
@@ -141,7 +165,12 @@ class SingleLineEditor extends Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.editor.getWrapperElement().remove();
|
||||
if (this.editor) {
|
||||
this.editor.off('change', this._onEdit);
|
||||
this.editor.off('keyup', this._onKeyUpMockDataHints);
|
||||
this.editor.getWrapperElement().remove();
|
||||
this.editor = null;
|
||||
}
|
||||
}
|
||||
|
||||
addOverlay = (variables) => {
|
||||
|
||||
45
packages/bruno-app/src/test-utils/mocks/codemirror.js
Normal file
45
packages/bruno-app/src/test-utils/mocks/codemirror.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const CodeMirror = jest.fn((node, options) => {
|
||||
const editor = {
|
||||
options,
|
||||
_currentValue: '',
|
||||
_onKeyUpMockDataHints: null,
|
||||
getCursor: jest.fn(() => ({ line: 0, ch: editor._currentValue?.length || 0 })),
|
||||
getRange: jest.fn((from, to) => editor._currentValue?.slice(0, to.ch) || ''),
|
||||
getValue: jest.fn(() => editor._currentValue),
|
||||
setValue: jest.fn(function (val) {
|
||||
editor._currentValue = val;
|
||||
}),
|
||||
getLine: jest.fn(() => editor._currentValue || ''),
|
||||
setOption: jest.fn(),
|
||||
refresh: jest.fn(),
|
||||
off: jest.fn(),
|
||||
showHint: jest.fn(),
|
||||
on: jest.fn(function (event, handler) {
|
||||
if (event === 'keyup') {
|
||||
if (handler && handler.name === '_onKeyUpMockDataHints') {
|
||||
this._onKeyUpMockDataHints = handler;
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
return editor;
|
||||
});
|
||||
|
||||
CodeMirror.commands = {
|
||||
autocomplete: jest.fn()
|
||||
};
|
||||
|
||||
CodeMirror.hint = {};
|
||||
|
||||
CodeMirror.registerHelper = jest.fn((type, name, value) => {
|
||||
if (!CodeMirror[type]) {
|
||||
CodeMirror[type] = {};
|
||||
}
|
||||
|
||||
CodeMirror[type][name] = value;
|
||||
});
|
||||
|
||||
CodeMirror.fromTextArea = jest.fn();
|
||||
CodeMirror.defineMode = jest.fn();
|
||||
|
||||
module.exports = CodeMirror;
|
||||
25
packages/bruno-app/src/utils/codemirror/mock-data-hints.js
Normal file
25
packages/bruno-app/src/utils/codemirror/mock-data-hints.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { mockDataFunctions } from '@usebruno/common';
|
||||
|
||||
const MOCK_FUNCTION_SUGGESTIONS = Object.keys(mockDataFunctions).map(key => `$${key}`);
|
||||
|
||||
export const getMockDataHints = (cm) => {
|
||||
const cursor = cm.getCursor();
|
||||
const currentString = cm.getRange({ line: cursor.line, ch: 0 }, cursor);
|
||||
|
||||
const match = currentString.match(/\{\{\$(\w*)$/);
|
||||
if (!match) return null;
|
||||
|
||||
const wordMatch = match[1];
|
||||
if (!wordMatch) return null;
|
||||
|
||||
const suggestions = MOCK_FUNCTION_SUGGESTIONS.filter(name => name.startsWith(`$${wordMatch}`));
|
||||
if (!suggestions.length) return null;
|
||||
|
||||
const startPos = { line: cursor.line, ch: currentString.lastIndexOf('{{$') + 2 }; // +2 accounts for `{{`
|
||||
|
||||
return {
|
||||
list: suggestions,
|
||||
from: startPos,
|
||||
to: cm.getCursor(),
|
||||
};
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import get from 'lodash/get';
|
||||
import { mockDataFunctions } from '@usebruno/common';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
@@ -108,7 +109,9 @@ export const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPa
|
||||
while ((ch = stream.next()) != null) {
|
||||
if (ch === '}' && stream.peek() === '}') {
|
||||
stream.eat('}');
|
||||
const found = pathFoundInVariables(word, variables);
|
||||
// Check if it's a mock variable (starts with $) and exists in mockDataFunctions
|
||||
const isMockVariable = word.startsWith('$') && mockDataFunctions.hasOwnProperty(word.substring(1));
|
||||
const found = isMockVariable || pathFoundInVariables(word, variables);
|
||||
const status = found ? 'valid' : 'invalid';
|
||||
const randomClass = `random-${(Math.random() + 1).toString(36).substring(9)}`;
|
||||
return `variable-${status} ${randomClass}`;
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { mockDataFunctions } from './utils/faker-functions';
|
||||
export { default as interpolate } from './interpolate';
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.750.0",
|
||||
"@faker-js/faker": "^9.5.1",
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/converters": "^0.1.0",
|
||||
"@usebruno/js": "0.12.0",
|
||||
|
||||
Reference in New Issue
Block a user