Skip to content

Add testsuites hook #96494

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 29, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ type CodecovQueryParamsProviderProps = {
};

const VALUES_TO_RESET_MAP = {
integratedOrgId: ['repository', 'branch'],
repository: ['branch'],
integratedOrgId: ['repository', 'branch', 'testSuites'],
repository: ['branch', 'testSuites'],
branch: [],
codecovPeriod: [],
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,57 +1,83 @@
import {useCallback, useMemo} from 'react';
import {useCallback, useEffect, useMemo, useState} from 'react';
import {useSearchParams} from 'react-router-dom';
import styled from '@emotion/styled';
import debounce from 'lodash/debounce';
import sortBy from 'lodash/sortBy';

import {useTestSuites} from 'sentry/components/codecov/testSuiteDropdown/useTestSuites';
import {Badge} from 'sentry/components/core/badge';
import DropdownButton from 'sentry/components/dropdownButton';
import {HybridFilter} from 'sentry/components/organizations/hybridFilter';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import {trimSlug} from 'sentry/utils/string/trimSlug';

// TODO: have these come from the API
const PLACEHOLDER_TEST_SUITES = [
'option 1',
'option 2',
'option 3',
'super-long-option-4',
];

const TEST_SUITE = 'testSuite';
const MAX_SUITE_UI_LENGTH = 22;
const TEST_SUITES = 'testSuites';
const MAX_SUITE_UI_LENGTH = 50;
const MAX_RECORD_LENGTH = 40;

export function TestSuiteDropdown() {
const [searchParams, setSearchParams] = useSearchParams();
const [dropdownSearch, setDropdownSearch] = useState<string>('');
const [urlSearchParams, setUrlSearchParams] = useSearchParams();
const {data: testSuites} = useTestSuites();

const handleChange = useCallback(
(newTestSuites: string[]) => {
searchParams.delete(TEST_SUITE);
urlSearchParams.delete(TEST_SUITES);

newTestSuites.forEach(suite => {
searchParams.append(TEST_SUITE, suite);
urlSearchParams.append(TEST_SUITES, suite);
});

setSearchParams(searchParams);
setUrlSearchParams(urlSearchParams);
setDropdownSearch('');
},
[searchParams, setSearchParams]
[urlSearchParams, setUrlSearchParams]
);

const options = useMemo(
const options = useMemo(() => {
const selectedNames = urlSearchParams.getAll(TEST_SUITES);
const selectedSet = new Set(selectedNames.map(name => name.toLowerCase()));

const filtered = testSuites.filter(suite => {
const matchesSearch =
!dropdownSearch || suite.toLowerCase().includes(dropdownSearch.toLowerCase());
return matchesSearch || selectedSet.has(suite.toLowerCase());
});

const mapped = filtered.map(suite => ({
label: suite,
value: suite,
isSelected: selectedSet.has(suite.toLowerCase()),
}));

const sorted = sortBy(mapped, [option => !option.isSelected]);

return sorted.slice(0, MAX_RECORD_LENGTH);
}, [testSuites, dropdownSearch, urlSearchParams]);

const handleOnSearch = useMemo(
() =>
PLACEHOLDER_TEST_SUITES.map(suite => ({
value: suite,
label: suite,
})),
[]
debounce((value: string) => {
setDropdownSearch(value);
}, 500),
[setDropdownSearch]
);

useEffect(() => {
// Create a use effect to cancel handleOnSearch fn on unmount to avoid memory leaks
return () => {
handleOnSearch.cancel();
};
}, [handleOnSearch]);

/**
* Validated values that only includes the currently available test suites
*/
const value = useMemo(() => {
const urlTestSuites = searchParams.getAll(TEST_SUITE);
return urlTestSuites.filter(suite => PLACEHOLDER_TEST_SUITES.includes(suite));
}, [searchParams]);
const urlTestSuites = urlSearchParams.getAll(TEST_SUITES);
return urlTestSuites.filter(suite => testSuites?.includes(suite));
}, [urlSearchParams, testSuites]);

return (
<HybridFilter
Expand All @@ -62,14 +88,14 @@ export function TestSuiteDropdown() {
value={value}
defaultValue={[]}
onChange={handleChange}
onSearch={handleOnSearch}
// TODO: Add the disabled and emptyMessage when connected to backend hook
menuTitle={t('Filter Test Suites')}
menuWidth={`${MAX_SUITE_UI_LENGTH}em`}
trigger={triggerProps => {
const areAllSuitesSelected =
value.length === 0 ||
PLACEHOLDER_TEST_SUITES.every(suite => value.includes(suite));
// Show 2 suites only if the combined string's length does not exceed 22.
value.length === 0 || testSuites?.every(suite => value.includes(suite));
// Show 2 suites only if the combined string's length does not exceed MAX_SUITE_UI_LENGTH.
// Otherwise show only 1 test suite.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we say doesn't exceed, but on line 103 we use < rather than <=

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Techincalllyyyy, you are right haha, changing it

const totalLength =
(value[0]?.length ?? 0) + (value[1]?.length ?? 0) + (value[1] ? 2 : 0);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {useMemo} from 'react';

import {useCodecovContext} from 'sentry/components/codecov/context/codecovContext';
import type {QueryKeyEndpointOptions} from 'sentry/utils/queryClient';
import {useQuery} from 'sentry/utils/queryClient';
import useApi from 'sentry/utils/useApi';
import useOrganization from 'sentry/utils/useOrganization';

type TestSuite = {
testSuites: string[];
};

type QueryKey = [url: string, endpointOptions: QueryKeyEndpointOptions];

export function useTestSuites() {
const api = useApi();
const organization = useOrganization();
const {integratedOrgId, repository} = useCodecovContext();

const {data, ...rest} = useQuery<TestSuite, Error, TestSuite, QueryKey>({
queryKey: [
`/organizations/${organization.slug}/prevent/owner/${integratedOrgId}/repository/${repository}/test-suites/`,
{query: {}},
],
queryFn: async ({queryKey: [url]}): Promise<TestSuite> => {
const result = await api.requestPromise(url, {
method: 'GET',
query: {},
});

return result as TestSuite;
},
enabled: !!(integratedOrgId && repository),
});

const memoizedData = useMemo(() => {
return data?.testSuites || [];
}, [data]);

return {
data: memoizedData,
// TODO: only provide the values that we're interested in
...rest,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const mockTestResultsResponse = {
hasPreviousPage: false,
startCursor: 'cursor000',
},
defaultBranch: 'main',
results: [
{
name: 'test_example_function',
Expand Down Expand Up @@ -48,6 +49,7 @@ const mockTestResultsResponse = {
};

const emptyTestResultsResponse = {
defaultBranch: 'another',
pageInfo: {
endCursor: null,
hasNextPage: false,
Expand Down Expand Up @@ -113,13 +115,14 @@ describe('useInfiniteTestResults', () => {
expect(result.current.isSuccess).toBe(true);
});

expect(result.current.data).toHaveLength(2);
expect(result.current.data.testResults).toHaveLength(2);
expect(result.current.totalCount).toBe(150);
expect(result.current.startCursor).toBe('cursor000');
expect(result.current.endCursor).toBe('cursor123');

// Verifies that the data is transformed correctly
expect(result.current.data[0]).toEqual({
expect(result.current.data.defaultBranch).toBe('main');
expect(result.current.data.testResults[0]).toEqual({
testName: 'test_example_function',
averageDurationMs: 45, // avgDuration * 1000
commitsFailed: 5,
Expand Down Expand Up @@ -173,7 +176,7 @@ describe('useInfiniteTestResults', () => {
expect(result.current.isSuccess).toBe(true);
});

expect(result.current.data).toHaveLength(2);
expect(result.current.data.testResults).toHaveLength(2);
expect(result.current.totalCount).toBe(150);
expect(result.current.hasNextPage).toBe(true);
});
Expand Down Expand Up @@ -211,7 +214,7 @@ describe('useInfiniteTestResults', () => {
expect(result.current.isSuccess).toBe(true);
});

expect(result.current.data).toHaveLength(0);
expect(result.current.data.testResults).toHaveLength(0);
expect(result.current.totalCount).toBe(0);
expect(result.current.startCursor).toBeNull();
expect(result.current.endCursor).toBeNull();
Expand Down Expand Up @@ -247,7 +250,7 @@ describe('useInfiniteTestResults', () => {
});

expect(result.current.error).toBeDefined();
expect(result.current.data).toHaveLength(0);
expect(result.current.data.testResults).toHaveLength(0);
expect(result.current.totalCount).toBe(0);
});

Expand Down Expand Up @@ -300,7 +303,7 @@ describe('useInfiniteTestResults', () => {
expect(result.current.isSuccess).toBe(true);
});

expect(result.current.data).toHaveLength(2);
expect(result.current.data.testResults).toHaveLength(2);
expect(result.current.totalCount).toBe(150);
});
});
12 changes: 11 additions & 1 deletion static/app/views/codecov/tests/queries/useGetTestResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type TestResultItem = {
};

interface TestResults {
defaultBranch: string;
pageInfo: {
endCursor: string;
hasNextPage: boolean;
Expand Down Expand Up @@ -77,6 +78,7 @@ export function useInfiniteTestResults({
const signedSortBy = sortValueToSortKey(sortBy);

const term = searchParams.get('term') || '';
const testSuites = searchParams.getAll('testSuites') || null;

const filterBy = searchParams.get('filterBy') as SummaryFilterKey;
let mappedFilterBy = null;
Expand All @@ -101,6 +103,7 @@ export function useInfiniteTestResults({
term,
cursor,
navigation,
testSuites,
},
},
],
Expand All @@ -123,6 +126,7 @@ export function useInfiniteTestResults({
branch,
term,
...(mappedFilterBy ? {filterBy: mappedFilterBy} : {}),
...(testSuites ? {testSuites} : {}),
...(cursor ? {cursor} : {}),
...(navigation ? {navigation} : {}),
},
Expand All @@ -144,6 +148,7 @@ export function useInfiniteTestResults({
: undefined;
},
initialPageParam: null,
enabled: !!(integratedOrgId && repository && branch && codecovPeriod),
});

const memoizedData = useMemo(
Expand Down Expand Up @@ -178,11 +183,16 @@ export function useInfiniteTestResults({
);

return {
data: memoizedData,
data: {
testResults: memoizedData,
defaultBranch: data?.pages?.[0]?.[0].defaultBranch,
},
totalCount: data?.pages?.[0]?.[0]?.totalCount ?? 0,
startCursor: data?.pages?.[0]?.[0]?.pageInfo?.startCursor,
endCursor: data?.pages?.[0]?.[0]?.pageInfo?.endCursor,
// TODO: only provide the values that we're interested in
...rest,
};
}

export type UseInfiniteTestResultsResult = ReturnType<typeof useInfiniteTestResults>;
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ export function isAValidSort(sort: Sort): sort is ValidSort {

interface Props {
response: {
data: Row[];
data: {
testResults: Row[];
};
isLoading: boolean;
};
sort: ValidSort;
Expand All @@ -81,6 +83,7 @@ export default function TestAnalyticsTable({response, sort}: Props) {
const {data, isLoading} = response;
const [searchParams] = useSearchParams();
const wrapToggleValue = searchParams.get('wrap') === 'true';
const testResults = data.testResults;

const selectorEmptyMessage = (
<MessageContainer>
Expand All @@ -98,7 +101,7 @@ export default function TestAnalyticsTable({response, sort}: Props) {
<GridEditable
aria-label={t('Test Analytics')}
isLoading={isLoading}
data={data ?? []}
data={testResults ?? []}
emptyMessage={selectorEmptyMessage}
columnOrder={COLUMNS_ORDER}
// TODO: This isn't used as per the docs but is still required. Test if
Expand Down
Loading
Loading