Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -7,7 +7,20 @@ import ConfigurationCard, { ConfigurationCardProps } from './ConfigurationCard'

const mockedProps = mock<ConfigurationCardProps>()

const mockUseConfigurationState = jest.fn()
jest.mock('./hooks/useConfigurationState', () => ({
useConfigurationState: () => mockUseConfigurationState(),
}))

describe('ConfigurationCard', () => {
beforeEach(() => {
mockUseConfigurationState.mockReturnValue({
hasChanges: false,
isValid: true,
configValidationErrors: [],
})
})

it('should render with correct title', () => {
render(<ConfigurationCard {...instance(mockedProps)} />)

Expand Down Expand Up @@ -49,4 +62,98 @@ describe('ConfigurationCard', () => {
expect(card).toHaveAttribute('tabIndex', '0')
expect(card).toHaveAttribute('role', 'button')
})

describe('Changes indicator', () => {
it('should not show changes indicator when no changes', () => {
mockUseConfigurationState.mockReturnValue({
hasChanges: false,
isValid: true,
configValidationErrors: [],
})

render(<ConfigurationCard {...instance(mockedProps)} />)

expect(
screen.queryByTestId('updated-configuration-highlight'),
).not.toBeInTheDocument()
})

it('should show changes indicator when config has changes', () => {
mockUseConfigurationState.mockReturnValue({
hasChanges: true,
isValid: true,
configValidationErrors: [],
})

render(<ConfigurationCard {...instance(mockedProps)} />)

expect(
screen.getByTestId('updated-configuration-highlight'),
).toBeInTheDocument()
})
})

describe('Validation errors', () => {
it('should not show error icon when config is valid', () => {
mockUseConfigurationState.mockReturnValue({
hasChanges: false,
isValid: true,
configValidationErrors: [],
})

render(<ConfigurationCard {...instance(mockedProps)} />)

expect(
screen.queryByTestId('rdi-pipeline-nav__error-configuration'),
).not.toBeInTheDocument()
})

it('should show error icon when config has validation errors', () => {
mockUseConfigurationState.mockReturnValue({
hasChanges: false,
isValid: false,
configValidationErrors: [
'Invalid configuration',
'Missing required field',
],
})

render(<ConfigurationCard {...instance(mockedProps)} />)

expect(
screen.getByTestId('rdi-pipeline-nav__error-configuration'),
).toBeInTheDocument()
})

it('should handle single validation error', () => {
mockUseConfigurationState.mockReturnValue({
hasChanges: false,
isValid: false,
configValidationErrors: ['Single error'],
})

render(<ConfigurationCard {...instance(mockedProps)} />)

expect(
screen.getByTestId('rdi-pipeline-nav__error-configuration'),
).toBeInTheDocument()
})
})

it('should show both changes indicator and error icon when config has changes and errors', () => {
mockUseConfigurationState.mockReturnValue({
hasChanges: true,
isValid: false,
configValidationErrors: ['Invalid configuration'],
})

render(<ConfigurationCard {...instance(mockedProps)} />)

expect(
screen.getByTestId('updated-configuration-highlight'),
).toBeInTheDocument()
expect(
screen.getByTestId('rdi-pipeline-nav__error-configuration'),
).toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import React from 'react'
import { RdiPipelineTabs } from 'uiSrc/slices/interfaces'

import { RiTooltip } from 'uiSrc/components'
import { Indicator } from 'uiSrc/components/base/text/text.styles'
import { Row } from 'uiSrc/components/base/layout/flex'
import { Text } from 'uiSrc/components/base/text'
import { Icon, ToastNotificationIcon } from 'uiSrc/components/base/icons'
import { useConfigurationState } from './hooks'

import BaseCard, { BaseCardProps } from './BaseCard'
import ValidationErrorsList from '../../validation-errors-list/ValidationErrorsList'

export type ConfigurationCardProps = Omit<
BaseCardProps,
Expand All @@ -13,6 +22,9 @@ const ConfigurationCard = ({
onSelect,
isSelected,
}: ConfigurationCardProps) => {
const { hasChanges, isValid, configValidationErrors } =
useConfigurationState()

const handleClick = () => {
onSelect(RdiPipelineTabs.Config)
}
Expand All @@ -25,7 +37,39 @@ const ConfigurationCard = ({
onClick={handleClick}
data-testid={`rdi-nav-btn-${RdiPipelineTabs.Config}`}
>
Configuration file
<Row gap="s" align="center">
{!hasChanges && <Indicator $color="transparent" />}

{hasChanges && (
<RiTooltip
content="This file contains undeployed changes."
position="top"
>
<Indicator
$color="informative"
data-testid={`updated-configuration-highlight`}
/>
</RiTooltip>
)}

<Text>Configuration file</Text>

{!isValid && (
<RiTooltip
position="right"
content={
<ValidationErrorsList validationErrors={configValidationErrors} />
}
>
<Icon
icon={ToastNotificationIcon}
color="danger500"
size="M"
data-testid={`rdi-pipeline-nav__error-configuration`}
/>
</RiTooltip>
)}
</Row>
</BaseCard>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useConfigurationState'
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { renderHook } from 'uiSrc/utils/test-utils'
import { rdiPipelineSelector, initialState } from 'uiSrc/slices/rdi/pipeline'
import { IStateRdiPipeline, FileChangeType } from 'uiSrc/slices/interfaces'
import { useConfigurationState } from './useConfigurationState'

jest.mock('uiSrc/slices/rdi/pipeline', () => ({
...jest.requireActual('uiSrc/slices/rdi/pipeline'),
rdiPipelineSelector: jest.fn(),
}))

const mockRdiPipelineSelector = rdiPipelineSelector as jest.MockedFunction<
typeof rdiPipelineSelector
>

const createMockState = (
overrides: Partial<IStateRdiPipeline> = {},
): IStateRdiPipeline => ({
...initialState,
...overrides,
})

describe('useConfigurationState', () => {
beforeEach(() => {
jest.clearAllMocks()
})

it('should return correct state when no changes and no validation errors', () => {
mockRdiPipelineSelector.mockReturnValue(
createMockState({
changes: {},
configValidationErrors: [],
}),
)

const { result } = renderHook(() => useConfigurationState())

expect(result.current).toEqual({
hasChanges: false,
isValid: true,
configValidationErrors: [],
})
})

it('should return hasChanges as true when config has changes', () => {
mockRdiPipelineSelector.mockReturnValue(
createMockState({
changes: { config: FileChangeType.Modified },
configValidationErrors: [],
}),
)

const { result } = renderHook(() => useConfigurationState())

expect(result.current).toEqual({
hasChanges: true,
isValid: true,
configValidationErrors: [],
})
})

it('should return isValid as false when config has validation errors', () => {
mockRdiPipelineSelector.mockReturnValue(
createMockState({
changes: {},
configValidationErrors: [
'Invalid configuration',
'Missing required field',
],
}),
)

const { result } = renderHook(() => useConfigurationState())

expect(result.current).toEqual({
hasChanges: false,
isValid: false,
configValidationErrors: [
'Invalid configuration',
'Missing required field',
],
})
})

it('should handle both changes and validation errors', () => {
mockRdiPipelineSelector.mockReturnValue(
createMockState({
changes: { config: FileChangeType.Added },
configValidationErrors: ['Configuration error'],
}),
)

const { result } = renderHook(() => useConfigurationState())

expect(result.current).toEqual({
hasChanges: true,
isValid: false,
configValidationErrors: ['Configuration error'],
})
})

it('should handle empty validation errors array', () => {
mockRdiPipelineSelector.mockReturnValue(
createMockState({
changes: {},
configValidationErrors: [],
}),
)

const { result } = renderHook(() => useConfigurationState())

expect(result.current.isValid).toBe(true)
})

it('should handle single validation error', () => {
mockRdiPipelineSelector.mockReturnValue(
createMockState({
changes: {},
configValidationErrors: ['Single error'],
}),
)

const { result } = renderHook(() => useConfigurationState())

expect(result.current).toEqual({
hasChanges: false,
isValid: false,
configValidationErrors: ['Single error'],
})
})

it('should handle multiple validation errors', () => {
const errors = ['Error 1', 'Error 2', 'Error 3']
mockRdiPipelineSelector.mockReturnValue(
createMockState({
changes: {},
configValidationErrors: errors,
}),
)

const { result } = renderHook(() => useConfigurationState())

expect(result.current).toEqual({
hasChanges: false,
isValid: false,
configValidationErrors: errors,
})
})

it('should handle changes in other files without affecting config state', () => {
mockRdiPipelineSelector.mockReturnValue(
createMockState({
changes: {
job1: FileChangeType.Modified,
job2: FileChangeType.Added,
// no config changes
},
configValidationErrors: [],
}),
)

const { result } = renderHook(() => useConfigurationState())

expect(result.current.hasChanges).toBe(false)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useSelector } from 'react-redux'
import { rdiPipelineSelector } from 'uiSrc/slices/rdi/pipeline'

export interface ConfigurationState {
hasChanges: boolean
isValid: boolean
configValidationErrors: string[]
}

export const useConfigurationState = (): ConfigurationState => {
const { changes, configValidationErrors } = useSelector(rdiPipelineSelector)

const hasChanges = !!changes.config
const isValid = configValidationErrors.length === 0

return {
hasChanges,
isValid,
configValidationErrors,
}
}
Loading