Apply runner filters to requests/iterations as well as tests [INS-3348] (#10353)

* Hide empty iterations/requests in runner results if there are no tests matching the filter

* Add an empty state in case there are no tests/requests matching the selected filter

* Add tests to cover changes in runner filter behavior

* fix: minor improvements

* chore: rewording

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: George He <hexxa@outlook.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-08-11 14:10:59 +08:00
1 parent cecbec02d3
commit 6469a6eaeb
4 files changed
+172 -23

No files matched your search

@@ -0,0 +1,107 @@
import type { RequestTestResult } from 'insomnia-data';
import { describe, expect, it } from 'vitest';
import { filterTestResults, hasMatchingTestResults, isFilterEngaged } from '../request-test-result-pane';
const testResult = (overrides: Partial<RequestTestResult> = {}): RequestTestResult => ({
testCase: 'renders a response',
status: 'passed',
executionTime: 1,
category: 'after-response',
...overrides,
});
describe('filterTestResults', () => {
const results: RequestTestResult[] = [
testResult({ testCase: 'status code is 200', status: 'passed' }),
testResult({ testCase: 'has expected body', status: 'failed' }),
testResult({ testCase: 'pre-request runs', status: 'skipped' }),
];
it('returns everything, with original indexes, when targetTests is "all" and there is no text filter', () => {
expect(filterTestResults(results, 'all', '')).toEqual([
{ result: results[0], index: 0 },
{ result: results[1], index: 1 },
{ result: results[2], index: 2 },
]);
});
it('filters to only passed results', () => {
expect(filterTestResults(results, 'passed', '')).toEqual([{ result: results[0], index: 0 }]);
});
it('filters to only failed results', () => {
expect(filterTestResults(results, 'failed', '')).toEqual([{ result: results[1], index: 1 }]);
});
it('filters to only skipped results', () => {
expect(filterTestResults(results, 'skipped', '')).toEqual([{ result: results[2], index: 2 }]);
});
it('applies the text filter on top of the status filter', () => {
expect(filterTestResults(results, 'all', 'expected')).toEqual([{ result: results[1], index: 1 }]);
});
it('returns nothing when the text filter matches no test case in the selected status', () => {
expect(filterTestResults(results, 'passed', 'expected')).toEqual([]);
});
it('ignores a whitespace-only text filter', () => {
expect(filterTestResults(results, 'all', ' ')).toHaveLength(3);
});
it('returns an empty array when there are no test results to filter', () => {
expect(filterTestResults([], 'all', '')).toEqual([]);
expect(filterTestResults([], 'passed', '')).toEqual([]);
});
});
describe('isFilterEngaged', () => {
it('is not engaged for the default "all" status with no text filter', () => {
expect(isFilterEngaged('all', '')).toBe(false);
});
it('is not engaged when the text filter is only whitespace', () => {
expect(isFilterEngaged('all', ' ')).toBe(false);
});
it('is engaged when a non-"all" status is selected', () => {
expect(isFilterEngaged('passed', '')).toBe(true);
expect(isFilterEngaged('failed', '')).toBe(true);
expect(isFilterEngaged('skipped', '')).toBe(true);
});
it('is engaged when a text filter is present', () => {
expect(isFilterEngaged('all', 'expected')).toBe(true);
});
});
describe('hasMatchingTestResults', () => {
const results: RequestTestResult[] = [testResult({ testCase: 'status code is 200', status: 'passed' })];
it('is true when no filter is engaged, even with no test results at all', () => {
expect(hasMatchingTestResults([], 'all', '')).toBe(true);
expect(hasMatchingTestResults(results, 'all', '')).toBe(true);
});
it('is true when a filter is engaged and at least one result matches', () => {
expect(hasMatchingTestResults(results, 'passed', '')).toBe(true);
});
it('is false when a status filter is engaged and no result matches', () => {
expect(hasMatchingTestResults(results, 'failed', '')).toBe(false);
expect(hasMatchingTestResults(results, 'skipped', '')).toBe(false);
});
it('is false when a status filter is engaged and there are no results at all', () => {
expect(hasMatchingTestResults([], 'failed', '')).toBe(false);
});
it('is false when a text filter is engaged and no result matches', () => {
expect(hasMatchingTestResults(results, 'all', 'nonexistent test name')).toBe(false);
});
it('is true when a text filter is engaged and a result matches', () => {
expect(hasMatchingTestResults(results, 'all', 'status code')).toBe(true);
});
});
@@ -11,12 +11,12 @@ import {
type RunnerLiveItem,
} from '../../../common/runner-feedback';
import { RenderedText } from '../rendered-text';
import { RequestTestResultRows } from './request-test-result-pane';
import { hasMatchingTestResults, RequestTestResultRows, type TargetTestType } from './request-test-result-pane';
interface Props {
item: RunnerResultPerRequest | RunnerLiveItem;
resultFilter?: string;
targetTests?: string;
targetTests?: TargetTestType;
onSkip?: () => void;
testId?: string;
defaultExpanded?: boolean;
@@ -49,6 +49,10 @@ export const RequestResultCard: FC<Props> = ({
const requestId = 'requestId' in item ? item.requestId : undefined;
if (!hasMatchingTestResults(results, targetTests, resultFilter)) {
return null;
}
return (
<div data-testid={testId} className="m-3 rounded-sm border border-solid border-(--hl-md) p-3">
<div className="flex items-start gap-2">
@@ -3,7 +3,7 @@ import { fuzzyMatch } from 'insomnia-data/common';
import React, { type FC, useState } from 'react';
import { Toolbar } from 'react-aria-components';
type TargetTestType = 'all' | 'passed' | 'failed' | 'skipped';
export type TargetTestType = 'all' | 'passed' | 'failed' | 'skipped';
const filterClassnames =
'mx-1 w-24 text-center rounded-md h-(--line-height-xxs) text-sm cursor-pointer outline-hidden select-none px-2 py-1 hover:bg-[rgba(var(--color-surprise-rgb),50%)] text-(--hl) aria-selected:text-(--color-font-surprise) hover:text-(--color-font-surprise) aria-selected:bg-[rgba(var(--color-surprise-rgb),40%)] transition-colors duration-300';
@@ -13,23 +13,15 @@ const activeFilterClassnames =
export interface RequestTestResultRowsProps {
requestTestResults: RequestTestResult[];
resultFilter: string;
targetTests: string;
targetTests: TargetTestType;
}
export const RequestTestResultRows: FC<RequestTestResultRowsProps> = ({
requestTestResults,
resultFilter,
targetTests,
}: RequestTestResultRowsProps) => {
if (requestTestResults.length === 0) {
return (
<div className="my-2 w-full pl-3 text-sm text-neutral-400">
No test was detected, add test cases in scripts to see results.
</div>
);
}
const testResultRows = requestTestResults
export function filterTestResults(
requestTestResults: RequestTestResult[],
targetTests: TargetTestType,
resultFilter: string,
): { result: RequestTestResult; index: number }[] {
return requestTestResults
.map((result, index) => ({ result, index }))
.filter(({ result }) => {
switch (targetTests) {
@@ -56,7 +48,38 @@ export const RequestTestResultRows: FC<RequestTestResultRowsProps> = ({
}
return Boolean(fuzzyMatch(resultFilter, result.testCase, { splitSpace: false, loose: true })?.indexes);
})
});
}
export function isFilterEngaged(targetTests: TargetTestType, resultFilter: string): boolean {
return targetTests !== 'all' || resultFilter.trim() !== '';
}
export function hasMatchingTestResults(
requestTestResults: RequestTestResult[],
targetTests: TargetTestType,
resultFilter: string,
): boolean {
return (
!isFilterEngaged(targetTests, resultFilter) ||
filterTestResults(requestTestResults, targetTests, resultFilter).length > 0
);
}
export const RequestTestResultRows: FC<RequestTestResultRowsProps> = ({
requestTestResults,
resultFilter,
targetTests,
}: RequestTestResultRowsProps) => {
if (requestTestResults.length === 0) {
return (
<div className="my-2 w-full pl-3 text-sm text-neutral-400">
No test was detected, add test cases in scripts to see results.
</div>
);
}
const testResultRows = filterTestResults(requestTestResults, targetTests, resultFilter)
.map(({ result, index }) => {
const statusText = {
passed: 'PASS',
@@ -6,8 +6,7 @@ import { useRootLoaderData } from '~/root';
import { Hotkey } from '~/ui/components/hotkey';
import { RequestResultCard } from './request-result-card';
type TargetTestType = 'all' | 'passed' | 'failed' | 'skipped';
import { hasMatchingTestResults, type TargetTestType } from './request-test-result-pane';
const filterClassnames =
'mx-1 w-24 text-center rounded-md h-(--line-height-xxs) text-sm cursor-pointer outline-hidden select-none px-2 py-1 hover:bg-[rgba(var(--color-surprise-rgb),50%)] text-(--hl) aria-selected:text-(--color-font-surprise) hover:text-(--color-font-surprise) aria-selected:bg-[rgba(var(--color-surprise-rgb),40%)] transition-colors duration-300';
@@ -56,6 +55,14 @@ export const RunnerTestResultPane: FC<Props> = ({ result }) => {
const key = `runner-test-result-iteration-${i + 1}`;
if (Array.isArray(iterationResults)) {
const hasVisibleRequest = iterationResults.some(
requestTestResult => hasMatchingTestResults(requestTestResult.results ?? [], targetTests, resultFilter),
);
if (!hasVisibleRequest) {
return null;
}
const resultByRequest = iterationResults.map((requestTestResult: RunnerResultPerRequest, reqIndex: number) => {
const key = `request-test-result-${reqIndex}`;
return (
@@ -80,9 +87,11 @@ export const RunnerTestResultPane: FC<Props> = ({ result }) => {
return <div key={key}>Invalid test result format</div>;
});
const hasVisibleResults = resultsByIteration.some(Boolean);
return (
<>
<div className="flex h-full flex-col divide-y divide-solid divide-(--hl-md)">
<div className="flex h-[calc(100%-var(--line-height-sm))] flex-col divide-y divide-solid divide-(--hl-md)">
<div className="h-[calc(100%-var(--line-height-sm))]">
<Toolbar className="box-border flex h-(--line-height-sm) flex-row items-center overflow-x-auto border-b border-solid border-b-(--hl-md) pl-2 text-(--font-size-sm)">
<button
@@ -111,7 +120,13 @@ export const RunnerTestResultPane: FC<Props> = ({ result }) => {
</button>
</Toolbar>
<div className="h-[calc(100%-var(--line-height-sm))] w-auto overflow-x-auto overflow-y-auto">
{resultsByIteration}
{hasVisibleResults ? (
resultsByIteration
) : (
<div className="mt-5 text-center">
<div>No matching requests/tests to display</div>
</div>
)}
</div>
</div>
<Toolbar className="box-border flex h-(--line-height-sm) shrink-0 flex-row items-center overflow-x-auto text-(--font-size-sm)">