diff --git a/packages/bruno-app/src/components/RunnerResults/index.jsx b/packages/bruno-app/src/components/RunnerResults/index.jsx index 31b6d97f20..8ce7b945f8 100644 --- a/packages/bruno-app/src/components/RunnerResults/index.jsx +++ b/packages/bruno-app/src/components/RunnerResults/index.jsx @@ -3,16 +3,13 @@ import path from 'utils/common/path'; import { useDispatch } from 'react-redux'; import { get, cloneDeep } from 'lodash'; import { runCollectionFolder, cancelRunnerExecution, mountCollection, updateRunnerConfiguration } from 'providers/ReduxStore/slices/collections/actions'; -import { resetCollectionRunner } from 'providers/ReduxStore/slices/collections'; -import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections'; -import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, IconX, IconRun, IconLoader2 } from '@tabler/icons'; +import { resetCollectionRunner, updateRunnerTagsDetails } from 'providers/ReduxStore/slices/collections'; +import { findItemInCollection, getTotalRequestCountInCollection, areItemsLoading, getRequestItemsForCollectionRun } from 'utils/collections'; +import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, IconX, IconRun, IconExternalLink } from '@tabler/icons'; import ResponsePane from './ResponsePane'; import StyledWrapper from './StyledWrapper'; -import { areItemsLoading } from 'utils/collections'; import RunnerTags from './RunnerTags/index'; import RunConfigurationPanel from './RunConfigurationPanel'; -import { getRequestItemsForCollectionRun } from 'utils/collections/index'; -import { updateRunnerTagsDetails } from 'providers/ReduxStore/slices/collections/index'; const getDisplayName = (fullPath, pathname, name = '') => { let relativePath = path.relative(fullPath, pathname); @@ -42,67 +39,61 @@ const anyTestFailed = (item) => { item.postResponseTestStatus === 'fail'; }; +// === Centralized filters definition === +const FILTERS = { + all: { + label: 'All', + predicate: () => true, + resultFilter: (results) => results + }, + passed: { + label: 'Passed', + predicate: (item) => allTestsPassed(item), + resultFilter: (results) => results?.filter((r) => r.status === 'pass') + }, + failed: { + label: 'Failed', + predicate: (item) => anyTestFailed(item), + resultFilter: (results) => results?.filter((r) => ['fail', 'error'].includes(r.status)) + }, + skipped: { + label: 'Skipped', + predicate: (item) => item.status === 'skipped', + resultFilter: (results) => results + } +}; + +// === Reusable filter button === +const FilterButton = ({ label, count, active, onClick }) => ( + +); + export default function RunnerResults({ collection }) { const dispatch = useDispatch(); const [selectedItem, setSelectedItem] = useState(null); const [delay, setDelay] = useState(null); const [activeFilter, setActiveFilter] = useState('all'); - - - const getActiveFilterPredicate = () => { - switch (activeFilter) { - case 'passing_requests': - return (item) => item.status !== 'error' && item.testStatus === 'pass' && item.assertionStatus === 'pass'; - case 'failing_requests': - return (item) => (item.status !== 'error' && item.testStatus === 'fail') || item.assertionStatus === 'fail'; - case 'passing_tests': - return (item) => item.testResults?.some((result) => result.status === 'pass'); - case 'failing_tests': - return (item) => item.testResults?.some((result) => result.status === 'fail' || result.status === 'error'); - default: - return () => true - } - } - const [selectedRequestItems, setSelectedRequestItems] = useState([]); const [configureMode, setConfigureMode] = useState(false); - // ref for the runner output body const runnerBodyRef = useRef(); - const autoScrollRunnerBody = () => { - if (runnerBodyRef?.current) { - // mimics the native terminal scroll style - runnerBodyRef.current.scrollTo(0, 100000); - } - }; - - useEffect(() => { - if (!collection.runnerResult) { - setSelectedItem(null); - } - autoScrollRunnerBody(); - }, [collection, setSelectedItem]); - - useEffect(() => { - const runnerInfo = get(collection, 'runnerResult.info', {}); - if (runnerInfo.status === 'running') { - setConfigureMode(false); - } - }, [collection.runnerResult]); - - useEffect(() => { - const savedConfiguration = get(collection, 'runnerConfiguration', null); - if (savedConfiguration) { - if (savedConfiguration.selectedRequestItems && configureMode) { - setSelectedRequestItems(savedConfiguration.selectedRequestItems); - } - if (savedConfiguration.delay !== undefined && delay === null) { - setDelay(savedConfiguration.delay); - } - } - }, [collection.runnerConfiguration, configureMode, delay]); - const collectionCopy = cloneDeep(collection); const runnerInfo = get(collection, 'runnerResult.info', {}); @@ -144,6 +135,63 @@ export default function RunnerResults({ collection }) { }) .filter(Boolean); + const activeFilterConfig = FILTERS[activeFilter]; + const filteredItems = items.filter(activeFilterConfig.predicate); + + const filterTestResults = (results) => { + if (!results || !Array.isArray(results)) return []; + return activeFilterConfig.resultFilter(results); + }; + + const autoScrollRunnerBody = () => { + if (runnerBodyRef?.current) { + const element = runnerBodyRef.current; + const scrollThreshold = 100; // pixels from bottom to consider "at bottom" + const isNearBottom + = element.scrollHeight - element.scrollTop - element.clientHeight < scrollThreshold; + + // Only auto-scroll if user is already near the bottom + if (isNearBottom) { + // mimics the native terminal scroll style + element.scrollTo(0, 100000); + } + } + }; + + useEffect(() => { + if (!collection.runnerResult) { + setSelectedItem(null); + } + autoScrollRunnerBody(); + }, [collection, setSelectedItem]); + + useEffect(() => { + // Auto-scroll when items are added or updated during execution + // Only scrolls if user is already at/near the bottom + if (filteredItems.length > 0) { + autoScrollRunnerBody(); + } + }, [filteredItems]); + + useEffect(() => { + const runnerInfo = get(collection, 'runnerResult.info', {}); + if (runnerInfo.status === 'running') { + setConfigureMode(false); + } + }, [collection.runnerResult]); + + useEffect(() => { + const savedConfiguration = get(collection, 'runnerConfiguration', null); + if (savedConfiguration) { + if (savedConfiguration.selectedRequestItems && configureMode) { + setSelectedRequestItems(savedConfiguration.selectedRequestItems); + } + if (savedConfiguration.delay !== undefined && delay === null) { + setDelay(savedConfiguration.delay); + } + } + }, [collection.runnerConfiguration, configureMode, delay]); + const ensureCollectionIsMounted = () => { if(collection.mountStatus === 'mounted'){ return; @@ -210,52 +258,14 @@ export default function RunnerResults({ collection }) { }, [tagsEnabled]); const totalRequestsInCollection = getTotalRequestCountInCollection(collectionCopy); + const filterCounts = { + all: items.length, + passed: items.filter(allTestsPassed).length, + failed: items.filter(anyTestFailed).length, + skipped: items.filter((i) => i.status === 'skipped').length + }; - const displayCollectionResults = () => { - let passedRequests = 0; - let failedRequests = 0; - let totalTestsInCollection = 0; - let passedTests = 0; - let failedTests = 0; - items.forEach(item => { - const isPassedRequest = item.status !== 'error' && item.testStatus === 'pass' && item.assertionStatus === 'pass'; - const isFailedRequest = (item.status !== 'error' && item.testStatus === 'fail') || item.assertionStatus === 'fail'; - - if (isPassedRequest) passedRequests++; - if (isFailedRequest) failedRequests++; - - const testResults = Array.isArray(item?.testResults) ? item.testResults : []; - totalTestsInCollection += testResults.length; - testResults.forEach(result => { - if (result.status === 'pass') passedTests++; - if (result.status === 'fail' || result.status === 'error') failedTests++; - }); - }); - - return ( -
-
- setActiveFilter('all')} className={`cursor-pointer ${activeFilter === 'all' ? 'underline font-semibold' : ''} hover:font-semibold`}>Total Requests: {items.length}, - setActiveFilter('passing_requests')} className={`cursor-pointer ${activeFilter === 'passing_requests' ? 'underline font-semibold' : ''} hover:font-semibold`}>Passed: {passedRequests}, - setActiveFilter('failing_requests') } className={`cursor-pointer ${activeFilter === 'failing_requests' ? 'underline font-semibold' : ''} hover:font-semibold`}>Failed: {failedRequests} -
-
- setActiveFilter('all')} className={`cursor-pointer ${activeFilter === 'all' ? 'underline font-semibold' : ''} hover:font-semibold`}>Total Tests: {totalTestsInCollection}, - setActiveFilter('passing_tests')} className={`cursor-pointer ${activeFilter === 'passing_tests' ? 'underline font-semibold' : ''} hover:font-semibold`}>Passed: {passedTests}, - setActiveFilter('failing_tests')} className={`cursor-pointer ${activeFilter === 'failing_tests' ? 'underline font-semibold' : ''} hover:font-semibold`}>Failed: {failedTests} -
-
- ) - } - - const passedRequests = items.filter(allTestsPassed); - const failedRequests = items.filter(anyTestFailed); - - const skippedRequests = items.filter((item) => { - return item.status === 'skipped'; - }); let isCollectionLoading = areItemsLoading(collection); - if (!items || !items.length) { return ( @@ -341,35 +351,57 @@ export default function RunnerResults({ collection }) { return ( -
-
- Runner - + {/* Filter Bar and Actions */} +
+
+
+ + Filter by: + +
+
+ {Object.entries(FILTERS).map(([key, { label }]) => ( + setActiveFilter(key)} + /> + ))} +
- {displayCollectionResults()} - {runnerInfo.status !== 'ended' && runnerInfo.cancelTokenUid && ( - - )} + + {runnerInfo.status !== 'ended' && runnerInfo.cancelTokenUid ? ( +
+ +
+ ) : runnerInfo.status === 'ended' ? ( +
+ + +
+ ) : null}
- {runnerInfo?.statusText ? -
- {runnerInfo?.statusText} -
- : null} - -
- Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}, Skipped:{' '} - {skippedRequests.length} -
- {tagsEnabled && areTagsAdded && (
Tags: @@ -383,9 +415,15 @@ export default function RunnerResults({ collection }) {
)} + {runnerInfo?.statusText ? +
+ {runnerInfo?.statusText} +
+ : null} + {/* Items list */} -
- {items.filter(getActiveFilterPredicate()).map((item) => { +
+ {filteredItems.map((item) => { return (
@@ -429,7 +467,7 @@ export default function RunnerResults({ collection }) {
    {item.preRequestTestResults - ? item.preRequestTestResults.map((result) => ( + ? filterTestResults(item.preRequestTestResults).map((result) => (
  • {result.status === 'pass' ? ( @@ -449,7 +487,7 @@ export default function RunnerResults({ collection }) { )) : null} {item.postResponseTestResults - ? item.postResponseTestResults.map((result) => ( + ? filterTestResults(item.postResponseTestResults).map((result) => (
  • {result.status === 'pass' ? ( @@ -469,7 +507,7 @@ export default function RunnerResults({ collection }) { )) : null} {item.testResults - ? item.testResults.map((result) => ( + ? filterTestResults(item.testResults).map((result) => (
  • {result.status === 'pass' ? ( @@ -488,7 +526,7 @@ export default function RunnerResults({ collection }) {
  • )) : null} - {item.assertionResults?.map((result) => ( + {filterTestResults(item.assertionResults).map((result) => (
  • {result.status === 'pass' ? ( @@ -512,43 +550,51 @@ export default function RunnerResults({ collection }) { ); })}
- - {runnerInfo.status === 'ended' ? ( -
- - - -
- ) : null}
+ {selectedItem ? (
-
- {selectedItem.displayName} - - {allTestsPassed(selectedItem) ? - - : null} - {anyTestFailed(selectedItem) ? - - : null} - {selectedItem.status === 'skipped' ? - - : null} - +
+
+ {selectedItem.displayName} + + {allTestsPassed(selectedItem) + ? + : null} + {anyTestFailed(selectedItem) + ? + : null} + {selectedItem.status === 'skipped' + ? + : null} + +
+
- ) : null} + ) : ( +
+
+
+ +
+

+ Click on the status code to view the response +

+
+
+ )}
); -} +} \ No newline at end of file