mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-24 21:25:45 +00:00
feat(app): scroll to and highlight error line on script error (#8183)
This commit is contained in:
@@ -165,6 +165,32 @@ const StyledWrapper = styled.div`
|
||||
background: ${(props) => props.theme.codemirror.searchLineHighlightCurrent};
|
||||
}
|
||||
|
||||
@keyframes cm-error-line-flash {
|
||||
0%, 60% {
|
||||
background-color: ${(props) => props.theme.status.danger.background};
|
||||
}
|
||||
100% {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror .cm-error-line-flash {
|
||||
background-color: transparent;
|
||||
animation: cm-error-line-flash 3s ease-in-out;
|
||||
}
|
||||
|
||||
.CodeMirror .cm-error-line-flash-gutter {
|
||||
color: ${(props) => props.theme.colors.text.danger} !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.CodeMirror .cm-error-line-flash {
|
||||
animation: none;
|
||||
background-color: ${(props) => props.theme.status.danger.background};
|
||||
}
|
||||
}
|
||||
|
||||
.cm-search-match {
|
||||
background: rgba(255, 193, 7, 0.25);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { flattenItems, isItemARequest } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Button from 'ui/Button';
|
||||
import { usePersistedState } from 'hooks/usePersistedState';
|
||||
import { useFocusErrorLine } from 'hooks/useFocusErrorLine';
|
||||
|
||||
const Script = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -60,6 +61,20 @@ const Script = ({ collection }) => {
|
||||
return () => clearTimeout(timer);
|
||||
}, [activeTab]);
|
||||
|
||||
useFocusErrorLine({
|
||||
uid: collection.uid,
|
||||
editorRef: preRequestEditorRef,
|
||||
scriptPhase: 'pre-request',
|
||||
isVisible: activeTab === 'pre-request'
|
||||
});
|
||||
|
||||
useFocusErrorLine({
|
||||
uid: collection.uid,
|
||||
editorRef: postResponseEditorRef,
|
||||
scriptPhase: 'post-response',
|
||||
isVisible: activeTab === 'post-response'
|
||||
});
|
||||
|
||||
const onRequestScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateCollectionRequestScript({
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Button from 'ui/Button';
|
||||
import { usePersistedState } from 'hooks/usePersistedState';
|
||||
import { useFocusErrorLine } from 'hooks/useFocusErrorLine';
|
||||
|
||||
const Tests = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -30,6 +31,12 @@ const Tests = ({ collection }) => {
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
useFocusErrorLine({
|
||||
uid: collection.uid,
|
||||
editorRef: testsEditorRef,
|
||||
scriptPhase: 'test'
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
<div className="text-xs mb-4 text-muted">These tests will run any time a request in this collection is sent.</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { flattenItems, isItemARequest } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Button from 'ui/Button';
|
||||
import { usePersistedState } from 'hooks/usePersistedState';
|
||||
import { useFocusErrorLine } from 'hooks/useFocusErrorLine';
|
||||
|
||||
const Script = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -63,6 +64,20 @@ const Script = ({ collection, folder }) => {
|
||||
return () => clearTimeout(timer);
|
||||
}, [activeTab]);
|
||||
|
||||
useFocusErrorLine({
|
||||
uid: folder.uid,
|
||||
editorRef: preRequestEditorRef,
|
||||
scriptPhase: 'pre-request',
|
||||
isVisible: activeTab === 'pre-request'
|
||||
});
|
||||
|
||||
useFocusErrorLine({
|
||||
uid: folder.uid,
|
||||
editorRef: postResponseEditorRef,
|
||||
scriptPhase: 'post-response',
|
||||
isVisible: activeTab === 'post-response'
|
||||
});
|
||||
|
||||
const onRequestScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateFolderRequestScript({
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Button from 'ui/Button';
|
||||
import { usePersistedState } from 'hooks/usePersistedState';
|
||||
import { useFocusErrorLine } from 'hooks/useFocusErrorLine';
|
||||
|
||||
const Tests = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -31,6 +32,12 @@ const Tests = ({ collection, folder }) => {
|
||||
|
||||
const handleSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
|
||||
useFocusErrorLine({
|
||||
uid: folder.uid,
|
||||
editorRef: testsEditorRef,
|
||||
scriptPhase: 'test'
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
<div className="text-xs mb-4 text-muted">These tests will run any time a request in this collection is sent.</div>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useTheme } from 'providers/Theme';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';
|
||||
import StatusDot from 'components/StatusDot';
|
||||
import { usePersistedState } from 'hooks/usePersistedState';
|
||||
import { useFocusErrorLine } from 'hooks/useFocusErrorLine';
|
||||
|
||||
const Script = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -57,6 +58,20 @@ const Script = ({ item, collection }) => {
|
||||
return () => clearTimeout(timer);
|
||||
}, [activeTab]);
|
||||
|
||||
useFocusErrorLine({
|
||||
uid: item.uid,
|
||||
editorRef: preRequestEditorRef,
|
||||
scriptPhase: 'pre-request',
|
||||
isVisible: activeTab === 'pre-request'
|
||||
});
|
||||
|
||||
useFocusErrorLine({
|
||||
uid: item.uid,
|
||||
editorRef: postResponseEditorRef,
|
||||
scriptPhase: 'post-response',
|
||||
isVisible: activeTab === 'post-response'
|
||||
});
|
||||
|
||||
const onRequestScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateRequestScript({
|
||||
|
||||
@@ -8,6 +8,7 @@ import { updateRequestTests } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { usePersistedState } from 'hooks/usePersistedState';
|
||||
import { useFocusErrorLine } from 'hooks/useFocusErrorLine';
|
||||
|
||||
const Tests = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -31,6 +32,12 @@ const Tests = ({ item, collection }) => {
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
useFocusErrorLine({
|
||||
uid: item.uid,
|
||||
editorRef: testsEditorRef,
|
||||
scriptPhase: 'test'
|
||||
});
|
||||
|
||||
const requestContext = useMemo(() => buildRequestContextFromItem(item), [item]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,7 +5,7 @@ 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 { addTab, updateRequestPaneTab, updateScriptPaneTab, setFocusErrorLine } from 'providers/ReduxStore/slices/tabs';
|
||||
import { updateSettingsSelectedTab, updatedFolderSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
@@ -114,18 +114,28 @@ const ScriptErrorCard = ({ title, message, errorContext, item, collection, scrip
|
||||
const collectionSettingsTab = scriptPhase === 'test' ? 'tests' : 'script';
|
||||
const folderSettingsTab = scriptPhase === 'test' ? 'test' : 'script';
|
||||
|
||||
const errorLine = errorContext?.errorLine;
|
||||
const focusPayload = (uid) =>
|
||||
typeof errorLine === 'number'
|
||||
? { uid, scriptPhase, line: errorLine, requestedAt: Date.now() }
|
||||
: null;
|
||||
|
||||
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 }));
|
||||
}
|
||||
const payload = focusPayload(collection.uid);
|
||||
if (payload) dispatch(setFocusErrorLine(payload));
|
||||
} 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 }));
|
||||
}
|
||||
const payload = focusPayload(sourceInfo.sourceUid);
|
||||
if (payload) dispatch(setFocusErrorLine(payload));
|
||||
} else if (sourceInfo.sourceType === 'request') {
|
||||
dispatch(addTab({ uid: item.uid, collectionUid: collection.uid, type: 'request' }));
|
||||
if (scriptPhase === 'test') {
|
||||
@@ -134,6 +144,8 @@ const ScriptErrorCard = ({ title, message, errorContext, item, collection, scrip
|
||||
dispatch(updateRequestPaneTab({ uid: item.uid, requestPaneTab: 'script' }));
|
||||
dispatch(updateScriptPaneTab({ uid: item.uid, scriptPaneTab: scriptPhase }));
|
||||
}
|
||||
const payload = focusPayload(item.uid);
|
||||
if (payload) dispatch(setFocusErrorLine(payload));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
59
packages/bruno-app/src/hooks/useFocusErrorLine/index.js
Normal file
59
packages/bruno-app/src/hooks/useFocusErrorLine/index.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { clearFocusErrorLine } from 'providers/ReduxStore/slices/tabs';
|
||||
import { focusErrorLine } from 'utils/codemirror/focusErrorLine';
|
||||
|
||||
/**
|
||||
* Subscribes a CodeMirror-hosting component to the tab's `focusErrorLine` signal.
|
||||
* When the signal targets this host's `scriptPhase`, scrolls the editor to the
|
||||
* line and flashes a red highlight that fades over ~3s. Re-firing for the same
|
||||
* line is handled via the `requestedAt` token.
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {string} params.uid Tab uid (request/folder/collection uid)
|
||||
* @param {React.RefObject} params.editorRef Ref to a CodeEditor component (exposes `.editor`)
|
||||
* @param {string} params.scriptPhase 'pre-request' | 'post-response' | 'test'
|
||||
* @param {boolean} [params.isVisible=true] Whether this editor's tab is currently shown
|
||||
*/
|
||||
export const useFocusErrorLine = ({ uid, editorRef, scriptPhase, isVisible = true }) => {
|
||||
const dispatch = useDispatch();
|
||||
const focusErrorLineState = useSelector((state) => {
|
||||
const tab = find(state.tabs.tabs, (t) => t.uid === uid);
|
||||
return tab?.focusErrorLine || null;
|
||||
});
|
||||
|
||||
const disposeRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!focusErrorLineState || !isVisible) return;
|
||||
|
||||
if (focusErrorLineState.scriptPhase !== scriptPhase) return;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
const editor = editorRef.current?.editor;
|
||||
if (!editor) return;
|
||||
|
||||
if (disposeRef.current) {
|
||||
disposeRef.current();
|
||||
disposeRef.current = null;
|
||||
}
|
||||
|
||||
disposeRef.current = focusErrorLine(editor, focusErrorLineState.line);
|
||||
dispatch(clearFocusErrorLine({ uid }));
|
||||
}, 0);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [focusErrorLineState?.requestedAt, focusErrorLineState?.line, focusErrorLineState?.scriptPhase, isVisible, scriptPhase, uid]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (disposeRef.current) {
|
||||
disposeRef.current();
|
||||
disposeRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
export default useFocusErrorLine;
|
||||
@@ -280,6 +280,24 @@ export const tabsSlice = createSlice({
|
||||
tab.scriptPaneTab = action.payload.scriptPaneTab;
|
||||
}
|
||||
},
|
||||
setFocusErrorLine: (state, action) => {
|
||||
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
|
||||
|
||||
if (tab) {
|
||||
tab.focusErrorLine = {
|
||||
scriptPhase: action.payload.scriptPhase,
|
||||
line: action.payload.line,
|
||||
requestedAt: action.payload.requestedAt
|
||||
};
|
||||
}
|
||||
},
|
||||
clearFocusErrorLine: (state, action) => {
|
||||
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
|
||||
|
||||
if (tab) {
|
||||
tab.focusErrorLine = null;
|
||||
}
|
||||
},
|
||||
updateQueryBuilderOpen: (state, action) => {
|
||||
const tab = find(state.tabs, (t) => t.uid === action.payload.uid);
|
||||
|
||||
@@ -519,6 +537,8 @@ export const {
|
||||
updateGqlDocsOpen,
|
||||
updateTableColumnWidths,
|
||||
updateScriptPaneTab,
|
||||
setFocusErrorLine,
|
||||
clearFocusErrorLine,
|
||||
closeTabs,
|
||||
closeAllCollectionTabs,
|
||||
makeTabPermanent,
|
||||
|
||||
40
packages/bruno-app/src/utils/codemirror/focusErrorLine.js
Normal file
40
packages/bruno-app/src/utils/codemirror/focusErrorLine.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const LINE_CLASS_TARGET = 'background';
|
||||
const LINE_CLASS_NAME = 'cm-error-line-flash';
|
||||
const GUTTER_CLASS_TARGET = 'gutter';
|
||||
const GUTTER_CLASS_NAME = 'cm-error-line-flash-gutter';
|
||||
|
||||
export const focusErrorLine = (editor, line1Based, { durationMs = 3000 } = {}) => {
|
||||
if (!editor || typeof line1Based !== 'number' || Number.isNaN(line1Based)) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const lineCount = editor.lineCount();
|
||||
const line = Math.max(0, Math.min(line1Based - 1, lineCount - 1));
|
||||
|
||||
try {
|
||||
editor.scrollIntoView({ line, ch: 0 }, 80);
|
||||
editor.addLineClass(line, LINE_CLASS_TARGET, LINE_CLASS_NAME);
|
||||
editor.addLineClass(line, GUTTER_CLASS_TARGET, GUTTER_CLASS_NAME);
|
||||
} catch (e) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
let disposed = false;
|
||||
const dispose = () => {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
try {
|
||||
editor.removeLineClass(line, LINE_CLASS_TARGET, LINE_CLASS_NAME);
|
||||
editor.removeLineClass(line, GUTTER_CLASS_TARGET, GUTTER_CLASS_NAME);
|
||||
} catch (e) {
|
||||
// editor may have been swapped out; nothing to clean up
|
||||
}
|
||||
};
|
||||
|
||||
const timer = setTimeout(dispose, durationMs);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
dispose();
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
meta {
|
||||
name: long-pre-request-error
|
||||
type: http
|
||||
seq: 12
|
||||
}
|
||||
|
||||
get {
|
||||
url: http://localhost:8081/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
console.log('line 1');
|
||||
console.log('line 2');
|
||||
console.log('line 3');
|
||||
console.log('line 4');
|
||||
console.log('line 5');
|
||||
console.log('line 6');
|
||||
console.log('line 7');
|
||||
console.log('line 8');
|
||||
console.log('line 9');
|
||||
console.log('line 10');
|
||||
console.log('line 11');
|
||||
console.log('line 12');
|
||||
console.log('line 13');
|
||||
console.log('line 14');
|
||||
console.log('line 15');
|
||||
console.log('line 16');
|
||||
console.log('line 17');
|
||||
console.log('line 18');
|
||||
console.log('line 19');
|
||||
console.log('line 20');
|
||||
console.log('line 21');
|
||||
console.log('line 22');
|
||||
console.log('line 23');
|
||||
console.log('line 24');
|
||||
console.log('line 25');
|
||||
console.log('line 26');
|
||||
console.log('line 27');
|
||||
console.log('line 28');
|
||||
console.log('line 29');
|
||||
console.log('line 30');
|
||||
console.log('line 31');
|
||||
console.log('line 32');
|
||||
console.log('line 33');
|
||||
console.log('line 34');
|
||||
console.log('line 35');
|
||||
console.log('line 36');
|
||||
console.log('line 37');
|
||||
console.log('line 38');
|
||||
console.log('line 39');
|
||||
console.log('line 40');
|
||||
console.log('line 41');
|
||||
console.log('line 42');
|
||||
console.log('line 43');
|
||||
console.log('line 44');
|
||||
console.log('line 45');
|
||||
console.log('line 46');
|
||||
console.log('line 47');
|
||||
console.log('line 48');
|
||||
console.log('line 49');
|
||||
console.log('line 50');
|
||||
console.log('line 51');
|
||||
console.log('line 52');
|
||||
console.log('line 53');
|
||||
console.log('line 54');
|
||||
console.log('line 55');
|
||||
console.log('line 56');
|
||||
console.log('line 57');
|
||||
console.log('line 58');
|
||||
console.log('line 59');
|
||||
console.log('line 60');
|
||||
console.log('line 61');
|
||||
console.log('line 62');
|
||||
console.log('line 63');
|
||||
console.log('line 64');
|
||||
console.log('line 65');
|
||||
console.log('line 66');
|
||||
console.log('line 67');
|
||||
console.log('line 68');
|
||||
console.log('line 69');
|
||||
console.log('line 70');
|
||||
longScriptUndefinedVar.boom();
|
||||
console.log('after error');
|
||||
}
|
||||
175
tests/script-errors/focus-error-line.spec.ts
Normal file
175
tests/script-errors/focus-error-line.spec.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { test, expect, Page } from '../../playwright';
|
||||
import { buildScriptErrorLocators, buildCommonLocators } from '../utils/page/locators';
|
||||
import { openRequest, sendAndWaitForErrorCard, sendAndWaitForResponse, closeAllTabs } from '../utils/page/actions';
|
||||
import { setSandboxMode } from '../utils/page/runner';
|
||||
|
||||
/**
|
||||
* Resolves the CodeMirror scroller `scrollTop` for an editor inside a given
|
||||
* test-id container. Returns null if the container/scroller is not present.
|
||||
*/
|
||||
const getScrollerScrollTop = async (page: Page, dataTestId: string): Promise<number | null> => {
|
||||
return page.evaluate((id) => {
|
||||
const root = document.querySelector(`[data-testid="${id}"]`);
|
||||
const scroller = root?.querySelector('.CodeMirror-scroll') as HTMLElement | null;
|
||||
return scroller ? scroller.scrollTop : null;
|
||||
}, dataTestId);
|
||||
};
|
||||
|
||||
test.describe('Script Error — focus error line (highlight + scroll)', () => {
|
||||
let scriptErrorLocators: ReturnType<typeof buildScriptErrorLocators>;
|
||||
let commonLocators: ReturnType<typeof buildCommonLocators>;
|
||||
|
||||
test.beforeAll(async ({ pageWithUserData: page }) => {
|
||||
scriptErrorLocators = buildScriptErrorLocators(page);
|
||||
commonLocators = buildCommonLocators(page);
|
||||
// Highlight/scroll is a pure UI concern — pick one sandbox mode.
|
||||
await setSandboxMode(page, 'script-errors-test', 'developer');
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ pageWithUserData: page }) => {
|
||||
await closeAllTabs(page);
|
||||
});
|
||||
|
||||
test('Clicking file path adds the error-line highlight class', async ({ pageWithUserData: page }) => {
|
||||
await test.step('Open request and send', async () => {
|
||||
await openRequest(page, 'script-errors-test', 'pre-request-ref-error');
|
||||
await sendAndWaitForErrorCard(page);
|
||||
});
|
||||
|
||||
await test.step('Click file path to navigate', async () => {
|
||||
const card = scriptErrorLocators.card();
|
||||
await scriptErrorLocators.filePath(card).click();
|
||||
});
|
||||
|
||||
await test.step('Pre-request editor shows flash class on the error line', async () => {
|
||||
const scriptTab = commonLocators.paneTabs.responsiveTab('script');
|
||||
await expect(scriptTab).toHaveClass(/active/);
|
||||
|
||||
const flashedLine = page
|
||||
.getByTestId('pre-request-script-editor')
|
||||
.locator('.CodeMirror .cm-error-line-flash');
|
||||
await expect(flashedLine).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('Highlight clears automatically after the flash duration', async ({ pageWithUserData: page }) => {
|
||||
await test.step('Open request, send, then navigate', async () => {
|
||||
await openRequest(page, 'script-errors-test', 'pre-request-ref-error');
|
||||
await sendAndWaitForErrorCard(page);
|
||||
await scriptErrorLocators.filePath(scriptErrorLocators.card()).click();
|
||||
});
|
||||
|
||||
await test.step('Flash class is present immediately', async () => {
|
||||
const flashedLine = page
|
||||
.getByTestId('pre-request-script-editor')
|
||||
.locator('.CodeMirror .cm-error-line-flash');
|
||||
await expect(flashedLine).toHaveCount(1);
|
||||
});
|
||||
|
||||
await test.step('Flash class is gone after ~3s', async () => {
|
||||
const flashedLine = page
|
||||
.getByTestId('pre-request-script-editor')
|
||||
.locator('.CodeMirror .cm-error-line-flash');
|
||||
// Helper's default duration is 3000ms. Allow 4s for animation end + cleanup.
|
||||
await expect(flashedLine).toHaveCount(0, { timeout: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
test('Re-clicking the file path re-triggers the highlight', async ({ pageWithUserData: page }) => {
|
||||
await test.step('Open request and send', async () => {
|
||||
await openRequest(page, 'script-errors-test', 'pre-request-ref-error');
|
||||
await sendAndWaitForErrorCard(page);
|
||||
});
|
||||
|
||||
const flashedLine = page
|
||||
.getByTestId('pre-request-script-editor')
|
||||
.locator('.CodeMirror .cm-error-line-flash');
|
||||
|
||||
await test.step('First click flashes the line, then it fades', async () => {
|
||||
await scriptErrorLocators.filePath(scriptErrorLocators.card()).click();
|
||||
await expect(flashedLine).toHaveCount(1);
|
||||
await expect(flashedLine).toHaveCount(0, { timeout: 5000 });
|
||||
});
|
||||
|
||||
await test.step('Second click flashes the line again', async () => {
|
||||
await scriptErrorLocators.filePath(scriptErrorLocators.card()).click();
|
||||
await expect(flashedLine).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('Post-response error navigates to post-response sub-tab and flashes', async ({ pageWithUserData: page }) => {
|
||||
await test.step('Open request and send', async () => {
|
||||
await openRequest(page, 'script-errors-test', 'post-response-type-error');
|
||||
await sendAndWaitForResponse(page);
|
||||
});
|
||||
|
||||
await test.step('Click file path to navigate', async () => {
|
||||
const card = scriptErrorLocators.card();
|
||||
await expect(card).toBeVisible();
|
||||
await scriptErrorLocators.filePath(card).click();
|
||||
});
|
||||
|
||||
await test.step('Post Response sub-tab is active', async () => {
|
||||
const postResponseSubTab = commonLocators.paneTabs.tabTrigger('post-response');
|
||||
await expect(postResponseSubTab).toHaveClass(/active/);
|
||||
});
|
||||
|
||||
await test.step('Post-response editor shows flash class on the error line', async () => {
|
||||
const flashedLine = page
|
||||
.getByTestId('post-response-script-editor')
|
||||
.locator('.CodeMirror .cm-error-line-flash');
|
||||
await expect(flashedLine).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('Tests editor flashes the error line for test-script errors', async ({ pageWithUserData: page }) => {
|
||||
await test.step('Open request and send', async () => {
|
||||
await openRequest(page, 'script-errors-test', 'test-script-error');
|
||||
await sendAndWaitForResponse(page);
|
||||
});
|
||||
|
||||
await test.step('Click file path to navigate', async () => {
|
||||
const card = scriptErrorLocators.card();
|
||||
await expect(card).toBeVisible();
|
||||
await scriptErrorLocators.filePath(card).click();
|
||||
});
|
||||
|
||||
await test.step('Tests editor shows flash class on the error line', async () => {
|
||||
const flashedLine = page
|
||||
.getByTestId('test-script-editor')
|
||||
.locator('.CodeMirror .cm-error-line-flash');
|
||||
await expect(flashedLine).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('Long-script error scrolls the editor so the line is visible', async ({ pageWithUserData: page }) => {
|
||||
await test.step('Open long-script request and send', async () => {
|
||||
await openRequest(page, 'script-errors-test', 'long-pre-request-error');
|
||||
await sendAndWaitForErrorCard(page);
|
||||
});
|
||||
|
||||
await test.step('Click file path to navigate', async () => {
|
||||
const card = scriptErrorLocators.card();
|
||||
await scriptErrorLocators.filePath(card).click();
|
||||
});
|
||||
|
||||
await test.step('Editor scrolled — scrollTop is non-zero', async () => {
|
||||
// Wait for the highlight to land, then sample the scroller's scrollTop.
|
||||
const flashedLine = page
|
||||
.getByTestId('pre-request-script-editor')
|
||||
.locator('.CodeMirror .cm-error-line-flash');
|
||||
await expect(flashedLine).toHaveCount(1);
|
||||
|
||||
const scrollTop = await getScrollerScrollTop(page, 'pre-request-script-editor');
|
||||
expect(scrollTop).not.toBeNull();
|
||||
expect(scrollTop!).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
await test.step('The flashed line is the one carrying the error', async () => {
|
||||
const flashedRow = page
|
||||
.getByTestId('pre-request-script-editor')
|
||||
.locator('.CodeMirror-code > div', { has: page.locator('.cm-error-line-flash') });
|
||||
await expect(flashedRow).toContainText('longScriptUndefinedVar');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user