Skip to content

Commit fd94b45

Browse files
AdamSalehclaude
andcommitted
Add unit test infrastructure and inline snapshot tests
Set up Jest with ts-jest, jsdom, and module mocks for PatternFly, OpenShift Console SDK, and react-i18next. All tests use toMatchInlineSnapshot() for zero-friction maintenance (jest -u). 24 test suites, 144 tests, 203 inline snapshots covering utility functions, service layer, and React components. Adds a GitHub Actions workflow to run tests and upload coverage to Codecov with the unit-tests flag. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Adam Saleh <adam@asaleh.net>
1 parent 05c5379 commit fd94b45

38 files changed

Lines changed: 3768 additions & 2457 deletions

.github/workflows/unit-tests.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Unit Tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- uses: actions/setup-node@v4
16+
with:
17+
node-version: '22'
18+
cache: 'yarn'
19+
20+
- run: yarn install --frozen-lockfile
21+
22+
- run: yarn test:coverage
23+
24+
- uses: codecov/codecov-action@v5
25+
with:
26+
files: coverage/coverage-final.json
27+
flags: unit-tests
28+
fail_ci_if_error: false

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
node_modules
1717
.DS_Store
1818
dist
19+
coverage
1920
yarn-error.log
2021

2122
# VisualStudioCode ###

__mocks__/dynamic-plugin-sdk.tsx

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// @ts-ignore — React is used by JSX but react-jsx transform flags it as unused
2+
import * as React from 'react';
3+
4+
export type K8sResourceCommon = {
5+
apiVersion?: string;
6+
kind?: string;
7+
metadata?: {
8+
name?: string;
9+
namespace?: string;
10+
labels?: Record<string, string>;
11+
annotations?: Record<string, string>;
12+
deletionTimestamp?: string;
13+
[key: string]: unknown;
14+
};
15+
[key: string]: unknown;
16+
};
17+
18+
export const k8sListItems = jest.fn();
19+
20+
export const K8sResourceConditionStatus = {
21+
True: 'True',
22+
False: 'False',
23+
Unknown: 'Unknown',
24+
};
25+
26+
export const getGroupVersionKindForModel = jest.fn(
27+
(model: any) => `${model.apiGroup || 'core'}~${model.apiVersion}~${model.kind}`,
28+
);
29+
30+
export const useAccessReview = jest.fn(() => [true, true]);
31+
32+
export type ColoredIconProps = {
33+
className?: string;
34+
title?: string;
35+
};
36+
37+
export const GreenCheckCircleIcon = ({ className, title }: any) => (
38+
<svg data-icon="GreenCheckCircleIcon" className={className} aria-label={title} />
39+
);
40+
export const BlueInfoCircleIcon = ({ className, title }: any) => (
41+
<svg data-icon="BlueInfoCircleIcon" className={className} aria-label={title} />
42+
);
43+
export const RedExclamationCircleIcon = ({ className, title }: any) => (
44+
<svg data-icon="RedExclamationCircleIcon" className={className} aria-label={title} />
45+
);
46+
export const YellowExclamationTriangleIcon = ({ className, title }: any) => (
47+
<svg data-icon="YellowExclamationTriangleIcon" className={className} aria-label={title} />
48+
);
49+
50+
export const CamelCaseWrap = ({ value }: { value: string }) => value || '';
51+
export const Timestamp = ({ timestamp }: { timestamp: string }) => timestamp || '';
52+
export const ResourceLink = ({ kind, name, namespace, groupVersionKind }: any) =>
53+
`[${groupVersionKind ? `${groupVersionKind.kind}` : kind}] ${name}`;
54+
55+
export const k8sUpdate = jest.fn((opts: any) => Promise.resolve(opts.data));
56+
export const k8sPatch = jest.fn((opts: any) => Promise.resolve(opts.resource));
57+
58+
export enum Operator {
59+
Exists = 'Exists',
60+
DoesNotExist = 'DoesNotExist',
61+
In = 'In',
62+
NotIn = 'NotIn',
63+
Equals = 'Equals',
64+
NotEquals = 'NotEquals',
65+
GreaterThan = 'GreaterThan',
66+
LessThan = 'LessThan',
67+
NotEqual = 'NotEqual',
68+
}
69+
70+
export type K8sModel = any;
71+
export type Selector = any;
72+
export type K8sResourceCondition = any;
73+
export type K8sResourceKind = any;
74+
export type K8sResourceKindReference = string;
75+
export type GroupVersionKind = string;
76+
export type K8sVerb = string;
77+
export type SetFeatureFlag = (flag: string, value: boolean) => void;
78+
export type MatchLabels = Record<string, string>;
79+
export type ObjectMetadata = any;
80+
export type NodeAddress = any;
81+
export type NodeCondition = any;
82+
export type ObjectReference = any;
83+
export type TaintEffect = string;
84+
export type OwnerReference = {
85+
apiVersion: string;
86+
kind: string;
87+
name: string;
88+
uid: string;
89+
};
90+
export type Action = {
91+
id: string;
92+
label: string;
93+
description?: string;
94+
cta?: () => void;
95+
disabled?: boolean;
96+
icon?: any;
97+
accessReview?: any;
98+
};
99+
100+
export enum AllPodStatus {
101+
Running = 'Running',
102+
NotReady = 'Not Ready',
103+
Warning = 'Warning',
104+
Empty = 'Empty',
105+
Failed = 'Failed',
106+
Pending = 'Pending',
107+
Succeeded = 'Succeeded',
108+
Terminating = 'Terminating',
109+
Unknown = 'Unknown',
110+
ScaledTo0 = 'Scaled to 0',
111+
Idle = 'Idle',
112+
AutoScaledTo0 = 'Autoscaled to 0',
113+
ScalingUp = 'Scaling Up',
114+
CrashLoopBackOff = 'CrashLoopBackOff',
115+
}

