Skip to content

Commit

Permalink
test(search): extract, add test for filter function
Browse files Browse the repository at this point in the history
  • Loading branch information
kahboom committed Nov 29, 2024
1 parent 6a53f70 commit 5b7db8d
Show file tree
Hide file tree
Showing 9 changed files with 466 additions and 138 deletions.
24 changes: 16 additions & 8 deletions client/config/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,38 @@ const config: JestConfigWithTsJest = {
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",

moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],

// Stub out resources and provide handling for tsconfig.json paths
moduleNameMapper: {
// stub out files that don't matter for tests
"\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js",
"\\.(xsd)$": "<rootDir>/__mocks__/styleMock.js",
"\\.(css|less)$": "<rootDir>/src/mocks/styleMock.ts",
"\\.(xsd)$": "<rootDir>/mocks/styleMock.ts",
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
"<rootDir>/__mocks__/fileMock.js",
"<rootDir>/src/mocks/fileMock.ts",
"@patternfly/react-icons/dist/esm/icons/":
"<rootDir>/__mocks__/fileMock.js",
"<rootDir>/src/mocks/fileMock.ts",

// match the paths in tsconfig.json
"@app/(.*)": "<rootDir>/src/app/$1",
"@assets/(.*)":
"<rootDir>../node_modules/@patternfly/react-core/dist/styles/assets/$1",
},

// moduleNameMapper: {
// "^@app/(.*)$": "<rootDir>/client/src/app/$1",
// "^@mocks/(.*)$": "<rootDir>/client/src/mocks/$1",
// },

// A list of paths to directories that Jest should use to search for files
roots: ["<rootDir>/src"],

// Code to set up the testing framework before each test file in the suite is executed
setupFilesAfterEnv: ["<rootDir>/setupTests.ts"],

// The test environment that will be used for testing
testEnvironment: "jest-environment-jsdom",
// testEnvironment: "jest-environment-jsdom",
testEnvironment: "jsdom", // or "node" for non-DOM tests

// The pattern or patterns Jest uses to find test files
testMatch: ["<rootDir>/src/**/*.{test,spec}.{js,jsx,ts,tsx}"],
Expand All @@ -42,9 +53,6 @@ const config: JestConfigWithTsJest = {
transform: {
"^.+\\.(js|mjs|ts|mts)x?$": "ts-jest",
},

// Code to set up the testing framework before each test file in the suite is executed
setupFilesAfterEnv: ["<rootDir>/src/app/test-config/setupTests.ts"],
};

export default config;
1 change: 0 additions & 1 deletion client/jest.setup.ts

This file was deleted.

File renamed without changes.
242 changes: 130 additions & 112 deletions client/src/app/pages/search/components/SearchMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import {
SearchInput,
} from "@patternfly/react-core";
import { FILTER_TEXT_CATEGORY_KEY } from "@app/Constants";
import { VulnerabilitySearchContext } from "@app/pages/vulnerability-list/vulnerability-context";
import { SbomSearchContext } from "@app/pages/sbom-list/sbom-context";
import { Label } from "@patternfly/react-core";
import { AdvisorySearchContext } from "@app/pages/advisory-list/advisory-context";
import { PackageSearchContext } from "@app/pages/package-list/package-context";
import { useFetchAdvisories } from "@app/queries/advisories";
import { HubRequestParams } from "@app/api/models";
import { useFetchPackages } from "@app/queries/packages";
import { useFetchSBOMs } from "@app/queries/sboms";
import { useFetchVulnerabilities } from "@app/queries/vulnerabilities";

export interface IEntity {
id: string;
Expand All @@ -32,91 +33,144 @@ export interface IEntity {
| undefined;
}

function useAllEntities() {
const [entityList, setEntityList] = React.useState<IEntity[]>([]);
// Filter function
export function filterEntityListByValue(list: IEntity[], searchString: string) {
// When the value of the search input changes, build a list of no more than 10 autocomplete options.
// Options which start with the search input value are listed first, followed by options which contain
// the search input value.
let options: React.JSX.Element[] = list
.filter(
(option) =>
option.id.toLowerCase().startsWith(searchString.toLowerCase()) ||
option.title?.toLowerCase().startsWith(searchString.toLowerCase()) ||
option.description?.toLowerCase().startsWith(searchString.toLowerCase())
)
.map((option) => (
<MenuItem
itemId={option.id}
key={option.id}
description={option.description}
to={option.navLink}
>
{option.title} <Label color={option.typeColor}>{option.type}</Label>
</MenuItem>
));

if (options.length > 10) {
options = options.slice(0, 10);
} else {
options = [
...options,
...list
.filter(
(option: IEntity) =>
!option.id.startsWith(searchString.toLowerCase()) &&
option.id.includes(searchString.toLowerCase())
)
.map((option: IEntity) => (
<MenuItem
itemId={option.id}
key={option.id}
description={option.description}
to={option.navLink}
>
{option.title} <Label color={option.typeColor}>{option.type}</Label>
</MenuItem>
)),
].slice(0, 10);
}

return options;
}

function useAllEntities(filterText: string) {
const params: HubRequestParams = {
filters: [
{ field: FILTER_TEXT_CATEGORY_KEY, operator: "~", value: filterText },
],
page: { pageNumber: 1, itemsPerPage: 10 },
};

const {
tableControls: { currentPageItems: advisories },
} = React.useContext(AdvisorySearchContext);
result: { data: advisories },
} = useFetchAdvisories({ ...params });

const {
tableControls: { currentPageItems: packages },
} = React.useContext(PackageSearchContext);
result: { data: packages },
} = useFetchPackages({ ...params });

const {
tableControls: { currentPageItems: sboms, filterState: sbomFilterState },
} = React.useContext(SbomSearchContext);
result: { data: sboms },
} = useFetchSBOMs({ ...params });

