-
Notifications
You must be signed in to change notification settings - Fork 16
Add unit test infrastructure and inline snapshot tests #245
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| name: Unit Tests | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| branches: [main] | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pin all GitHub Actions to immutable commit SHAs. Line 13, Line 15, and Line 24 use mutable tags ( Suggested hardening patch- - uses: actions/checkout@v4
+ - uses: actions/checkout@<full-commit-sha>
- - uses: actions/setup-node@v4
+ - uses: actions/setup-node@<full-commit-sha>
- - uses: codecov/codecov-action@v5
+ - uses: codecov/codecov-action@<full-commit-sha>Also applies to: 15-15, 24-24 🧰 Tools🪛 zizmor (1.25.2)[warning] 13-13: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false (artipacked) [error] 13-13: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy) (unpinned-uses) 🤖 Prompt for AI AgentsSource: Linters/SAST tools Disable checkout credential persistence before running dependency-controlled scripts. On Line 13, Suggested hardening patch+permissions:
+ contents: read
+
jobs:
test:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@<full-commit-sha>
+ with:
+ persist-credentials: false🧰 Tools🪛 zizmor (1.25.2)[warning] 13-13: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false (artipacked) [error] 13-13: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy) (unpinned-uses) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
|
|
||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '22' | ||
| cache: 'yarn' | ||
|
|
||
| - run: yarn install --frozen-lockfile | ||
|
|
||
| - run: yarn test:coverage | ||
|
|
||
| - uses: codecov/codecov-action@v5 | ||
| with: | ||
| files: coverage/coverage-final.json | ||
| flags: unit-tests | ||
| fail_ci_if_error: false | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ | |
| node_modules | ||
| .DS_Store | ||
| dist | ||
| coverage | ||
| yarn-error.log | ||
|
|
||
| # VisualStudioCode ### | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| // @ts-ignore — React is used by JSX but react-jsx transform flags it as unused | ||
| import * as React from 'react'; | ||
|
|
||
| export type K8sResourceCommon = { | ||
| apiVersion?: string; | ||
| kind?: string; | ||
| metadata?: { | ||
| name?: string; | ||
| namespace?: string; | ||
| labels?: Record<string, string>; | ||
| annotations?: Record<string, string>; | ||
| deletionTimestamp?: string; | ||
| [key: string]: unknown; | ||
| }; | ||
| [key: string]: unknown; | ||
| }; | ||
|
|
||
| export const k8sListItems = jest.fn(); | ||
|
|
||
| export const K8sResourceConditionStatus = { | ||
| True: 'True', | ||
| False: 'False', | ||
| Unknown: 'Unknown', | ||
| }; | ||
|
|
||
| export const getGroupVersionKindForModel = jest.fn( | ||
| (model: any) => `${model.apiGroup || 'core'}~${model.apiVersion}~${model.kind}`, | ||
| ); | ||
|
|
||
| export const useAccessReview = jest.fn(() => [true, true]); | ||
|
|
||
| export type ColoredIconProps = { | ||
| className?: string; | ||
| title?: string; | ||
| }; | ||
|
|
||
| export const GreenCheckCircleIcon = ({ className, title }: any) => ( | ||
| <svg data-icon="GreenCheckCircleIcon" className={className} aria-label={title} /> | ||
| ); | ||
| export const BlueInfoCircleIcon = ({ className, title }: any) => ( | ||
| <svg data-icon="BlueInfoCircleIcon" className={className} aria-label={title} /> | ||
| ); | ||
| export const RedExclamationCircleIcon = ({ className, title }: any) => ( | ||
| <svg data-icon="RedExclamationCircleIcon" className={className} aria-label={title} /> | ||
| ); | ||
| export const YellowExclamationTriangleIcon = ({ className, title }: any) => ( | ||
| <svg data-icon="YellowExclamationTriangleIcon" className={className} aria-label={title} /> | ||
| ); | ||
|
|
||
| export const CamelCaseWrap = ({ value }: { value: string }) => value || ''; | ||
| export const Timestamp = ({ timestamp }: { timestamp: string }) => timestamp || ''; | ||
| export const ResourceLink = ({ kind, name, namespace, groupVersionKind }: any) => | ||
| `[${groupVersionKind ? `${groupVersionKind.kind}` : kind}] ${name}`; | ||
|
|
||
| export const k8sUpdate = jest.fn((opts: any) => Promise.resolve(opts.data)); | ||
| export const k8sPatch = jest.fn((opts: any) => Promise.resolve(opts.resource)); | ||
|
|
||
| export enum Operator { | ||
| Exists = 'Exists', | ||
| DoesNotExist = 'DoesNotExist', | ||
| In = 'In', | ||
| NotIn = 'NotIn', | ||
| Equals = 'Equals', | ||
| NotEquals = 'NotEquals', | ||
| GreaterThan = 'GreaterThan', | ||
| LessThan = 'LessThan', | ||
| NotEqual = 'NotEqual', | ||
| } | ||
|
|
||
| export type K8sModel = any; | ||
| export type Selector = any; | ||
| export type K8sResourceCondition = any; | ||
| export type K8sResourceKind = any; | ||
| export type K8sResourceKindReference = string; | ||
| export type GroupVersionKind = string; | ||
| export type K8sVerb = string; | ||
| export type SetFeatureFlag = (flag: string, value: boolean) => void; | ||
| export type MatchLabels = Record<string, string>; | ||
| export type ObjectMetadata = any; | ||
| export type NodeAddress = any; | ||
| export type NodeCondition = any; | ||
| export type ObjectReference = any; | ||
| export type TaintEffect = string; | ||
| export type OwnerReference = { | ||
| apiVersion: string; | ||
| kind: string; | ||
| name: string; | ||
| uid: string; | ||
| }; | ||
| export type Action = { | ||
| id: string; | ||
| label: string; | ||
| description?: string; | ||
| cta?: () => void; | ||
| disabled?: boolean; | ||
| icon?: any; | ||
| accessReview?: any; | ||
| }; | ||
|
|
||
| export enum AllPodStatus { | ||
| Running = 'Running', | ||
| NotReady = 'Not Ready', | ||
| Warning = 'Warning', | ||
| Empty = 'Empty', | ||
| Failed = 'Failed', | ||
| Pending = 'Pending', | ||
| Succeeded = 'Succeeded', | ||
| Terminating = 'Terminating', | ||
| Unknown = 'Unknown', | ||
| ScaledTo0 = 'Scaled to 0', | ||
| Idle = 'Idle', | ||
| AutoScaledTo0 = 'Autoscaled to 0', | ||
| ScalingUp = 'Scaling Up', | ||
| CrashLoopBackOff = 'CrashLoopBackOff', | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export type JSONSchema7 = any; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import * as React from 'react'; | ||
|
|
||
| export const Button: React.FC<any> = ({ children, variant, isInline, component, ...rest }) => ( | ||
| <button data-variant={variant} {...rest}>{children}</button> | ||
| ); | ||
|
|
||
| export const Popover: React.FC<any> = ({ headerContent, bodyContent, children }) => ( | ||
| <div data-testid="popover"> | ||
| <div data-testid="popover-header">{headerContent}</div> | ||
| <div data-testid="popover-body">{bodyContent}</div> | ||
| {children} | ||
| </div> | ||
| ); | ||
|
|
||
| export const MenuToggle = React.forwardRef<any, any>(({ children, variant, ...rest }, ref) => ( | ||
| <button ref={ref} data-variant={variant} {...rest}>{children}</button> | ||
| )); | ||
| MenuToggle.displayName = 'MenuToggle'; | ||
|
|
||
| export type MenuToggleElement = HTMLButtonElement; | ||
| export type MenuToggleProps = any; | ||
|
|
||
| export const Dropdown: React.FC<any> = ({ children, isOpen, toggle, ...props }) => ( | ||
| <div data-testid="dropdown" data-open={isOpen} {...props}> | ||
| {typeof toggle === 'function' ? toggle(null) : toggle} | ||
| {isOpen && children} | ||
| </div> | ||
| ); | ||
|
|
||
| export const DropdownList: React.FC<any> = ({ children }) => <ul>{children}</ul>; | ||
|
|
||
| export const DropdownItem: React.FC<any> = ({ children, description, isDisabled, ...props }) => ( | ||
| <li data-disabled={isDisabled} {...props}>{children}{description && <small>{description}</small>}</li> | ||
| ); | ||
|
|
||
| export const Tooltip: React.FC<any> = ({ content, children }) => ( | ||
| <span data-testid="tooltip" data-tooltip={typeof content === 'string' ? content : undefined}> | ||
| {children} | ||
| </span> | ||
| ); | ||
|
|
||
| export const TooltipPosition = { | ||
| top: 'top', | ||
| bottom: 'bottom', | ||
| left: 'left', | ||
| right: 'right', | ||
| }; | ||
|
|
||
| export const Title: React.FC<any> = ({ children, headingLevel: Tag = 'h2', className }) => ( | ||
| <Tag className={className}>{children}</Tag> | ||
| ); | ||
|
|
||
| export const Label: React.FC<any> = ({ children, className, color, href }) => ( | ||
| <span data-testid="label" className={className} data-color={color} data-href={href}>{children}</span> | ||
| ); | ||
|
|
||
| export const LabelGroup: React.FC<any> = ({ children, className, numLabels }) => ( | ||
| <div data-testid="label-group" className={className} data-num-labels={numLabels}>{children}</div> | ||
| ); | ||
|
|
||
| export const Icon: React.FC<any> = ({ children, size }) => ( | ||
| <span data-testid="icon" data-size={size}>{children}</span> | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import * as React from 'react'; | ||
|
|
||
| const icon = (name: string): React.FC<any> => { | ||
| const Icon: React.FC<any> = ({ className, title, color }) => ( | ||
| <svg data-icon={name} className={className} style={color ? { color } : undefined} aria-label={title} /> | ||
| ); | ||
| Icon.displayName = name; | ||
| return Icon; | ||
| }; | ||
|
|
||
| export const ArrowCircleUpIcon = icon('ArrowCircleUpIcon'); | ||
| export const BanIcon = icon('BanIcon'); | ||
| export const CheckIcon = icon('CheckIcon'); | ||
| export const CircleNotchIcon = icon('CircleNotchIcon'); | ||
| export const ExclamationCircleIcon = icon('ExclamationCircleIcon'); | ||
| export const GhostIcon = icon('GhostIcon'); | ||
| export const HeartBrokenIcon = icon('HeartBrokenIcon'); | ||
| export const HeartIcon = icon('HeartIcon'); | ||
| export const MonitoringIcon = icon('MonitoringIcon'); | ||
| export const OutlinedPauseCircleIcon = icon('OutlinedPauseCircleIcon'); | ||
| export const PausedIcon = icon('PausedIcon'); | ||
| export const PendingIcon = icon('PendingIcon'); | ||
| export const ResourcesAlmostFullIcon = icon('ResourcesAlmostFullIcon'); | ||
| export const ResourcesFullIcon = icon('ResourcesFullIcon'); | ||
| export const SyncAltIcon = icon('SyncAltIcon'); | ||
| export const UnknownIcon = icon('UnknownIcon'); | ||
| export const EllipsisVIcon = icon('EllipsisVIcon'); | ||
| export const OutlinedQuestionCircleIcon = icon('OutlinedQuestionCircleIcon'); | ||
| export const TopologyIcon = icon('TopologyIcon'); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import * as React from 'react'; | ||
|
|
||
| export const Table: React.FC<any> = ({ children, ...props }) => <table {...props}>{children}</table>; | ||
| export const Thead: React.FC<any> = ({ children }) => <thead>{children}</thead>; | ||
| export const Tbody: React.FC<any> = ({ children }) => <tbody>{children}</tbody>; | ||
| export const Tr: React.FC<any> = ({ children, ...props }) => <tr {...props}>{children}</tr>; | ||
| export const Th: React.FC<any> = ({ children }) => <th>{children}</th>; | ||
| export const Td: React.FC<any> = ({ children, dataLabel, ...props }) => ( | ||
| <td data-label={dataLabel} {...props}>{children}</td> | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| const token = { name: 'mock-token', value: '#000000', var: 'var(--mock-token)' }; | ||
|
|
||
| export default token; | ||
| module.exports = token; | ||
| module.exports.default = token; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| export enum NodeStatus { | ||
| default = 'default', | ||
| success = 'success', | ||
| warning = 'warning', | ||
| danger = 'danger', | ||
| info = 'info', | ||
| } | ||
|
|
||
| export type NodeModel = any; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| const t = (key: string) => key; | ||
| export const useTranslation = () => ({ t, i18n: { language: 'en' } }); | ||
| export const getI18n = () => ({ t }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| module.exports = {}; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import type { Config } from '@jest/types'; | ||
|
|
||
| const config: Config.InitialOptions = { | ||
| preset: 'ts-jest', | ||
| testEnvironment: 'jsdom', | ||
| testRegex: '.*\\.test\\.tsx?$', | ||
| moduleNameMapper: { | ||
| '^src/(.*)': '<rootDir>/src/$1', | ||
| '@gitops/(.*)': '<rootDir>/src/gitops/$1', | ||
| '@gitops-models/(.*)': '<rootDir>/src/gitops/models/$1', | ||
| '@gitops-services/(.*)': '<rootDir>/src/gitops/services/$1', | ||
| '@gitops-shared/(.*)': '<rootDir>/src/gitops/components/shared/$1', | ||
| '@openshift-console/dynamic-plugin-sdk/lib/(.*)': '<rootDir>/__mocks__/dynamic-plugin-sdk.tsx', | ||
| '@openshift-console/dynamic-plugin-sdk': '<rootDir>/__mocks__/dynamic-plugin-sdk.tsx', | ||
| '@openshift-console/dynamic-plugin-sdk-internal/(.*)': '<rootDir>/__mocks__/dynamic-plugin-sdk.tsx', | ||
| '@patternfly/react-icons': '<rootDir>/__mocks__/patternfly-react-icons.tsx', | ||
| '@patternfly/react-core': '<rootDir>/__mocks__/patternfly-react-core.tsx', | ||
| '@patternfly/react-tokens/(.*)': '<rootDir>/__mocks__/patternfly-react-tokens.ts', | ||
| '@patternfly/react-table': '<rootDir>/__mocks__/patternfly-react-table.tsx', | ||
| '@patternfly/react-topology': '<rootDir>/__mocks__/patternfly-react-topology.ts', | ||
| '^lodash-es$': 'lodash', | ||
| '^json-schema$': '<rootDir>/__mocks__/json-schema.ts', | ||
| 'react-i18next': '<rootDir>/__mocks__/react-i18next.ts', | ||
| '\\.(css|scss)$': '<rootDir>/__mocks__/style-mock.ts', | ||
| }, | ||
| testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/dist/'], | ||
| collectCoverageFrom: [ | ||
| 'src/**/*.{ts,tsx}', | ||
| '!src/**/*.d.ts', | ||
| '!src/**/index.ts', | ||
| ], | ||
| }; | ||
|
|
||
| export default config; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for doing this Adam. Similar to Argo CD, it uses jest as well.
I think we should keep what you have.
https://github.com/argoproj/argo-cd/blob/cc3165f4189243d7b9a627b07b60138ed80ca795/.github/workflows/ci-build.yaml#L353