From 6717035dd2dce9922453fab6219531ef6ecf0a3a Mon Sep 17 00:00:00 2001 From: Pooja Date: Thu, 11 Jun 2026 15:10:57 +0530 Subject: [PATCH] fix(timeline): scope scripted requests to their own request (#8210) * fix(timeline): scope scripted requests to their own request * fix: oauth playwright test --- .../TimelineItem/Common/Body/index.js | 2 +- .../TimelineItem/Common/Headers/index.js | 2 +- .../TimelineItem/Common/Status/index.js | 1 + .../Timeline/TimelineItem/index.js | 11 +- .../ResponsePane/Timeline/entryMeta.js | 9 +- .../components/ResponsePane/Timeline/index.js | 7 +- .../bruno-electron/src/utils/collection.js | 6 +- .../timeline-nested-runrequest.spec.ts | 31 +++-- .../timeline-runrequest-network-error.spec.ts | 6 +- .../timeline/timeline-runrequest-skip.spec.ts | 8 +- ...imeline-scoped-request-attribution.spec.ts | 117 ++++++++++++++++++ .../timeline-scripted-requests.spec.ts | 56 ++++----- .../timeline/timeline-url-update.spec.ts | 2 +- 13 files changed, 189 insertions(+), 69 deletions(-) create mode 100644 tests/request/timeline/timeline-scoped-request-attribution.spec.ts diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Body/index.js b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Body/index.js index c71810830..7ad58cf33 100644 --- a/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Body/index.js +++ b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Body/index.js @@ -36,7 +36,7 @@ const BodyBlock = ({ collection, data, dataBuffer, headers, error, item, type }) /> ) : ( -
No Body found
+
No Body
) )} diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Headers/index.js b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Headers/index.js index 27e6fc82f..cc8ab96d9 100644 --- a/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Headers/index.js +++ b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Headers/index.js @@ -31,7 +31,7 @@ const Headers = ({ headers }) => { {isOpen && ( count === 0 - ?
No Headers found
+ ?
No Headers
: ( diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Status/index.js b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Status/index.js index f819caed8..4666947f2 100644 --- a/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Status/index.js +++ b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Status/index.js @@ -21,6 +21,7 @@ const Status = ({ statusCode }) => { return ( -
+
-
{url}
+
{url}
- {badge.badgeLabel} + {badge.badgeLabel}
{!hideTimestamp && (
@@ -167,7 +167,7 @@ const TimelineItem = ({
{isExpanded && ( -
+
{method} @@ -179,8 +179,9 @@ const TimelineItem = ({ href="#" title={canNavigate ? `Open ${sourceFile}` : sourceFile} onClick={canNavigate ? handleNavigate : (ev) => ev.preventDefault()} + data-testid="timeline-source-link" > - {sourceFile} + {sourceFile} )} diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/entryMeta.js b/packages/bruno-app/src/components/ResponsePane/Timeline/entryMeta.js index addaf31d9..ff97291ef 100644 --- a/packages/bruno-app/src/components/ResponsePane/Timeline/entryMeta.js +++ b/packages/bruno-app/src/components/ResponsePane/Timeline/entryMeta.js @@ -1,9 +1,10 @@ // Keys must match getEntryKind() in buildEntries.js. +// `kind` is a stable identifier used for data-testids (e.g. timeline-badge-pre). export const ENTRY_KINDS = { - main: { chipLabel: 'Main', badgeLabel: 'main', badgeClass: 'tl-badge tl-badge--main' }, - oauth: { chipLabel: 'OAuth', badgeLabel: 'oauth2.0', badgeClass: 'tl-badge tl-badge--oauth2' }, - pre: { chipLabel: 'Pre-Request', badgeLabel: 'sendRequest', badgeClass: 'tl-badge tl-badge--scripted' }, - post: { chipLabel: 'Post-Response', badgeLabel: 'runRequest', badgeClass: 'tl-badge tl-badge--run-request' } + main: { kind: 'main', chipLabel: 'Request', badgeLabel: 'request', badgeClass: 'tl-badge tl-badge--main' }, + oauth: { kind: 'oauth', chipLabel: 'OAuth', badgeLabel: 'oauth2.0', badgeClass: 'tl-badge tl-badge--oauth2' }, + pre: { kind: 'pre', chipLabel: 'Pre-Request', badgeLabel: 'sendRequest', badgeClass: 'tl-badge tl-badge--scripted' }, + post: { kind: 'post', chipLabel: 'Post-Response', badgeLabel: 'runRequest', badgeClass: 'tl-badge tl-badge--run-request' } }; export const FILTER_CHIPS = [ diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/index.js b/packages/bruno-app/src/components/ResponsePane/Timeline/index.js index e6d12a41f..65b660033 100644 --- a/packages/bruno-app/src/components/ResponsePane/Timeline/index.js +++ b/packages/bruno-app/src/components/ResponsePane/Timeline/index.js @@ -78,22 +78,23 @@ const Timeline = ({ collection, item }) => { ref={wrapperRef} > {showFilterBar && ( -
+
{visibleChips.map((chip) => ( ))}
)} -
+
{entries.map((entry, index) => { const kind = getEntryKind(entry); if (activeFilter !== 'all' && activeFilter !== kind) return null; diff --git a/packages/bruno-electron/src/utils/collection.js b/packages/bruno-electron/src/utils/collection.js index 39018e6cc..6c642b9cc 100644 --- a/packages/bruno-electron/src/utils/collection.js +++ b/packages/bruno-electron/src/utils/collection.js @@ -274,8 +274,10 @@ const mergeScripts = (collection, request, requestTreePath, scriptFlow) => { displayPath: config.collectionFile }; - const requestSegmentSource = request?.pathname && collection?.pathname - ? { displayPath: posixifyPath(path.relative(collection.pathname, request.pathname)) } + const requestItem = requestTreePath?.[requestTreePath.length - 1]; + const requestPathname = request?.pathname || requestItem?.pathname; + const requestSegmentSource = requestPathname && collection?.pathname + ? { displayPath: posixifyPath(path.relative(collection.pathname, requestPathname)) } : null; const withContent = (source, script) => diff --git a/tests/request/timeline/timeline-nested-runrequest.spec.ts b/tests/request/timeline/timeline-nested-runrequest.spec.ts index 30bf6012d..887a0aad4 100644 --- a/tests/request/timeline/timeline-nested-runrequest.spec.ts +++ b/tests/request/timeline/timeline-nested-runrequest.spec.ts @@ -55,32 +55,31 @@ test.describe('Timeline — nested bru.runRequest bubbles inner scripted entries await test.step('Outer Timeline shows three rows: main + runRequest + bubbled inner sendRequest', async () => { await selectResponsePaneTab(page, 'Timeline'); - const rows = page.locator('.timeline-container .tl-row-wrap'); + const rows = page.getByTestId('timeline-container').getByTestId('timeline-entry'); // Without the fix: 2 (main + runRequest); inner sendRequest is dropped. await expect(rows).toHaveCount(3); // Badge mix guards against an accidental wrong-3-rows pass. - await expect(rows.locator('.tl-badge--main')).toHaveCount(1); - await expect(rows.locator('.tl-badge--run-request')).toHaveCount(1); - await expect(rows.locator('.tl-badge--scripted')).toHaveCount(1); + await expect(rows.getByTestId('timeline-badge-main')).toHaveCount(1); + await expect(rows.getByTestId('timeline-badge-post')).toHaveCount(1); + await expect(rows.getByTestId('timeline-badge-pre')).toHaveCount(1); }); await test.step('Bubbled sendRequest row targets the inner-script URL (proving it came from inner)', async () => { - const rows = page.locator('.timeline-container .tl-row-wrap'); - const scriptedRow = rows.filter({ has: page.locator('.tl-badge--scripted') }); + const rows = page.getByTestId('timeline-container').getByTestId('timeline-entry'); + const scriptedRow = rows.filter({ has: page.getByTestId('timeline-badge-pre') }); await expect(scriptedRow).toHaveCount(1); - await expect(scriptedRow.locator('.tl-col-url')).toContainText('/headers'); + await expect(scriptedRow.getByTestId('timeline-url')).toContainText('/headers'); }); await test.step('Filter chips count the bubbled entry under Pre-Request', async () => { - const chips = page.locator('.timeline-filter-bar .timeline-chip'); - const countFor = (label: string) => - chips.filter({ hasText: label }).locator('.timeline-chip-count').first(); + const countFor = (id: string) => + page.getByTestId(`timeline-chip-${id}`).getByTestId('timeline-chip-count'); - await expect(countFor('All')).toHaveText('3'); - await expect(countFor('Main')).toHaveText('1'); + await expect(countFor('all')).toHaveText('3'); + await expect(countFor('main')).toHaveText('1'); // runRequest + bubbled sendRequest both ran during outer's pre-request. - await expect(countFor('Pre-Request')).toHaveText('2'); + await expect(countFor('pre')).toHaveText('2'); }); }); @@ -119,13 +118,13 @@ test.describe('Timeline — nested bru.runRequest bubbles inner scripted entries await test.step('Outer Timeline shows the bubbled post-response sendRequest row', async () => { await selectResponsePaneTab(page, 'Timeline'); - const rows = page.locator('.timeline-container .tl-row-wrap'); + const rows = page.getByTestId('timeline-container').getByTestId('timeline-entry'); await expect(rows).toHaveCount(3); // URL match confirms the scripted row is the post-response one. - const scriptedRow = rows.filter({ has: page.locator('.tl-badge--scripted') }); + const scriptedRow = rows.filter({ has: page.getByTestId('timeline-badge-pre') }); await expect(scriptedRow).toHaveCount(1); - await expect(scriptedRow.locator('.tl-col-url')).toContainText('/query'); + await expect(scriptedRow.getByTestId('timeline-url')).toContainText('/query'); }); }); }); diff --git a/tests/request/timeline/timeline-runrequest-network-error.spec.ts b/tests/request/timeline/timeline-runrequest-network-error.spec.ts index b6f601357..96d4c8e08 100644 --- a/tests/request/timeline/timeline-runrequest-network-error.spec.ts +++ b/tests/request/timeline/timeline-runrequest-network-error.spec.ts @@ -47,13 +47,13 @@ test.describe('Timeline — runRequest network-error row shows URL and error cod await test.step('Outer Timeline has the runRequest row with inner URL (URL fallback)', async () => { await selectResponsePaneTab(page, 'Timeline'); - const rows = page.locator('.timeline-container .tl-row-wrap'); + const rows = page.getByTestId('timeline-container').getByTestId('timeline-entry'); await expect(rows).toHaveCount(2); // main + runRequest // Without the URL fallback this column would be empty. - const runRequestRow = rows.filter({ has: page.locator('.tl-badge--run-request') }); + const runRequestRow = rows.filter({ has: page.getByTestId('timeline-badge-post') }); await expect(runRequestRow).toHaveCount(1); - await expect(runRequestRow.locator('.tl-col-url')).toContainText('localhost:9999'); + await expect(runRequestRow.getByTestId('timeline-url')).toContainText('localhost:9999'); }); }); }); diff --git a/tests/request/timeline/timeline-runrequest-skip.spec.ts b/tests/request/timeline/timeline-runrequest-skip.spec.ts index 794c46a33..3cb4766ff 100644 --- a/tests/request/timeline/timeline-runrequest-skip.spec.ts +++ b/tests/request/timeline/timeline-runrequest-skip.spec.ts @@ -42,13 +42,13 @@ test.describe('Timeline — bru.runRequest skips unsupported item types', () => await test.step('Timeline has main + two Skipped runRequest rows', async () => { await selectResponsePaneTab(page, 'Timeline'); - const rows = page.locator('.timeline-container .tl-row-wrap'); + const rows = page.getByTestId('timeline-container').getByTestId('timeline-entry'); await expect(rows).toHaveCount(3); - const skippedRows = rows.filter({ has: page.locator('.tl-badge--run-request') }); + const skippedRows = rows.filter({ has: page.getByTestId('timeline-badge-post') }); await expect(skippedRows).toHaveCount(2); - await expect(skippedRows.nth(0).locator('.timeline-status')).toContainText('Skipped'); - await expect(skippedRows.nth(1).locator('.timeline-status')).toContainText('Skipped'); + await expect(skippedRows.nth(0).getByTestId('timeline-status')).toContainText('Skipped'); + await expect(skippedRows.nth(1).getByTestId('timeline-status')).toContainText('Skipped'); }); }); }); diff --git a/tests/request/timeline/timeline-scoped-request-attribution.spec.ts b/tests/request/timeline/timeline-scoped-request-attribution.spec.ts new file mode 100644 index 000000000..7c2509a20 --- /dev/null +++ b/tests/request/timeline/timeline-scoped-request-attribution.spec.ts @@ -0,0 +1,117 @@ +import { test, expect } from '../../../playwright'; +import { + closeAllCollections, + createCollection, + createFolder, + createRequest, + expandFolder, + openRequest, + addCollectionScript, + addFolderScript, + addPreRequestScript, + saveRequest, + sendRequest, + selectResponsePaneTab +} from '../../utils/page/actions'; + +test.describe('Timeline — scoped request attribution', () => { + test.afterEach(async ({ page }) => { + await closeAllCollections(page); + }); + + test('request-level sendRequest is attributed to the request, not the collection script', async ({ + page, + createTmpDir + }) => { + const collectionName = 'timeline-scope-collection'; + const requestName = 'scoped-driver'; + const url = 'http://localhost:8081/ping'; + + await test.step('Create collection with a (non-empty) collection-level pre-request script', async () => { + await createCollection(page, collectionName, await createTmpDir(collectionName), 'yml'); + // Non-empty collection script => stamps the collection scope before the request runs. + await addCollectionScript(page, collectionName, 'pre-request', `bru.setVar('collectionRan', true);`); + }); + + await test.step('Create a request whose pre-request script issues a sendRequest', async () => { + await createRequest(page, requestName, collectionName, { url }); + await openRequest(page, collectionName, requestName); + await addPreRequestScript(page, `await bru.sendRequest({ url: "${url}", method: "GET" });`); + await saveRequest(page); + }); + + await test.step('Send the request', async () => { + await sendRequest(page, 200); + }); + + const scriptedRow = page + .getByTestId('timeline-entry') + .filter({ has: page.getByTestId('timeline-badge-pre') }); + + await test.step('Open Timeline and expand the scripted (sendRequest) row', async () => { + await selectResponsePaneTab(page, 'Timeline'); + await expect(scriptedRow).toHaveCount(1); + await scriptedRow.getByTestId('timeline-item-header').click(); + }); + + await test.step('Source file points to the request, not the collection', async () => { + const sourceFile = scriptedRow.getByTestId('timeline-source-file'); + await expect(sourceFile).toBeVisible(); + await expect(sourceFile).toHaveText('scoped-driver.yml'); + await expect(sourceFile).not.toContainText('opencollection.yml'); + }); + + await test.step('Clicking the source link opens the request Script tab', async () => { + await scriptedRow.getByTestId('timeline-source-link').click(); + await expect(page.locator('.request-tab.active')).toContainText(requestName); + await expect(page.getByTestId('responsive-tab-script')).toHaveClass(/active/); + }); + }); + + test('request-level sendRequest is attributed to the request, not the parent folder script', async ({ + page, + createTmpDir + }) => { + const collectionName = 'timeline-scope-folder'; + const folderName = 'auth'; + const requestName = 'folder-driver'; + const url = 'http://localhost:8081/ping'; + + await test.step('Create a folder with a (non-empty) folder-level pre-request script', async () => { + await createCollection(page, collectionName, await createTmpDir(collectionName), 'yml'); + await createFolder(page, folderName, collectionName); + await expandFolder(page, folderName); + // Folder script runs after the collection and overwrites the scope — used to + // be what a nested request's sendRequest inherited. + await addFolderScript(page, folderName, 'pre-request', `bru.setVar('folderRan', true);`); + }); + + await test.step('Create a request inside the folder whose pre-request script issues a sendRequest', async () => { + await createRequest(page, requestName, folderName, { url, inFolder: true }); + await page.locator('.collection-item-name').filter({ hasText: requestName }).first().click(); + await addPreRequestScript(page, `await bru.sendRequest({ url: "${url}", method: "GET" });`); + await saveRequest(page); + }); + + await test.step('Send the request', async () => { + await sendRequest(page, 200); + }); + + const scriptedRow = page + .getByTestId('timeline-entry') + .filter({ has: page.getByTestId('timeline-badge-pre') }); + + await test.step('Open Timeline and expand the scripted (sendRequest) row', async () => { + await selectResponsePaneTab(page, 'Timeline'); + await expect(scriptedRow).toHaveCount(1); + await scriptedRow.getByTestId('timeline-item-header').click(); + }); + + await test.step('Source file points to the request file, not the folder script', async () => { + const sourceFile = scriptedRow.getByTestId('timeline-source-file'); + await expect(sourceFile).toBeVisible(); + await expect(sourceFile).toHaveText('auth/folder-driver.yml'); + await expect(sourceFile).not.toContainText('auth/folder.yml'); + }); + }); +}); diff --git a/tests/request/timeline/timeline-scripted-requests.spec.ts b/tests/request/timeline/timeline-scripted-requests.spec.ts index 56a737979..653b1ee6a 100644 --- a/tests/request/timeline/timeline-scripted-requests.spec.ts +++ b/tests/request/timeline/timeline-scripted-requests.spec.ts @@ -54,55 +54,53 @@ test.describe('Timeline — scripted requests (sendRequest / runRequest)', () => await test.step('Open Timeline and assert four rows', async () => { await selectResponsePaneTab(page, 'Timeline'); - const rows = page.locator('.timeline-container .tl-row-wrap'); + const rows = page.getByTestId('timeline-container').getByTestId('timeline-entry'); await expect(rows).toHaveCount(4); }); - await test.step('Filter chips appear with correct counts (only Main + Pre-Request show)', async () => { - const chips = page.locator('.timeline-filter-bar .timeline-chip'); - await expect(chips).toHaveCount(3); // All, Main, Pre-Request + await test.step('Filter chips appear with correct counts (only Request + Pre-Request show)', async () => { + const filterBar = page.getByTestId('timeline-filter-bar'); + await expect(filterBar.getByRole('button')).toHaveCount(3); // All, Request, Pre-Request - const countFor = (label: string) => - chips.filter({ hasText: label }).locator('.timeline-chip-count').first(); + const countFor = (id: string) => + page.getByTestId(`timeline-chip-${id}`).getByTestId('timeline-chip-count'); - await expect(countFor('All')).toHaveText('4'); - await expect(countFor('Main')).toHaveText('1'); - await expect(countFor('Pre-Request')).toHaveText('3'); + await expect(countFor('all')).toHaveText('4'); + await expect(countFor('main')).toHaveText('1'); + await expect(countFor('pre')).toHaveText('3'); }); await test.step('Rows are sorted newest-first; the collection-script row sits last', async () => { - const rows = page.locator('.timeline-container .tl-row-wrap'); + const rows = page.getByTestId('timeline-container').getByTestId('timeline-entry'); // Execution order: collection → folder → request → main. // Newest-first: main → request-script → folder-script → collection-script. - await expect(rows.nth(0).locator('.tl-badge--main')).toHaveCount(1); + await expect(rows.nth(0).getByTestId('timeline-badge-main')).toHaveCount(1); const requestScriptRow = rows.nth(1); - await expect(requestScriptRow.locator('.tl-badge--scripted')).toHaveCount(1); - await expect(requestScriptRow.locator('.tl-col-url')).toContainText('/query'); + await expect(requestScriptRow.getByTestId('timeline-badge-pre')).toHaveCount(1); + await expect(requestScriptRow.getByTestId('timeline-url')).toContainText('/query'); const folderScriptRow = rows.nth(2); - await expect(folderScriptRow.locator('.tl-badge--scripted')).toHaveCount(1); - await expect(folderScriptRow.locator('.tl-col-url')).toContainText('/headers'); + await expect(folderScriptRow.getByTestId('timeline-badge-pre')).toHaveCount(1); + await expect(folderScriptRow.getByTestId('timeline-url')).toContainText('/headers'); const collectionScriptRow = rows.nth(3); - await expect(collectionScriptRow.locator('.tl-badge--scripted')).toHaveCount(1); - await expect(collectionScriptRow.locator('.tl-col-url')).toContainText('/echo/path'); + await expect(collectionScriptRow.getByTestId('timeline-badge-pre')).toHaveCount(1); + await expect(collectionScriptRow.getByTestId('timeline-url')).toContainText('/echo/path'); }); await test.step('Clicking the Pre-Request chip narrows to the three sendRequest rows', async () => { - const chips = page.locator('.timeline-filter-bar .timeline-chip'); - await chips.filter({ hasText: 'Pre-Request' }).click(); + await page.getByTestId('timeline-chip-pre').click(); - const visibleRows = page.locator('.timeline-container .tl-row-wrap'); + const visibleRows = page.getByTestId('timeline-container').getByTestId('timeline-entry'); await expect(visibleRows).toHaveCount(3); - await expect(visibleRows.locator('.tl-badge--scripted')).toHaveCount(3); + await expect(visibleRows.getByTestId('timeline-badge-pre')).toHaveCount(3); }); await test.step('Clicking All restores every row', async () => { - const chips = page.locator('.timeline-filter-bar .timeline-chip'); - await chips.filter({ hasText: 'All' }).click(); - await expect(page.locator('.timeline-container .tl-row-wrap')).toHaveCount(4); + await page.getByTestId('timeline-chip-all').click(); + await expect(page.getByTestId('timeline-container').getByTestId('timeline-entry')).toHaveCount(4); }); }); @@ -139,15 +137,15 @@ test.describe('Timeline — scripted requests (sendRequest / runRequest)', () => }); await test.step('Runner timeline shows main + sendRequest + runRequest rows', async () => { - const rows = page.locator('.tl-row-wrap'); + const rows = page.getByTestId('timeline-entry'); await expect(rows).toHaveCount(3, { timeout: 10000 }); - await expect(rows.locator('.tl-badge--main')).toHaveCount(1); - await expect(rows.locator('.tl-badge--scripted')).toHaveCount(1); - await expect(rows.locator('.tl-badge--run-request')).toHaveCount(1); + await expect(rows.getByTestId('timeline-badge-main')).toHaveCount(1); + await expect(rows.getByTestId('timeline-badge-pre')).toHaveCount(1); + await expect(rows.getByTestId('timeline-badge-post')).toHaveCount(1); // The runner view never shows the filter chip bar (no chip-bar UI here). - await expect(page.locator('.timeline-filter-bar')).toHaveCount(0); + await expect(page.getByTestId('timeline-filter-bar')).toHaveCount(0); }); }); }); diff --git a/tests/request/timeline/timeline-url-update.spec.ts b/tests/request/timeline/timeline-url-update.spec.ts index 145c07bc2..497e0d86f 100644 --- a/tests/request/timeline/timeline-url-update.spec.ts +++ b/tests/request/timeline/timeline-url-update.spec.ts @@ -64,7 +64,7 @@ test.describe('Timeline URL Update', () => { await selectResponsePaneTab(page, 'Timeline'); // Get all timeline entries - const timelineItems = page.locator('.tl-row-wrap'); + const timelineItems = page.getByTestId('timeline-container').getByTestId('timeline-entry'); await expect(timelineItems).toHaveCount(2, { timeout: 5000 }); // Most recent entry (first in list) should show the second URL