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:
Sanjai Kumar
2025-06-18 20:06:45 +05:30
committed by GitHub
parent acd42eaa1b
commit 34614f039f
15 changed files with 398 additions and 137 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -1,4 +1,4 @@
{
"presets": ["@babel/preset-env"],
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": [["styled-components", { "ssr": true }]]
}

View File

@@ -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)'
]
};
};

View File

@@ -0,0 +1,11 @@
jest.mock('nanoid', () => {
return {
nanoid: () => {}
};
});
jest.mock('strip-json-comments', () => {
return {
stripJsonComments: (str) => str
};
});

View File

@@ -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",

View File

@@ -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'

View File

@@ -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;
}

View 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();
});
});

View File

@@ -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) => {

View 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;

View 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(),
};
};

View File

@@ -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}`;

View File

@@ -1 +1,2 @@
export { mockDataFunctions } from './utils/faker-functions';
export { default as interpolate } from './interpolate';

View File

@@ -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",