Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
856dabf
Intial stepper version
vladsha-dev Oct 20, 2025
5a4e2aa
Fixed stepper and updated styles
vladsha-dev Oct 21, 2025
f7d1a65
Removed artifacts
vladsha-dev Oct 21, 2025
cc46cc7
Introduced HOC and aligned styles between intro and stepper
vladsha-dev Oct 21, 2025
4a529bd
Add Claim Eligibility component (partial impl)
VadimKovalenkoSNF Oct 21, 2025
6d35c15
Add unit tests
VadimKovalenkoSNF Oct 21, 2025
8b91fe7
Merge rebase conflicts
VadimKovalenkoSNF Oct 22, 2025
3b404df
Remove redundant style file
VadimKovalenkoSNF Oct 22, 2025
bbe2e6b
Remove unsuded code
VadimKovalenkoSNF Oct 22, 2025
80f34ec
Fix tests
VadimKovalenkoSNF Oct 22, 2025
1394c5c
Update .gitignore
VadimKovalenkoSNF Oct 22, 2025
252080f
Replace EligibilyStep to separate folder
VadimKovalenkoSNF Oct 24, 2025
0e12c3f
Fix rebase conflicts
VadimKovalenkoSNF Oct 27, 2025
21fc08d
Remove cursor test considerations
VadimKovalenkoSNF Oct 27, 2025
de94827
Remove unsuded util file
VadimKovalenkoSNF Oct 27, 2025
7990f2c
claim-form: restore Eligibility/Contact validation; fix Next navigation
VadimKovalenkoSNF Oct 27, 2025
b90f84d
Update button styles of popup window
VadimKovalenkoSNF Oct 27, 2025
4d79fed
claim-form: require non-null relationship in eligibility validation
VadimKovalenkoSNF Oct 27, 2025
d013142
claim-form: use flex styles for sectionTitle
VadimKovalenkoSNF Oct 27, 2025
c89e15b
update unit tests
VadimKovalenkoSNF Oct 27, 2025
46bdbd6
Apply minor fixes
VadimKovalenkoSNF Oct 27, 2025
6f09075
Remove disabled linter rule
VadimKovalenkoSNF Oct 27, 2025
81bd188
Apply minor fixes for styles and tests
VadimKovalenkoSNF Oct 27, 2025
eeafbb6
docs(release): document Eligibility Step in claim flow (OSDEV-2201)
VadimKovalenkoSNF Oct 27, 2025
8693d60
Fix test and lint issues
VadimKovalenkoSNF Oct 27, 2025
eb69a29
Use Formik onBlur() API for handleBlur() hook
VadimKovalenkoSNF Oct 27, 2025
4dcf89d
Add prop types
VadimKovalenkoSNF Oct 27, 2025
a2e4b32
Follow-up fix for proptypes
VadimKovalenkoSNF Oct 27, 2025
5ec4498
Move RELATIONSHIP_OPTIONS to constants.js
VadimKovalenkoSNF Oct 27, 2025
b9adbae
Merge branch 'main' into OSDEV-2201-claim-eligibility-step
VadimKovalenkoSNF Oct 27, 2025
49c0ef3
Fix path in ClaimForm test
VadimKovalenkoSNF Oct 27, 2025
dc5592b
Merge branch 'main' into OSDEV-2201-claim-eligibility-step
VadimKovalenkoSNF Oct 27, 2025
a5cbeaa
Pass error message to the select component
VadimKovalenkoSNF Oct 28, 2025
3555489
Bring back cooment for getButtonDisabledState hook
VadimKovalenkoSNF Oct 28, 2025
022e6c3
Fix unit test
VadimKovalenkoSNF Oct 28, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,4 @@ dumps

