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
5 changes: 5 additions & 0 deletions doc/release/RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
* Sidebar "Jump to" navigation links to individual partner groups; clicking a link opens the corresponding section and smoothly scrolls it into view.
* Added `UrlProperty` format component and `url` format type support for partner field JSON schemas, enabling clickable links with customizable link text.
* Includes loading state with a spinner while partner field groups are being fetched.
* [OSDEV-2372](https://opensupplyhub.atlassian.net/browse/OSDEV-2372) - Implemented the Operational Details section on the Production Location page:
* Added `ClaimDataContainer` component that displays operational details submitted by management through the claim process for claimed production locations.
* The section includes a "Claimed Profile" badge, informational tooltip with a "Learn More" link, and renders claim data fields (e.g., facility description, parent company, website, contact information) as data points with contributor metadata and timestamps.
* Each data point shows the claim status, contributor name, and claim approval/creation date, maintaining consistency with other sections on the page.
* The section only appears when the production location has a non-pending claim (i.e., `claim_info` is present and its status is not `PENDING`) and contains displayable claim data.

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

const makeClaimInfo = (overrides = {}) => ({
facility: {
website: 'https://example.com',
phone_number: '+1 234 567 8900',
minimum_order: '100 units',
average_lead_time: '30 days',
female_workers_percentage: 60,
affiliations: ['Fair Trade'],
certifications: ['ISO 9001'],
opening_date: '2010',
closing_date: null,
estimated_annual_throughput: 20000,
actual_annual_energy_consumption: null,
description: 'A sample facility.',
},
contact: {
name: 'Jane Doe',
email: 'jane@example.com',
},
office: {
name: 'Head Office',
address: '1 Office St',
country: 'US',
phone_number: '+1 800 000 0000',
},
contributor: { name: 'Test Contributor' },
approved_at: '2023-05-15T00:00:00Z',
created_at: '2023-01-01T00:00:00Z',
...overrides,
});

const renderComponent = (props = {}) => {
const claimInfo =
'claimInfo' in props ? props.claimInfo : makeClaimInfo();
return renderWithProviders(
<ClaimDataContainer
isClaimed={props.isClaimed ?? true}
claimInfo={claimInfo}
className={props.className}
/>,
);
};

describe('ClaimDataContainer — empty state', () => {
it('renders nothing when isClaimed is false', () => {
const { container } = renderComponent({ isClaimed: false });
expect(container.firstChild).toBeNull();
});

it('renders nothing when claimInfo is null', () => {
const { container } = renderComponent({
isClaimed: true,
claimInfo: null,
});
expect(container.firstChild).toBeNull();
});

it('renders nothing when all facility fields are empty', () => {
const { container } = renderComponent({
claimInfo: makeClaimInfo({
facility: {},
contact: null,
office: null,
}),
});
expect(container.firstChild).toBeNull();
});
});

describe('ClaimDataContainer — section header', () => {
it('renders the section title', () => {
const { getByText } = renderComponent();
expect(
getByText('Operational Details Submitted by Management'),
).toBeInTheDocument();
});

it('renders the info tooltip trigger', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('claim-data-info-tooltip')).toBeInTheDocument();
});

it('renders the toggle switch', () => {
const { getByRole } = renderComponent();
expect(
getByRole('checkbox', {
name: /show operational details submitted by management/i,
}),
).toBeInTheDocument();
});

it('shows "Close" label when content is open by default', () => {
const { getByText } = renderComponent();
expect(getByText('Close')).toBeInTheDocument();
});

it('sets the operational-details id on the root element', () => {
const { container } = renderComponent();
expect(container.querySelector('#operational-details')).toBeInTheDocument();
});
});