const {
tableControls: { currentPageItems: vulnerabilities },
} = React.useContext(VulnerabilitySearchContext);
result: { data: vulnerabilities },
} = useFetchVulnerabilities({ ...params });

const tmpArray: IEntity[] = [];

const transformedAdvisories: IEntity[] = advisories.map((item) => ({
id: item.identifier,
title: item.identifier,
description: item.title?.substring(0, 75),
navLink: `/advisories/${item.uuid}`,
type: "Advisory",
typeColor: "blue",
}));

const transformedPackages: IEntity[] = packages.map((item) => ({
id: item.uuid,
title: "item.decomposedPurl ? item.decomposedPurl?.name : item.purl",
description: "item.decomposedPurl?.namespace",
navLink: `/packages/${item.uuid}`,
type: "Package",
typeColor: "cyan",
}));

const transformedSboms: IEntity[] = sboms.map((item) => ({
id: item.id,
title: item.name,
description: item.authors.join(", "),
navLink: `/sboms/${item.id}`,
type: "SBOM",
typeColor: "purple",
}));

const transformedVulnerabilities: IEntity[] = vulnerabilities.map((item) => ({
id: item.identifier,
title: item.identifier,
description: item.description?.substring(0, 75),
navLink: `/vulnerabilities/${item.identifier}`,
type: "CVE",
typeColor: "orange",
}));

tmpArray.push(
...transformedAdvisories,
...transformedPackages,
...transformedSboms,
...transformedVulnerabilities
);

React.useEffect(() => {
function fetchAllEntities() {
const tmpArray: IEntity[] = [];

const transformedAdvisories: IEntity[] = advisories.map((item) => ({
id: item.identifier,
title: item.identifier,
description: item.title?.substring(0, 75),
navLink: `/advisories/${item.uuid}`,
type: "Advisory",
typeColor: "blue",
}));

const transformedPackages: IEntity[] = packages.map((item) => ({
id: item.uuid,
title: item.decomposedPurl ? item.decomposedPurl?.name : item.purl,
description: item.decomposedPurl?.namespace,
navLink: `/packages/${item.uuid}`,
type: "Package",
typeColor: "cyan",
}));

const transformedSboms: IEntity[] = sboms.map((item) => ({
id: item.id,
title: item.name,
description: item.authors.join(", "),
navLink: `/sboms/${item.id}`,
type: "SBOM",
typeColor: "purple",
}));

const transformedVulnerabilities: IEntity[] = vulnerabilities.map(
(item) => ({
id: item.identifier,
title: item.identifier,
description: item.description?.substring(0, 75),
navLink: `/vulnerabilities/${item.identifier}`,
type: "CVE",
typeColor: "orange",
})
);

tmpArray.push(
...transformedAdvisories,
...transformedPackages,
...transformedSboms,
...transformedVulnerabilities
);
setEntityList(tmpArray);
}
// fetch on load
fetchAllEntities();
}, [advisories, packages, sboms, vulnerabilities]);
return {
list: entityList,
defaultValue:
sbomFilterState.filterValues[FILTER_TEXT_CATEGORY_KEY]?.[0] || "",
list: tmpArray,
defaultValue: "",
};
}

export interface ISearchMenu {
filterFunction?: (
list: IEntity[],
searchString: string
) => React.JSX.Element[];
onChangeSearch: (searchValue: string | undefined) => void;
}

export const SearchMenu: React.FC<ISearchMenu> = ({ onChangeSearch }) => {
const { list: entityList, defaultValue } = useAllEntities();
export const SearchMenu: React.FC<ISearchMenu> = ({
filterFunction = filterEntityListByValue,
onChangeSearch,
}) => {
const { list: entityList, defaultValue } = useAllEntities("");

const [searchValue, setSearchValue] = React.useState<string | undefined>(
defaultValue
Expand All @@ -139,45 +193,10 @@ export const SearchMenu: React.FC<ISearchMenu> = ({ onChangeSearch }) => {
) {
setIsAutocompleteOpen(true);

// When the value of the search input changes, build a list of no more than 10 autocomplete options.
// Options which start with the search input value are listed first, followed by options which contain
// the search input value.
let options: React.JSX.Element[] = entityList
.filter(
(option) =>
option.id.toLowerCase().startsWith(newValue.toLowerCase()) ||
option.title?.toLowerCase().startsWith(newValue.toLowerCase()) ||
option.description?.toLowerCase().startsWith(newValue.toLowerCase())
)
.map((option) => (
<MenuItem
itemId={option.id}
key={option.id}
description={option.description}
to={option.navLink}
>
{option.title} <Label color={option.typeColor}>{option.type}</Label>
</MenuItem>
));

if (options.length > 10) {
options = options.slice(0, 10);
} else {
options = [
...options,
...entityList
.filter(
(option: IEntity) =>
!option.id.startsWith(newValue.toLowerCase()) &&
option.id.includes(newValue.toLowerCase())
)
.map((option: IEntity) => (
<MenuItem itemId={option.id} key={option.id}>
{option.id}
</MenuItem>
)),
].slice(0, 10);
}
console.table(entityList);
console.log(newValue);
const options = filterFunction(entityList, newValue);
console.log(options);

// The menu is hidden if there are no options
setIsAutocompleteOpen(options.length > 0);
Expand All @@ -191,7 +210,6 @@ export const SearchMenu: React.FC<ISearchMenu> = ({ onChangeSearch }) => {

const onClearSearchValue = () => {
setSearchValue("");
onChangeSearch("");
};

const onSubmitInput = () => {
Expand Down
Loading

0 comments on commit 5b7db8d

Please sign in to comment.