# Cursor
.cursor-rules
.cursor-test-conventions.md
3 changes: 3 additions & 0 deletions doc/release/RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
### What's new
* [OSDEV-2200](https://opensupplyhub.atlassian.net/browse/OSDEV-2200) - Implements a new claim introduction page for the new facility claiming process, accessible via `/claim/:osId`, which can be enabled or activated through a feature flag.
* [OSDEV-2203](https://opensupplyhub.atlassian.net/browse/OSDEV-2203) - Implemented new multi-step claim flow for production locations with routing skeleton and shared layout. Introduced claim form page (`/claim/:osID/details/`) featuring a four-step stepper (Eligibility Check, Contact Information, Business Details, Production Location Details). Added step-isolated form validation with optimistic button states that enable on initial render and disable after user interaction with errors. Integrated session-based URL access protection and data prefetching for countries, facility processing types, parent companies, and production location data using the existing Redux infrastructure for the claim form steps where this data should be prepopulated.
* [OSDEV-2201](https://opensupplyhub.atlassian.net/browse/OSDEV-2201) - Claim Flow: Eligibility Step
- Added a dedicated Eligibility step UI showing account email and organization name, plus a required relationship selector with the following options: owner, manager, parent company representative, worker, partner, and other.
- Implemented an ineligibility dialog for the "partner" and "other" selections that blocks progression but preserves any previously valid selection when the dialog is closed.

### Release instructions
* Ensure that the following commands are included in the `post_deployment` command:
Expand Down
317 changes: 317 additions & 0 deletions src/react/src/__tests__/components/EligibilityStep.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
import React from 'react';
import { fireEvent, screen } from '@testing-library/react';
import renderWithProviders from '../../util/testUtils/renderWithProviders';
import EligibilityStep from '../../components/InitialClaimFlow/ClaimForm/Steps/EligibilityStep/EligibilityStep';
import { mainRoute } from '../../util/constants';

const mockHistoryPush = jest.fn();

jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
return {
...actual,
useHistory: () => ({
push: mockHistoryPush,
}),
useLocation: () => ({
pathname: '/claim/test-os-id',
search: '',
hash: '',
state: null,
}),
};
});

jest.mock('../../components/Filters/StyledSelect', () => {
// eslint-disable-next-line global-require
const mockPropTypes = require('prop-types');
const MockStyledSelect = ({
options,
value,
onChange,
onBlur,
placeholder,
name,
}) => (
<select
data-testid="relationship-select"
name={name}
value={value ? value.value : ''}
onChange={e => {
const selectedOption = options.find(
opt => opt.value === e.target.value,
);
onChange(selectedOption);
}}
onBlur={onBlur}
>
<option value="">{placeholder}</option>
{options.map(opt => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
);

MockStyledSelect.propTypes = {
options: mockPropTypes.arrayOf(
mockPropTypes.shape({
value: mockPropTypes.string.isRequired,
label: mockPropTypes.string.isRequired,
}),
).isRequired,
value: mockPropTypes.oneOfType([
mockPropTypes.shape({
value: mockPropTypes.string,
label: mockPropTypes.string,
}),
mockPropTypes.oneOf([null]),
]),
onChange: mockPropTypes.func.isRequired,
onBlur: mockPropTypes.func,
placeholder: mockPropTypes.string,
name: mockPropTypes.string,
};

MockStyledSelect.defaultProps = {
value: null,
onBlur: () => {},
placeholder: '',
name: 'relationship',
};

return MockStyledSelect;
});

// jsdom does not implement scrollTo; stub to avoid errors in hooks.
beforeAll(() => {
// eslint-disable-next-line no-underscore-dangle
if (!window._scrollToStubbed) {
window.scrollTo = jest.fn();
// eslint-disable-next-line no-underscore-dangle
window._scrollToStubbed = true;
}
});

describe('EligibilityStep component', () => {
const mockHandleChange = jest.fn();
const mockOnNext = jest.fn();
const mockOnBack = jest.fn();

const defaultProps = {
formData: { relationship: null },
handleChange: mockHandleChange,
onNext: mockOnNext,
onBack: mockOnBack,
};

const preloadedState = {
auth: {
user: {
user: {
email: 'test@example.com',
name: 'Test Organization',
isAnon: false,
},
},
},
};

const renderComponent = (props = {}, state = preloadedState) =>
renderWithProviders(<EligibilityStep {...defaultProps} {...props} />, {
preloadedState: state,
});

afterEach(() => {
jest.clearAllMocks();
});

test('renders without crashing', () => {
const { container } = renderComponent();
expect(container).toBeInTheDocument();
});

test('renders component with user information', () => {
renderComponent();

expect(screen.getByText('Account email:')).toBeInTheDocument();
expect(screen.getByText('test@example.com')).toBeInTheDocument();
expect(screen.getByText('Organization name:')).toBeInTheDocument();
expect(screen.getByText('Test Organization')).toBeInTheDocument();
});

test('renders select field with placeholder', () => {
renderComponent();

expect(
screen.getByText(
'Select your relationship to this production location:'
)
).toBeInTheDocument();

const selectField = screen.getByTestId('relationship-select');
expect(selectField).toBeInTheDocument();
});

test('displays "Not available" when user email is not provided', () => {
const stateWithoutEmail = {
auth: {
user: {
user: {
email: null,
name: null,
isAnon: true,
},
},
},
};

renderComponent({}, stateWithoutEmail);

const notAvailableElements = screen.getAllByText('Not available');
expect(notAvailableElements).toHaveLength(2);
});

test('ineligibility dialog is not shown by default', () => {
renderComponent();

expect(
screen.queryByText('Not Eligible to File Claim')
).not.toBeInTheDocument();
});

test('shows ineligibility dialog when "partner" option is selected', () => {
renderComponent();

const selectField = screen.getByTestId('relationship-select');
fireEvent.change(selectField, { target: { value: 'partner' } });

expect(screen.getByText('Not Eligible to File Claim')).toBeInTheDocument();
expect(
screen.getByText(/You are not eligible to file a claim for this location/i)
).toBeInTheDocument();
});

test('shows ineligibility dialog when "other" option is selected', () => {
renderComponent();

const selectField = screen.getByTestId('relationship-select');
fireEvent.change(selectField, { target: { value: 'other' } });

expect(screen.getByText('Not Eligible to File Claim')).toBeInTheDocument();
expect(
screen.getByText(/You are not eligible to file a claim for this location/i)
).toBeInTheDocument();
});

test('ineligibility dialog contains correct message and buttons', () => {
renderComponent();

const selectField = screen.getByTestId('relationship-select');
fireEvent.change(selectField, { target: { value: 'partner' } });

expect(
screen.getByText(/Only the owner, manager, authorized employee/i)
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /Back to Open Supply Hub/i })
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /Return to Claims/i })
).toBeInTheDocument();
});

test('navigates to main page when "Back to Open Supply Hub" is clicked', () => {
renderComponent();

const selectField = screen.getByTestId('relationship-select');
fireEvent.change(selectField, { target: { value: 'partner' } });

const backButton = screen.getByRole('button', {
name: /Back to Open Supply Hub/i,
});
backButton.click();

expect(mockHistoryPush).toHaveBeenCalledTimes(1);
expect(mockHistoryPush).toHaveBeenCalledWith(mainRoute);
});

test('closes dialog without resetting selection when "Return to Claims" is clicked', () => {
renderComponent();

const selectField = screen.getByTestId('relationship-select');
fireEvent.change(selectField, { target: { value: 'other' } });

expect(screen.getByText('Not Eligible to File Claim')).toBeInTheDocument();

const returnButton = screen.getByRole('button', {
name: /Return to Claims/i,
});
fireEvent.click(returnButton);

// The dialog should close and NOT reset previously selected valid relationship
expect(mockHandleChange).not.toHaveBeenCalledWith('relationship', null);
// And no change should be emitted at all for ineligible selections.
expect(mockHandleChange).not.toHaveBeenCalled();
});

test('calls handleChange when eligible "owner" option is selected', () => {
renderComponent();

const selectField = screen.getByTestId('relationship-select');
fireEvent.change(selectField, { target: { value: 'owner' } });

expect(mockHandleChange).toHaveBeenCalledTimes(1);
expect(mockHandleChange).toHaveBeenCalledWith('relationship', {
value: 'owner',
label: 'I am the owner of this production location',
});
expect(
screen.queryByText('Not Eligible to File Claim')
).not.toBeInTheDocument();
});

test('calls handleChange when eligible "manager" option is selected', () => {
renderComponent();

const selectField = screen.getByTestId('relationship-select');
fireEvent.change(selectField, { target: { value: 'manager' } });

expect(mockHandleChange).toHaveBeenCalledTimes(1);
expect(mockHandleChange).toHaveBeenCalledWith('relationship', {
value: 'manager',
label: 'I am a manager working at this production location',
});
expect(
screen.queryByText('Not Eligible to File Claim')
).not.toBeInTheDocument();
});

test('calls handleChange when eligible "worker" option is selected', () => {
renderComponent();

const selectField = screen.getByTestId('relationship-select');
fireEvent.change(selectField, { target: { value: 'worker' } });

expect(mockHandleChange).toHaveBeenCalledTimes(1);
expect(
screen.queryByText('Not Eligible to File Claim')
).not.toBeInTheDocument();
});

test('calls handleChange when "parent_company_owner_or_manager" option is selected', () => {
renderComponent();

const selectField = screen.getByTestId('relationship-select');
fireEvent.change(selectField, {
target: { value: 'parent_company_owner_or_manager' },
});

expect(mockHandleChange).toHaveBeenCalledTimes(1);
expect(
screen.queryByText('Not Eligible to File Claim')
).not.toBeInTheDocument();
});
});

Loading