__mocks__/json-schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type JSONSchema7 = any;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import * as React from 'react';
2+
3+
export const Button: React.FC<any> = ({ children, variant, isInline, component, ...rest }) => (
4+
<button data-variant={variant} {...rest}>{children}</button>
5+
);
6+
7+
export const Popover: React.FC<any> = ({ headerContent, bodyContent, children }) => (
8+
<div data-testid="popover">
9+
<div data-testid="popover-header">{headerContent}</div>
10+
<div data-testid="popover-body">{bodyContent}</div>
11+
{children}
12+
</div>
13+
);
14+
15+
export const MenuToggle = React.forwardRef<any, any>(({ children, variant, ...rest }, ref) => (
16+
<button ref={ref} data-variant={variant} {...rest}>{children}</button>
17+
));
18+
MenuToggle.displayName = 'MenuToggle';
19+
20+
export type MenuToggleElement = HTMLButtonElement;
21+
export type MenuToggleProps = any;
22+
23+
export const Dropdown: React.FC<any> = ({ children, isOpen, toggle, ...props }) => (
24+
<div data-testid="dropdown" data-open={isOpen} {...props}>
25+
{typeof toggle === 'function' ? toggle(null) : toggle}
26+
{isOpen && children}
27+
</div>
28+
);
29+
30+
export const DropdownList: React.FC<any> = ({ children }) => <ul>{children}</ul>;
31+
32+
export const DropdownItem: React.FC<any> = ({ children, description, isDisabled, ...props }) => (
33+
<li data-disabled={isDisabled} {...props}>{children}{description && <small>{description}</small>}</li>
34+
);
35+
36+
export const Tooltip: React.FC<any> = ({ content, children }) => (
37+
<span data-testid="tooltip" data-tooltip={typeof content === 'string' ? content : undefined}>
38+
{children}
39+
</span>
40+
);
41+
42+
export const TooltipPosition = {
43+
top: 'top',
44+
bottom: 'bottom',
45+
left: 'left',
46+
right: 'right',
47+
};
48+
49+
export const Title: React.FC<any> = ({ children, headingLevel: Tag = 'h2', className }) => (
50+
<Tag className={className}>{children}</Tag>
51+
);
52+
53+
export const Label: React.FC<any> = ({ children, className, color, href }) => (
54+
<span data-testid="label" className={className} data-color={color} data-href={href}>{children}</span>
55+
);
56+
57+
export const LabelGroup: React.FC<any> = ({ children, className, numLabels }) => (
58+
<div data-testid="label-group" className={className} data-num-labels={numLabels}>{children}</div>
59+
);
60+
61+
export const Icon: React.FC<any> = ({ children, size }) => (
62+
<span data-testid="icon" data-size={size}>{children}</span>
63+
);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as React from 'react';
2+
3+
const icon = (name: string): React.FC<any> => {
4+
const Icon: React.FC<any> = ({ className, title, color }) => (
5+
<svg data-icon={name} className={className} style={color ? { color } : undefined} aria-label={title} />
6+
);
7+
Icon.displayName = name;
8+
return Icon;
9+
};
10+
11+
export const ArrowCircleUpIcon = icon('ArrowCircleUpIcon');
12+
export const BanIcon = icon('BanIcon');
13+
export const CheckIcon = icon('CheckIcon');
14+
export const CircleNotchIcon = icon('CircleNotchIcon');
15+
export const ExclamationCircleIcon = icon('ExclamationCircleIcon');
16+
export const GhostIcon = icon('GhostIcon');
17+
export const HeartBrokenIcon = icon('HeartBrokenIcon');
18+
export const HeartIcon = icon('HeartIcon');
19+
export const MonitoringIcon = icon('MonitoringIcon');
20+
export const OutlinedPauseCircleIcon = icon('OutlinedPauseCircleIcon');
21+
export const PausedIcon = icon('PausedIcon');
22+
export const PendingIcon = icon('PendingIcon');
23+
export const ResourcesAlmostFullIcon = icon('ResourcesAlmostFullIcon');
24+
export const ResourcesFullIcon = icon('ResourcesFullIcon');
25+
export const SyncAltIcon = icon('SyncAltIcon');
26+
export const UnknownIcon = icon('UnknownIcon');
27+
export const EllipsisVIcon = icon('EllipsisVIcon');
28+
export const OutlinedQuestionCircleIcon = icon('OutlinedQuestionCircleIcon');
29+
export const TopologyIcon = icon('TopologyIcon');
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as React from 'react';
2+
3+
export const Table: React.FC<any> = ({ children, ...props }) => <table {...props}>{children}</table>;
4+
export const Thead: React.FC<any> = ({ children }) => <thead>{children}</thead>;
5+
export const Tbody: React.FC<any> = ({ children }) => <tbody>{children}</tbody>;
6+
export const Tr: React.FC<any> = ({ children, ...props }) => <tr {...props}>{children}</tr>;
7+
export const Th: React.FC<any> = ({ children }) => <th>{children}</th>;
8+
export const Td: React.FC<any> = ({ children, dataLabel, ...props }) => (
9+
<td data-label={dataLabel} {...props}>{children}</td>
10+
);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const token = { name: 'mock-token', value: '#000000', var: 'var(--mock-token)' };
2+
3+
export default token;
4+
module.exports = token;
5+
module.exports.default = token;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export enum NodeStatus {
2+
default = 'default',
3+
success = 'success',
4+
warning = 'warning',
5+
danger = 'danger',
6+
info = 'info',
7+
}
8+
9+
export type NodeModel = any;

__mocks__/react-i18next.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const t = (key: string) => key;
2+
export const useTranslation = () => ({ t, i18n: { language: 'en' } });
3+
export const getI18n = () => ({ t });

0 commit comments

Comments
 (0)