fix(timeline): scope scripted requests to their own request (#8210)

* fix(timeline): scope scripted requests to their own request

* fix: oauth playwright test
This commit is contained in:
Pooja
2026-06-11 15:10:57 +05:30
committed by lohit
parent f05bb9c49d
commit 6717035dd2
13 changed files with 189 additions and 69 deletions

View File

@@ -36,7 +36,7 @@ const BodyBlock = ({ collection, data, dataBuffer, headers, error, item, type })
/>
</div>
) : (
<div className="tl-empty">No Body found</div>
<div className="tl-empty">No Body</div>
)
)}
</div>

View File

@@ -31,7 +31,7 @@ const Headers = ({ headers }) => {
</button>
{isOpen && (
count === 0
? <div className="tl-empty">No Headers found</div>
? <div className="tl-empty">No Headers</div>
: (
<table className="tl-headers-table">
<tbody>

View File

@@ -21,6 +21,7 @@ const Status = ({ statusCode }) => {
return (
<span
className="timeline-status"
data-testid="timeline-status"
style={{
color,
background,

View File

@@ -137,7 +137,7 @@ const TimelineItem = ({
return (
<StyledWrapper>
<div className={`tl-row-wrap ${isOauth2 ? 'tl-row-wrap--oauth2' : ''}`}>
<div className={`tl-row-wrap ${isOauth2 ? 'tl-row-wrap--oauth2' : ''}`} data-testid="timeline-entry">
<div
className={`tl-row ${isExpanded ? 'is-expanded' : ''}`}
role="button"
@@ -155,9 +155,9 @@ const TimelineItem = ({
<div className="tl-col-method">
<Method method={method} />
</div>
<div className="tl-col-url" title={url}>{url}</div>
<div className="tl-col-url" title={url} data-testid="timeline-url">{url}</div>
<div className="tl-col-badge">
<span className={badge.badgeClass}>{badge.badgeLabel}</span>
<span className={badge.badgeClass} data-testid={`timeline-badge-${badge.kind}`}>{badge.badgeLabel}</span>
</div>
{!hideTimestamp && (
<div className="tl-col-time">
@@ -167,7 +167,7 @@ const TimelineItem = ({
</div>
{isExpanded && (
<div className="tl-detail">
<div className="tl-detail" data-testid="timeline-detail">
<div className="tl-header">
<div className="tl-header-url" title={`${method || ''} ${url}`}>
<span className="tl-header-url-method">{method}</span>
@@ -179,8 +179,9 @@ const TimelineItem = ({
href="#"
title={canNavigate ? `Open ${sourceFile}` : sourceFile}
onClick={canNavigate ? handleNavigate : (ev) => ev.preventDefault()}
data-testid="timeline-source-link"
>
<span className="tl-header-src-file">{sourceFile}</span>
<span className="tl-header-src-file" data-testid="timeline-source-file">{sourceFile}</span>
<span className="tl-header-src-icon"></span>
</a>
)}

View File

@@ -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 = [

View File

@@ -78,22 +78,23 @@ const Timeline = ({ collection, item }) => {
ref={wrapperRef}
>
{showFilterBar && (
<div className="timeline-filter-bar">
<div className="timeline-filter-bar" data-testid="timeline-filter-bar">
{visibleChips.map((chip) => (
<button
key={chip.id}
type="button"
className={`timeline-chip ${activeFilter === chip.id ? 'is-active' : ''}`}
onClick={() => setActiveFilter(chip.id)}
data-testid={`timeline-chip-${chip.id}`}
>
{chip.label}
<span className="timeline-chip-count">{counts[chip.id] ?? 0}</span>
<span className="timeline-chip-count" data-testid="timeline-chip-count">{counts[chip.id] ?? 0}</span>
</button>
))}
</div>
)}
<div className="timeline-container">
<div className="timeline-container" data-testid="timeline-container">
{entries.map((entry, index) => {
const kind = getEntryKind(entry);
if (activeFilter !== 'all' && activeFilter !== kind) return null;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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