diff --git a/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js b/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js index b3c5647b4..a117d48dd 100644 --- a/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js +++ b/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js @@ -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); } diff --git a/packages/bruno-app/src/components/CollectionSettings/Script/index.js b/packages/bruno-app/src/components/CollectionSettings/Script/index.js index ebfc795cd..8bceb42ad 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Script/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Script/index.js @@ -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({ diff --git a/packages/bruno-app/src/components/CollectionSettings/Tests/index.js b/packages/bruno-app/src/components/CollectionSettings/Tests/index.js index b17082154..e2832dca9 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Tests/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Tests/index.js @@ -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 (
These tests will run any time a request in this collection is sent.
diff --git a/packages/bruno-app/src/components/FolderSettings/Script/index.js b/packages/bruno-app/src/components/FolderSettings/Script/index.js index 3501f8da6..f3039aab0 100644 --- a/packages/bruno-app/src/components/FolderSettings/Script/index.js +++ b/packages/bruno-app/src/components/FolderSettings/Script/index.js @@ -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({ diff --git a/packages/bruno-app/src/components/FolderSettings/Tests/index.js b/packages/bruno-app/src/components/FolderSettings/Tests/index.js index 2488b4abf..b5997591a 100644 --- a/packages/bruno-app/src/components/FolderSettings/Tests/index.js +++ b/packages/bruno-app/src/components/FolderSettings/Tests/index.js @@ -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 (
These tests will run any time a request in this collection is sent.
diff --git a/packages/bruno-app/src/components/RequestPane/Script/index.js b/packages/bruno-app/src/components/RequestPane/Script/index.js index 2d8a15ed7..f7ad0ab66 100644 --- a/packages/bruno-app/src/components/RequestPane/Script/index.js +++ b/packages/bruno-app/src/components/RequestPane/Script/index.js @@ -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({ diff --git a/packages/bruno-app/src/components/RequestPane/Tests/index.js b/packages/bruno-app/src/components/RequestPane/Tests/index.js index 21146579b..a70133e25 100644 --- a/packages/bruno-app/src/components/RequestPane/Tests/index.js +++ b/packages/bruno-app/src/components/RequestPane/Tests/index.js @@ -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 ( diff --git a/packages/bruno-app/src/components/ResponsePane/ScriptError/index.js b/packages/bruno-app/src/components/ResponsePane/ScriptError/index.js index 1fee964c1..dcbdb2372 100644 --- a/packages/bruno-app/src/components/ResponsePane/ScriptError/index.js +++ b/packages/bruno-app/src/components/ResponsePane/ScriptError/index.js @@ -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)); } }; diff --git a/packages/bruno-app/src/hooks/useFocusErrorLine/index.js b/packages/bruno-app/src/hooks/useFocusErrorLine/index.js new file mode 100644 index 000000000..beec2044e --- /dev/null +++ b/packages/bruno-app/src/hooks/useFocusErrorLine/index.js @@ -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; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js index 3c38b277f..e362e4dbe 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js @@ -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, diff --git a/packages/bruno-app/src/utils/codemirror/focusErrorLine.js b/packages/bruno-app/src/utils/codemirror/focusErrorLine.js new file mode 100644 index 000000000..d4b3bddf5 --- /dev/null +++ b/packages/bruno-app/src/utils/codemirror/focusErrorLine.js @@ -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(); + }; +}; diff --git a/tests/script-errors/fixtures/collections/script-errors-test/long-pre-request-error.bru b/tests/script-errors/fixtures/collections/script-errors-test/long-pre-request-error.bru new file mode 100644 index 000000000..03c506b66 --- /dev/null +++ b/tests/script-errors/fixtures/collections/script-errors-test/long-pre-request-error.bru @@ -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'); +} diff --git a/tests/script-errors/focus-error-line.spec.ts b/tests/script-errors/focus-error-line.spec.ts new file mode 100644 index 000000000..c510e75ce --- /dev/null +++ b/tests/script-errors/focus-error-line.spec.ts @@ -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 => { + 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; + let commonLocators: ReturnType; + + 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'); + }); + }); +});