describe('ClaimDataContainer — toggle switch', () => {
it('content is visible by default', () => {
const { getByText } = renderComponent();
expect(getByText('A sample facility.')).toBeInTheDocument();
});

it('hides content when toggled closed', () => {
const { getByRole, queryByText } = renderComponent();
const toggle = getByRole('checkbox', {
name: /show operational details submitted by management/i,
});
fireEvent.click(toggle);
expect(queryByText('A sample facility.')).not.toBeInTheDocument();
});

it('shows "Open" label when content is closed', () => {
const { getByRole, getByText } = renderComponent();
const toggle = getByRole('checkbox', {
name: /show operational details submitted by management/i,
});
fireEvent.click(toggle);
expect(getByText('Open')).toBeInTheDocument();
});

it('shows content again when toggled back open', () => {
const { getByRole, getByText } = renderComponent();
const toggle = getByRole('checkbox', {
name: /show operational details submitted by management/i,
});
fireEvent.click(toggle);
fireEvent.click(toggle);
expect(getByText('A sample facility.')).toBeInTheDocument();
});
});

describe('ClaimDataContainer — field labels and values', () => {
it('renders all expected field labels', () => {
const { getByText } = renderComponent();

const expectedLabels = [
'Website',
'Contact Person',
'Contact Email',
'Phone Number',
'Minimum Order',
'Average Lead Time',
'Affiliations',
'Certifications/Standards/Regulations',
'Opening Date',
'Estimated Annual Throughput',
'Office Name',
'Office Address',
'Office Phone Number',
'Description',
];

expectedLabels.forEach(label => {
expect(getByText(label, { exact: true })).toBeInTheDocument();
});
});

it('renders plain field values', () => {
const claimInfo = makeClaimInfo();
const { getByText } = renderComponent({ claimInfo });

expect(
getByText(claimInfo.facility.phone_number, { exact: true }),
).toBeInTheDocument();
expect(
getByText(claimInfo.facility.minimum_order, { exact: true }),
).toBeInTheDocument();
expect(
getByText(claimInfo.facility.average_lead_time, { exact: true }),
).toBeInTheDocument();
expect(
getByText(claimInfo.facility.description, { exact: true }),
).toBeInTheDocument();
expect(
getByText('20000 kg/year', { exact: true }),
).toBeInTheDocument();
});

it('renders contact fields when contact is provided', () => {
const { getByText } = renderComponent();
expect(getByText('Jane Doe', { exact: true })).toBeInTheDocument();
expect(
getByText('jane@example.com', { exact: true }),
).toBeInTheDocument();
});

it('omits contact fields when contact is null', () => {
const { queryByText } = renderComponent({
claimInfo: makeClaimInfo({ contact: null }),
});
expect(queryByText('Contact Person')).not.toBeInTheDocument();
expect(queryByText('Contact Email')).not.toBeInTheDocument();
});

it('renders office fields when office is provided', () => {
const { getByText } = renderComponent();
expect(getByText('Head Office', { exact: true })).toBeInTheDocument();
});

it('omits office fields when office is null', () => {
const { queryByText } = renderComponent({
claimInfo: makeClaimInfo({ office: null }),
});
expect(queryByText('Office Name')).not.toBeInTheDocument();
expect(queryByText('Office Address')).not.toBeInTheDocument();
expect(queryByText('Office Phone Number')).not.toBeInTheDocument();
});

it('renders female_workers_percentage field when value is 0', () => {
const { getByText } = renderComponent({
claimInfo: makeClaimInfo({
facility: { female_workers_percentage: 0 },
contact: null,
office: null,
}),
});
expect(
getByText('Percentage of female workers', { exact: true }),
).toBeInTheDocument();
});
});

