diff --git a/packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/StyledWrapper.js b/packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/StyledWrapper.js index 698bb90ae..447f036c7 100644 --- a/packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/StyledWrapper.js +++ b/packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/StyledWrapper.js @@ -314,6 +314,7 @@ const StyledWrapper = styled.div` height: 100% !important; max-height: 400px !important; padding: 0.5rem !important; + overflow: auto !important; .network-logs-pre { color: ${(props) => props.theme.console.messageColor} !important; diff --git a/tests/devtools/network/fixtures/collection/bruno.json b/tests/devtools/network/fixtures/collection/bruno.json new file mode 100644 index 000000000..9c51b2d05 --- /dev/null +++ b/tests/devtools/network/fixtures/collection/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "network-log-scroll", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} diff --git a/tests/devtools/network/fixtures/collection/network-log-scroll.bru b/tests/devtools/network/fixtures/collection/network-log-scroll.bru new file mode 100644 index 000000000..d99aeeb48 --- /dev/null +++ b/tests/devtools/network/fixtures/collection/network-log-scroll.bru @@ -0,0 +1,104 @@ +meta { + name: network-log-scroll + type: http + seq: 1 +} + +get { + url: http://localhost:8081/api/echo/headers + body: none + auth: none +} + +headers { + X-Network-Log-1: value-1 + X-Network-Log-2: value-2 + X-Network-Log-3: value-3 + X-Network-Log-4: value-4 + X-Network-Log-5: value-5 + X-Network-Log-6: value-6 + X-Network-Log-7: value-7 + X-Network-Log-8: value-8 + X-Network-Log-9: value-9 + X-Network-Log-10: value-10 + X-Network-Log-11: value-11 + X-Network-Log-12: value-12 + X-Network-Log-13: value-13 + X-Network-Log-14: value-14 + X-Network-Log-15: value-15 + X-Network-Log-16: value-16 + X-Network-Log-17: value-17 + X-Network-Log-18: value-18 + X-Network-Log-19: value-19 + X-Network-Log-20: value-20 + X-Network-Log-21: value-21 + X-Network-Log-22: value-22 + X-Network-Log-23: value-23 + X-Network-Log-24: value-24 + X-Network-Log-25: value-25 + X-Network-Log-26: value-26 + X-Network-Log-27: value-27 + X-Network-Log-28: value-28 + X-Network-Log-29: value-29 + X-Network-Log-30: value-30 + X-Network-Log-31: value-31 + X-Network-Log-32: value-32 + X-Network-Log-33: value-33 + X-Network-Log-34: value-34 + X-Network-Log-35: value-35 + X-Network-Log-36: value-36 + X-Network-Log-37: value-37 + X-Network-Log-38: value-38 + X-Network-Log-39: value-39 + X-Network-Log-40: value-40 + X-Network-Log-41: value-41 + X-Network-Log-42: value-42 + X-Network-Log-43: value-43 + X-Network-Log-44: value-44 + X-Network-Log-45: value-45 + X-Network-Log-46: value-46 + X-Network-Log-47: value-47 + X-Network-Log-48: value-48 + X-Network-Log-49: value-49 + X-Network-Log-50: value-50 + X-Network-Log-51: value-51 + X-Network-Log-52: value-52 + X-Network-Log-53: value-53 + X-Network-Log-54: value-54 + X-Network-Log-55: value-55 + X-Network-Log-56: value-56 + X-Network-Log-57: value-57 + X-Network-Log-58: value-58 + X-Network-Log-59: value-59 + X-Network-Log-60: value-60 + X-Network-Log-61: value-61 + X-Network-Log-62: value-62 + X-Network-Log-63: value-63 + X-Network-Log-64: value-64 + X-Network-Log-65: value-65 + X-Network-Log-66: value-66 + X-Network-Log-67: value-67 + X-Network-Log-68: value-68 + X-Network-Log-69: value-69 + X-Network-Log-70: value-70 + X-Network-Log-71: value-71 + X-Network-Log-72: value-72 + X-Network-Log-73: value-73 + X-Network-Log-74: value-74 + X-Network-Log-75: value-75 + X-Network-Log-76: value-76 + X-Network-Log-77: value-77 + X-Network-Log-78: value-78 + X-Network-Log-79: value-79 + X-Network-Log-80: value-80 + X-Network-Log-81: value-81 + X-Network-Log-82: value-82 + X-Network-Log-83: value-83 + X-Network-Log-84: value-84 + X-Network-Log-85: value-85 + X-Network-Log-86: value-86 + X-Network-Log-87: value-87 + X-Network-Log-88: value-88 + X-Network-Log-89: value-89 + X-Network-Log-90: value-90 +} diff --git a/tests/devtools/network/init-user-data/collection-security.json b/tests/devtools/network/init-user-data/collection-security.json new file mode 100644 index 000000000..89dc2bfff --- /dev/null +++ b/tests/devtools/network/init-user-data/collection-security.json @@ -0,0 +1,10 @@ +{ + "collections": [ + { + "path": "{{collectionPath}}", + "securityConfig": { + "jsSandboxMode": "safe" + } + } + ] +} diff --git a/tests/devtools/network/init-user-data/preferences.json b/tests/devtools/network/init-user-data/preferences.json new file mode 100644 index 000000000..872cf5312 --- /dev/null +++ b/tests/devtools/network/init-user-data/preferences.json @@ -0,0 +1,12 @@ +{ + "maximized": false, + "lastOpenedCollections": [ + "{{collectionPath}}" + ], + "preferences": { + "onboarding": { + "hasLaunchedBefore": true, + "hasSeenWelcomeModal": true + } + } +} diff --git a/tests/devtools/network/network-log-details-scroll.spec.ts b/tests/devtools/network/network-log-details-scroll.spec.ts new file mode 100644 index 000000000..2b228c16f --- /dev/null +++ b/tests/devtools/network/network-log-details-scroll.spec.ts @@ -0,0 +1,101 @@ +import { test, expect, type Locator } from '../../../playwright'; +import { openCollection, openRequest, sendRequest } from '../../utils/page'; + +const COLLECTION_NAME = 'network-log-scroll'; +const REQUEST_NAME = 'network-log-scroll'; + +const isEntryVisibleInScroller = async (scroller: Locator, entry: Locator) => { + const entryHandle = await entry.elementHandle(); + if (!entryHandle) { + return false; + } + + return scroller.evaluate((container, entryEl) => { + const containerRect = container.getBoundingClientRect(); + const entryRect = entryEl.getBoundingClientRect(); + + return ( + entryRect.height > 0 + && entryRect.top >= containerRect.top - 1 + && entryRect.bottom <= containerRect.bottom + 1 + ); + }, entryHandle); +}; + +test.describe('DevTools Network Log Details Scroll', () => { + test('last network log lines are visible and scrollable in request details panel', async ({ pageWithUserData: page }) => { + await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + + await test.step('Open fixture request and send it', async () => { + await openCollection(page, COLLECTION_NAME); + await openRequest(page, COLLECTION_NAME, REQUEST_NAME); + await sendRequest(page, 200); + }); + + await test.step('Open DevTools Network tab and select the request', async () => { + await page.locator('button[data-trigger="dev-tools"]').click(); + await expect(page.locator('.console-header')).toBeVisible(); + + const networkTab = page.locator('.console-tab').filter({ hasText: 'Network' }); + await expect(networkTab).toBeVisible(); + await networkTab.click(); + await expect(networkTab).toHaveClass(/active/); + + const requestRow = page.getByTestId('network-request-row').first(); + await expect(requestRow).toBeVisible(); + await requestRow.click(); + }); + + const panel = page.locator('.details-panel-wrapper'); + const outerScroller = panel.locator('.panel-content'); + const innerScroller = panel.locator('.network-logs-wrapper .network-logs-container'); + const lastEntry = innerScroller.locator('.network-logs-entry').last(); + + await test.step('Open Network sub-tab in request details panel', async () => { + await expect(panel.getByText('Request Details')).toBeVisible(); + const networkSubTab = panel.locator('.tab-button').filter({ hasText: 'Network' }); + await expect(networkSubTab).toBeVisible(); + await networkSubTab.click(); + await expect(networkSubTab).toHaveClass(/active/); + await expect(innerScroller).toBeVisible(); + }); + + await test.step('Verify nested inner scroller overflows', async () => { + const innerOverflows = await innerScroller.evaluate((el) => el.scrollHeight > el.clientHeight); + expect(innerOverflows).toBe(true); + await expect(lastEntry).toContainText(/Request completed in/); + }); + + await test.step('Outer panel scroll alone does not reveal the last log line', async () => { + const initialOuterScrollTop = await outerScroller.evaluate((el) => el.scrollTop); + expect(initialOuterScrollTop).toBe(0); + + await outerScroller.evaluate((el) => { + el.scrollTop = el.scrollHeight; + }); + + await expect.poll(() => isEntryVisibleInScroller(innerScroller, lastEntry)).toBe(false); + + await outerScroller.evaluate((el) => { + el.scrollTop = 0; + }); + }); + + await test.step('Scroll nested inner container and verify last log line is visible in viewport', async () => { + await expect(async () => { + await innerScroller.evaluate((el) => { + el.scrollTop = el.scrollHeight; + }); + + const scrollTop = await innerScroller.evaluate((el) => el.scrollTop); + expect(scrollTop).toBeGreaterThan(0); + + await expect(lastEntry).toBeVisible({ timeout: 1000 }); + expect(await isEntryVisibleInScroller(innerScroller, lastEntry)).toBe(true); + }).toPass({ timeout: 10000 }); + + const outerScrollTop = await outerScroller.evaluate((el) => el.scrollTop); + expect(outerScrollTop).toBe(0); + }); + }); +});