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 ( -
+ Click on the status code to view the response +
+