mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-08 06:28:33 +00:00
feat: enhance ScriptError with source context and remove auto-commenting of untranslated pm commands (#7449)
* feat: enhance ScriptError with source context, code snippets, and navigation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: remove auto-commenting of untranslated pm commands during import Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: update CodeSnippet styles to use theme colors for error and warning highlights * fix: remove unused SCRIPT_TYPES import from network IPC module * refactor: remove unused functions and clean up source-context utility - Removed `getUnifiedScriptContext`, `getWarningSourceGroups`, and related helper functions from `source-context.js` to streamline the utility. - Updated tests in `source-context.spec.js` to reflect the removal of unused functions, ensuring only relevant tests for `findLineInSource` and `getScriptContext` remain. * refactor: simplify ScriptError component and update styles * refactor: streamline tab management in Script components * refactor: enhance tab management in ScriptError and add testsMetadata handling in prepare-request * refactor: improve error source identification in ScriptError component - Enhanced the logic for determining error source types by introducing separate checks for folder and collection files. - Updated the handling of folder file names to ensure accurate UID retrieval and labeling. - Streamlined the overall structure of the getErrorSourceInfo function for better readability and maintainability. * refactor: improve ScriptError component and enhance styling - Simplified the conditions for displaying the ScriptError component in ResponsePane. - Updated navigation logic in ScriptErrorCard to ensure proper handling of source information. - Adjusted styles in StyledWrapper to allow for visible overflow. - Enhanced ScriptErrorIcon to accept additional class names for better styling flexibility. - Minor layout adjustments in RunnerResults ResponsePane for improved UI consistency. * refactor: simplify test description for untranslated pm commands and consolidate error formatter imports * fixes * refactor: update focusedTab logic in Script components to use collection and folder UIDthe respective collection and folder UID instead of the activeTabUid. * feat: add buildErrorContext utility for enhanced error handling in network IPC - Introduced a new utility function `buildErrorContext` to construct detailed error context from script errors. - This function parses error locations, adjusts line numbers, and retrieves relevant source context, improving error reporting. - Removed the previous inline error handling logic from the network IPC module to streamline the codebase. * feat: added playwright test cases to test scriptError behavior * refactor: enhance ScriptError component and improve error handling - Updated labels for error source types in ScriptError to be more concise (e.g., "Request Script" to "Request"). - Improved navigation logic in ScriptErrorCard to ensure proper handling of navigable file paths. - Enhanced styling in StyledWrapper for better visual consistency and user experience. - Added tests to verify fallback behavior for missing error types and non-navigable file paths. - Refined utility functions for better context extraction from script errors. * refactor: normalize file paths for cross-platform compatibility in ScriptError component * refactor: improve file path handling in ScriptError component * refactor: enhance buildErrorContext for improved error handling * docs: add detailed comments to build-error-context for better understanding of error handling * refactor: enhance error block line detection in error formatter - Updated `findScriptBlockEndLine` and `findYmlScriptBlockEndLine` functions to return null for empty or missing blocks, improving accuracy in line detection. - Added comprehensive tests for both functions to ensure correct behavior across various scenarios, including handling of non-.bru and non-.yml files. * refactor: improve error handling and testing in ScriptError and buildErrorContext - Updated ScriptError component to streamline tab management by replacing focusTab with addTab for better request handling. - Enhanced buildErrorContext to return null for empty or missing script blocks in .bru and .yml files, ensuring accurate error reporting. - Added tests to validate behavior for empty script blocks and improved error context extraction in various scenarios. * test: add new tests for script error navigation and handling - Implemented tests for post-response file-path navigation to the Script tab and verification of active sub-tabs. - Added keyboard navigation tests to trigger file-path navigation using the Enter key. - Included a test for multiple error cards to ensure closing one does not affect others. - Enhanced runner tests to verify navigation to the Tests tab from script error results. * refactor: enhance CodeSnippet line rendering and remove unused source-context utilities * refactor: update locators in script-errors tests for improved readability and maintainability * test: enhance script-errors tests to verify error line content * review fixes * refactor: update RunnerResults component and enhance locators for improved testability * refactor: remove buildErrorContext and replace with formatErrorWithContextV2 - Deleted the buildErrorContext function and its associated tests. - Updated network IPC to utilize formatErrorWithContextV2 for improved error context handling. - Enhanced error reporting by ensuring structured error context is returned for desktop UI. * refactor: enhance tab components with data-testid attributes for improved testability - Added data-testid attributes to tab elements in CollectionSettings and FolderSettings components for better integration with testing frameworks. - Updated Tabs and ResponsiveTabs components to include data-testid attributes for tab triggers, enhancing the ability to select and verify tabs in tests. - Modified script-errors tests to utilize new locators for improved readability and maintainability. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,37 +1,261 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { IconX, IconChevronDown, IconChevronRight, IconExternalLink } from '@tabler/icons';
|
||||
import ErrorBanner from 'ui/ErrorBanner';
|
||||
import CodeSnippet from 'components/CodeSnippet';
|
||||
import { getTreePathFromCollectionToItem } from 'utils/collections';
|
||||
import { normalizePath } from 'utils/common/path';
|
||||
import { addTab, updateRequestPaneTab, updateScriptPaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { updateSettingsSelectedTab, updatedFolderSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ScriptError = ({ item, onClose }) => {
|
||||
/**
|
||||
* Determines the source of a script error (request, folder, or collection)
|
||||
* based on the filePath from the error context.
|
||||
*
|
||||
* Bruno executes scripts at three levels in order: collection -> folder -> request.
|
||||
* When an error occurs, the filePath tells us which level it came from:
|
||||
*
|
||||
* filePath: "echo json.bru" -> request-level -> { sourceType: 'request', label: 'Request' }
|
||||
* filePath: "auth/folder.bru" -> folder-level -> { sourceType: 'folder', label: 'Folder: auth', sourceUid: 'f1' }
|
||||
* filePath: "collection.bru" -> collection-level -> { sourceType: 'collection', label: 'Collection' }
|
||||
*
|
||||
* For folder-level errors, this function walks the tree path from collection to
|
||||
* the current item to match the folder by its relative path, resolving its UID
|
||||
* and display name. If the folder can't be matched (e.g. missing tree data),
|
||||
* it falls back to a generic "Folder" label without a sourceUid.
|
||||
*
|
||||
* @param {string|undefined} filePath - Relative path from errorContext (e.g. "subfolder/folder.bru")
|
||||
* @param {object} item - The current request item
|
||||
* @param {object} collection - The parent collection (needs .pathname for folder matching)
|
||||
* @param {function} getTreePath - Function to get the tree path from collection root to item
|
||||
* @returns {{ sourceType: string, label: string, sourceUid?: string } | null}
|
||||
*/
|
||||
const getErrorSourceInfo = (filePath, item, collection, getTreePath) => {
|
||||
if (!filePath) return null;
|
||||
|
||||
// Normalize backslashes to forward slashes for cross-platform compatibility.
|
||||
// On Windows, path.relative() produces backslash separators, but the renderer
|
||||
// logic and regexes expect forward slashes.
|
||||
const normalizedPath = normalizePath(filePath);
|
||||
|
||||
const isFolderFile = /(?:^|\/)folder\.(?:bru|yml)$/.test(normalizedPath);
|
||||
const isCollectionFile = normalizedPath === 'collection.bru' || /^opencollection\.yml$/.test(normalizedPath);
|
||||
|
||||
// Folder level (check before collection to avoid folder.yml matching as collection)
|
||||
if (isFolderFile) {
|
||||
const info = { sourceType: 'folder', label: 'Folder' };
|
||||
const folderFileName = normalizedPath.split('/').pop();
|
||||
|
||||
// Try to find the folder UID and name from the tree path
|
||||
if (getTreePath && collection && item) {
|
||||
const collectionPathname = normalizePath(collection.pathname || '');
|
||||
const treePath = getTreePath(collection, item);
|
||||
if (treePath?.length) {
|
||||
for (const node of treePath) {
|
||||
if (node?.type === 'folder') {
|
||||
const nodePath = normalizePath(node.pathname || '');
|
||||
const folderRelPath = nodePath && nodePath.startsWith(collectionPathname)
|
||||
? nodePath.slice(collectionPathname.length).replace(/^\//, '') + '/' + folderFileName
|
||||
: folderFileName;
|
||||
if (folderRelPath === normalizedPath) {
|
||||
info.sourceUid = node.uid;
|
||||
info.label = `Folder: ${node.name}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
// Collection level
|
||||
if (isCollectionFile) {
|
||||
return { sourceType: 'collection', label: 'Collection' };
|
||||
}
|
||||
|
||||
// Request level
|
||||
return { sourceType: 'request', label: 'Request' };
|
||||
};
|
||||
|
||||
const ScriptErrorCard = ({ title, message, errorContext, item, collection, scriptPhase, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [showStack, setShowStack] = useState(false);
|
||||
|
||||
const displayFilePath = errorContext?.filePath ? normalizePath(errorContext.filePath) : null;
|
||||
|
||||
const sourceInfo = getErrorSourceInfo(
|
||||
errorContext?.filePath,
|
||||
item,
|
||||
collection,
|
||||
getTreePathFromCollectionToItem
|
||||
);
|
||||
|
||||
const canNavigate = sourceInfo
|
||||
&& collection?.uid
|
||||
&& (sourceInfo.sourceType === 'collection'
|
||||
|| (sourceInfo.sourceType === 'folder' && sourceInfo.sourceUid)
|
||||
|| (sourceInfo.sourceType === 'request' && item?.uid));
|
||||
|
||||
const handleNavigateKeyDown = (e) => {
|
||||
if (!canNavigate) return;
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleNavigate();
|
||||
}
|
||||
};
|
||||
|
||||
const handleNavigate = () => {
|
||||
if (!canNavigate) return;
|
||||
|
||||
// CollectionSettings expects 'tests', FolderSettings expects 'test'
|
||||
const collectionSettingsTab = scriptPhase === 'test' ? 'tests' : 'script';
|
||||
const folderSettingsTab = scriptPhase === 'test' ? 'test' : 'script';
|
||||
|
||||
if (sourceInfo.sourceType === 'collection') {
|
||||
dispatch(addTab({ uid: collection.uid, collectionUid: collection.uid, type: 'collection-settings' }));
|
||||
dispatch(updateSettingsSelectedTab({ collectionUid: collection.uid, tab: collectionSettingsTab }));
|
||||
if (collectionSettingsTab === 'script') {
|
||||
dispatch(updateScriptPaneTab({ uid: collection.uid, scriptPaneTab: scriptPhase }));
|
||||
}
|
||||
} else if (sourceInfo.sourceType === 'folder' && sourceInfo.sourceUid) {
|
||||
dispatch(addTab({ uid: sourceInfo.sourceUid, collectionUid: collection.uid, type: 'folder-settings' }));
|
||||
dispatch(updatedFolderSettingsSelectedTab({ collectionUid: collection.uid, folderUid: sourceInfo.sourceUid, tab: folderSettingsTab }));
|
||||
if (folderSettingsTab === 'script') {
|
||||
dispatch(updateScriptPaneTab({ uid: sourceInfo.sourceUid, scriptPaneTab: scriptPhase }));
|
||||
}
|
||||
} else if (sourceInfo.sourceType === 'request') {
|
||||
dispatch(addTab({ uid: item.uid, collectionUid: collection.uid, type: 'request' }));
|
||||
if (scriptPhase === 'test') {
|
||||
dispatch(updateRequestPaneTab({ uid: item.uid, requestPaneTab: 'tests' }));
|
||||
} else {
|
||||
dispatch(updateRequestPaneTab({ uid: item.uid, requestPaneTab: 'script' }));
|
||||
dispatch(updateScriptPaneTab({ uid: item.uid, scriptPaneTab: scriptPhase }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!errorContext) {
|
||||
return <ErrorBanner errors={[{ title, message }]} onClose={onClose} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="script-error-card" data-testid="script-error-card">
|
||||
<div className="script-error-header">
|
||||
<div className="error-title" data-testid="script-error-title">{title}</div>
|
||||
{onClose && (
|
||||
<button className="close-button flex-shrink-0 cursor-pointer" data-testid="script-error-close" onClick={onClose} aria-label="Close error">
|
||||
<IconX size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{(sourceInfo || displayFilePath) && (
|
||||
<div className="script-error-source-label" data-testid="script-error-source-label">
|
||||
{sourceInfo && <span>{sourceInfo.label}</span>}
|
||||
{displayFilePath && (
|
||||
<span
|
||||
className={`script-error-file-path${canNavigate ? ' navigable' : ''}`}
|
||||
data-testid="script-error-file-path"
|
||||
role={canNavigate ? 'button' : undefined}
|
||||
tabIndex={canNavigate ? 0 : undefined}
|
||||
onClick={handleNavigate}
|
||||
onKeyDown={handleNavigateKeyDown}
|
||||
title={canNavigate ? `Open ${displayFilePath}` : undefined}
|
||||
>
|
||||
<span>{displayFilePath}</span>
|
||||
{canNavigate && <IconExternalLink size={12} className="flex-shrink-0" />}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<CodeSnippet lines={errorContext.lines} variant="error" />
|
||||
<div className="script-error-message" data-testid="script-error-message">
|
||||
{errorContext.errorType || 'Error'}: {message}
|
||||
</div>
|
||||
{errorContext.stack && (
|
||||
<div>
|
||||
<button
|
||||
className="script-error-stack-toggle"
|
||||
data-testid="script-error-stack-toggle"
|
||||
onClick={() => setShowStack(!showStack)}
|
||||
aria-expanded={showStack}
|
||||
aria-label={`${showStack ? 'Hide' : 'Show'} stack trace`}
|
||||
>
|
||||
{showStack ? <IconChevronDown size={14} /> : <IconChevronRight size={14} />}
|
||||
<span>{showStack ? 'Hide' : 'Show'} stack trace</span>
|
||||
</button>
|
||||
{showStack && (
|
||||
<pre className="script-error-stack" data-testid="script-error-stack">{errorContext.stack}</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const ScriptError = ({ item, collection, onClose }) => {
|
||||
const preRequestError = item?.preRequestScriptErrorMessage;
|
||||
const postResponseError = item?.postResponseScriptErrorMessage;
|
||||
const testScriptError = item?.testScriptErrorMessage;
|
||||
|
||||
if (!preRequestError && !postResponseError && !testScriptError) return null;
|
||||
|
||||
const errors = [];
|
||||
const preRequestContext = item?.preRequestScriptErrorContext;
|
||||
const postResponseContext = item?.postResponseScriptErrorContext;
|
||||
const testContext = item?.testScriptErrorContext;
|
||||
|
||||
if (preRequestError) {
|
||||
errors.push({
|
||||
title: 'Pre-Request Script Error',
|
||||
message: preRequestError
|
||||
});
|
||||
const hasAnyContext = preRequestContext || postResponseContext || testContext;
|
||||
|
||||
// If no error context available for any error, fall back to ErrorBanner
|
||||
if (!hasAnyContext) {
|
||||
const errors = [];
|
||||
if (preRequestError) errors.push({ title: 'Pre-Request Script Error', message: preRequestError });
|
||||
if (postResponseError) errors.push({ title: 'Post-Response Script Error', message: postResponseError });
|
||||
if (testScriptError) errors.push({ title: 'Test Script Error', message: testScriptError });
|
||||
return <ErrorBanner errors={errors} onClose={onClose} className="mb-2" />;
|
||||
}
|
||||
|
||||
if (postResponseError) {
|
||||
errors.push({
|
||||
title: 'Post-Response Script Error',
|
||||
message: postResponseError
|
||||
});
|
||||
}
|
||||
|
||||
if (testScriptError) {
|
||||
errors.push({
|
||||
title: 'Test Script Error',
|
||||
message: testScriptError
|
||||
});
|
||||
}
|
||||
|
||||
return <ErrorBanner errors={errors} onClose={onClose} className="mt-4 mb-2" />;
|
||||
return (
|
||||
<div className="mb-2 flex flex-col gap-2">
|
||||
{preRequestError && (
|
||||
<ScriptErrorCard
|
||||
title="Pre-Request Script Error"
|
||||
message={preRequestError}
|
||||
errorContext={preRequestContext}
|
||||
item={item}
|
||||
collection={collection}
|
||||
scriptPhase="pre-request"
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
{postResponseError && (
|
||||
<ScriptErrorCard
|
||||
title="Post-Response Script Error"
|
||||
message={postResponseError}
|
||||
errorContext={postResponseContext}
|
||||
item={item}
|
||||
collection={collection}
|
||||
scriptPhase="post-response"
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
{testScriptError && (
|
||||
<ScriptErrorCard
|
||||
title="Test Script Error"
|
||||
message={testScriptError}
|
||||
errorContext={testContext}
|
||||
item={item}
|
||||
collection={collection}
|
||||
scriptPhase="test"
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScriptError;
|
||||
|
||||
Reference in New Issue
Block a user