describe('ClaimDataContainer — field ordering', () => {
it('renders fields in the order defined by FIELD_ORDER', () => {
const { getAllByTestId } = renderComponent();
const labels = getAllByTestId('data-point-label').map(
el => el.textContent,
);

const websiteIndex = labels.indexOf('Website');
const phoneIndex = labels.indexOf('Phone Number');
const officeNameIndex = labels.indexOf('Office Name');
const descriptionIndex = labels.indexOf('Description');
const certificationsIndex = labels.indexOf(
'Certifications/Standards/Regulations',
);
const affiliationsIndex = labels.indexOf('Affiliations');
const minimumOrderIndex = labels.indexOf('Minimum Order');

expect(websiteIndex).toBeLessThan(phoneIndex);
expect(phoneIndex).toBeLessThan(officeNameIndex);
expect(descriptionIndex).toBeLessThan(certificationsIndex);
expect(certificationsIndex).toBeLessThan(affiliationsIndex);
expect(affiliationsIndex).toBeLessThan(minimumOrderIndex);
});
});

describe('ClaimDataContainer — Claimed status chips', () => {
it('renders a Claimed chip for each displayed field', () => {
const { getAllByTestId, getAllByText } = renderComponent();
const dataPoints = getAllByTestId('data-point');
const claimedChips = getAllByText(STATUS_CLAIMED);
expect(claimedChips.length).toBe(dataPoints.length);
});
});

describe('ClaimDataContainer — contributor attribution', () => {
it('resolves contributor name from contributor.name object', () => {
const { getAllByTestId } = renderComponent({
claimInfo: makeClaimInfo({
contributor: { name: 'Acme Corp' },
}),
});
getAllByTestId('data-point-contributor').forEach(el => {
expect(el).toHaveTextContent('Acme Corp');
});
});

it('resolves contributor name from a plain string', () => {
const { getAllByTestId } = renderComponent({
claimInfo: makeClaimInfo({ contributor: 'String Contributor' }),
});
getAllByTestId('data-point-contributor').forEach(el => {
expect(el).toHaveTextContent('String Contributor');
});
});

it('prefers approved_at over created_at for the date', () => {
const { getAllByTestId } = renderComponent({
claimInfo: makeClaimInfo({
approved_at: '2023-05-15T00:00:00Z',
created_at: '2023-01-01T00:00:00Z',
}),
});
getAllByTestId('data-point-date').forEach(el => {
expect(el).toHaveTextContent('May 15, 2023');
});
});

it('falls back to created_at when approved_at is absent', () => {
const { getAllByTestId } = renderComponent({
claimInfo: makeClaimInfo({
approved_at: undefined,
created_at: '2022-11-09T00:00:00Z',
}),
});
getAllByTestId('data-point-date').forEach(el => {
expect(el).toHaveTextContent('November 9, 2022');
});
});
});
7 changes: 4 additions & 3 deletions src/react/src/components/BadgeClaimed.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ import SvgIcon from '@material-ui/core/SvgIcon';

export default function BadgeClaimed({
color,
fontSize = '24px',
viewBox = '0 0 16 20',
overflow = 'hidden',
className,
}) {
return (
<SvgIcon viewBox={viewBox} style={{ fontSize, overflow }}>
<SvgIcon viewBox={viewBox} fontSize="inherit" className={className}>
<path
fill={color}
d="M6.95 13.55L12.6 7.9L11.175 6.475L6.95 10.7L4.85 8.6L3.425 10.025L6.95 13.55ZM8 20C5.68333 19.4167 3.771 18.0873 2.263 16.012C0.754333 13.9373 0 11.6333 0 9.1V3L8 0L16 3V9.1C16 11.6333 15.246 13.9373 13.738 16.012C12.2293 18.0873 10.3167 19.4167 8 20ZM8 17.9C9.73333 17.35 11.1667 16.25 12.3 14.6C13.4333 12.95 14 11.1167 14 9.1V4.375L8 2.125L2 4.375V9.1C2 11.1167 2.56667 12.95 3.7 14.6C4.83333 16.25 6.26667 17.35 8 17.9Z"
Expand All @@ -20,8 +19,10 @@ export default function BadgeClaimed({

BadgeClaimed.defaultProps = {
color: 'currentColor',
className: undefined,
};

BadgeClaimed.propTypes = {
color: string,
className: string,
};
Loading
Loading