Skip to content
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

task/WG-389-Asset-Panel-Questionnaire #300

Merged
merged 8 commits into from
Jan 13, 2025
44 changes: 44 additions & 0 deletions react/src/__fixtures__/featuresFixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,47 @@ export const mockVideoFeature: Feature = {
},
],
};

export const mockQuestionnaireFeature: Feature = {
id: 2466139,
project_id: 94,
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [-122.306436952, 47.653046488],
},
properties: {
_hazmapper: {
questionnaire: {
assets: [
{
filename: 'Q11-Photo-001.jpg',
coordinates: [-122.30645555555556, 47.65306388888889],
},
{
filename: 'Q12-Photo-001.jpg',
coordinates: [-122.30648611111111, 47.65299722222222],
},
{
filename: 'Q13-Photo-001.jpg',
coordinates: [-122.3064611111111, 47.652975],
},
],
},
},
},
styles: {},
assets: [
{
id: 2037094,
path: '94/816c47f4-b34d-4a30-b0d8-e92b11ba100c',
uuid: '816c47f4-b34d-4a30-b0d8-e92b11ba100c',
asset_type: 'questionnaire',
original_path:
'project-3891343857756007955-242ac117-0001-012/RApp/asif27/Home/GNSS Base Station Setup (Copy 1) 1690582474191.rqa/GNSS Base Station Setup (Copy 1) 1690582474191.rq',
original_name: null,
display_path:
'project-3891343857756007955-242ac117-0001-012/RApp/asif27/Home/GNSS Base Station Setup (Copy 1) 1690582474191.rqa/GNSS Base Station Setup (Copy 1) 1690582474191.rq',
},
],
};
9 changes: 9 additions & 0 deletions react/src/components/AssetDetail/AssetDetail.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,12 @@ th {
height: 35em;
width: 100%;
}
.caption {
color: #1f5c7a;
font-size: 0.8em;
padding-bottom: 0.8em;
}
.questionnaireImage {
width: 100%;
height: 30em;
}
8 changes: 6 additions & 2 deletions react/src/components/AssetDetail/AssetDetail.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { render, screen, act } from '@testing-library/react';
import AssetDetail from './AssetDetail';
import { mockImgFeature } from '@hazmapper/__fixtures__/featuresFixture';
import AssetGeometry from './AssetGeometry';

jest.mock('@hazmapper/hooks', () => ({
useFeatureSelection: jest.fn(),
Expand All @@ -23,9 +24,12 @@ describe('AssetDetail', () => {
isPublicView: false,
};

it('renders all main components', () => {
it('renders all main components', async () => {
const { getByText } = render(<AssetDetail {...AssetModalProps} />);
const assetGeometry = screen.getByTestId('asset-geometry');
await act(async () => {
render(<AssetGeometry selectedFeature={mockImgFeature} />);
});
// Check for title, button, and tables
expect(getByText('Photo 4.jpg')).toBeDefined();
expect(getByText('Download')).toBeDefined();
Expand Down
87 changes: 52 additions & 35 deletions react/src/components/AssetDetail/AssetDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,44 +64,61 @@ const AssetDetail: React.FC<AssetModalProps> = ({
/>
</Suspense>
</div>
<div className={styles.bottomSection}>
<div className={styles.metadataTable}>
<table>
<thead>
<tr>
<th colSpan={2} className="text-center">
Metadata
</th>
</tr>
</thead>
<tbody>
{selectedFeature?.properties &&
Object.keys(selectedFeature.properties).length > 0 ? (
Object.entries(selectedFeature.properties)
.filter(([key]) => !key.startsWith('_hazmapper'))
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) // Alphabetizes metadata
.map(([propKey, propValue]) => (
<tr key={propKey}>
<td>{_.startCase(propKey)}</td>
<td>
{propKey.startsWith('description') ? (
<code>{propValue}</code>
) : (
_.trim(JSON.stringify(propValue), '"')
)}
</td>
</tr>
))
) : (
{featureType !== FeatureType.Questionnaire && (
<div className={styles.bottomSection}>
<div className={styles.metadataTable}>
<table>
<thead>
<tr>
<td colSpan={2}>There are no metadata properties.</td>
<th colSpan={2} className="text-center">
Metadata
</th>
</tr>
)}
</tbody>
</table>
<AssetGeometry selectedFeature={selectedFeature} />
</thead>
<tbody>
{selectedFeature?.properties &&
Object.keys(selectedFeature.properties).length > 0 ? (
(() => {
/* Function check that shows the "There are no metadata properties"
in any of these cases:
- The properties object is empty or null
- The properties object only contains keys that start with "_hazmapper"
- The properties object exists but has no properties*/
const filteredProperties = Object.entries(
selectedFeature.properties
)
.filter(([key]) => !key.startsWith('_hazmapper'))
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB));
return filteredProperties.length > 0 ? (
filteredProperties.map(([propKey, propValue]) => (
<tr key={propKey}>
<td>{_.startCase(propKey)}</td>
<td>
{propKey.startsWith('description') ? (
<code>{propValue}</code>
) : (
_.trim(JSON.stringify(propValue), '"')
)}
</td>
</tr>
))
) : (
<tr>
<td colSpan={2}>There are no metadata properties.</td>
</tr>
);
})()
) : (
<tr>
<td colSpan={2}>There are no metadata properties.</td>
</tr>
)}
</tbody>
</table>
<AssetGeometry selectedFeature={selectedFeature} />
</div>
</div>
</div>
)}
</div>
);
};
Expand Down
4 changes: 2 additions & 2 deletions react/src/components/AssetDetail/AssetGeometry.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { render } from '@testing-library/react';
import GeometryAsset from './AssetGeometry';
import AssetGeometry from './AssetGeometry';
import { mockImgFeature } from '@hazmapper/__fixtures__/featuresFixture';

jest.mock('@turf/turf', () => ({
Expand All @@ -20,7 +20,7 @@ describe('AssetGeometry', () => {
};

it('renders geometry for point on an image', () => {
const { getByText } = render(<GeometryAsset {...GeometryAssetProps} />);
const { getByText } = render(<AssetGeometry {...GeometryAssetProps} />);
expect(getByText('Geometry: Point')).toBeDefined();
expect(getByText('Latitude')).toBeDefined();
expect(getByText('Longitude')).toBeDefined();
Expand Down
5 changes: 3 additions & 2 deletions react/src/components/AssetDetail/AssetGeometry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import _ from 'lodash';
import * as turf from '@turf/turf';
import { Feature, FeatureType } from '@hazmapper/types';
import styles from './AssetDetail.module.css';

interface AssetGeometryProps {
selectedFeature: Feature;
Expand All @@ -18,7 +19,7 @@ const AssetGeometry: React.FC<AssetGeometryProps> = ({ selectedFeature }) => {
const geometryType = selectedFeature.geometry.type;

return (
<>
<div className={styles.metadataTable}>
{geometryType === FeatureType.Point && (
<table>
<thead>
Expand Down Expand Up @@ -111,7 +112,7 @@ const AssetGeometry: React.FC<AssetGeometryProps> = ({ selectedFeature }) => {
</tbody>
</table>
)}
</>
</div>
);
};

Expand Down
143 changes: 143 additions & 0 deletions react/src/components/AssetDetail/AssetQuestionnaire.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import React, { forwardRef } from 'react';
import { render, screen, fireEvent, act } from '@testing-library/react';
import { mockQuestionnaireFeature } from '@hazmapper/__fixtures__/featuresFixture';
import AssetQuestionnaire from './AssetQuestionnaire';

// Create a mock ref implementation for Carousel
const mockCarouselRef = {
next: jest.fn(),
prev: jest.fn(),
};

// Mock the antd components used in AssetQuestionnaire
jest.mock('antd', () => ({
Carousel: forwardRef(function Carousel({ children }: any, ref: any) {
React.useImperativeHandle(ref, () => mockCarouselRef);
return <div data-testid="mock-carousel">{children}</div>;
}),
Space: ({ children }: any) => <div data-testid="mock-space">{children}</div>,
Flex: ({ children }: any) => <div data-testid="mock-flex">{children}</div>,
}));

jest.mock('@tacc/core-components', () => ({
Button: ({ children, onClick, iconNameBefore }: any) => (
<button onClick={onClick} data-testid={`button-${iconNameBefore}`}>
{iconNameBefore}
{children}
</button>
),
}));

describe('AssetQuestionnaire', () => {
const mockFeatureSource = 'https://example.com/api/assets/123';

beforeEach(() => {
// Clear mock function calls before each test
mockCarouselRef.next.mockClear();
mockCarouselRef.prev.mockClear();
});

const setup = () => {
return render(
<AssetQuestionnaire
feature={mockQuestionnaireFeature}
featureSource={mockFeatureSource}
/>
);
};

it('renders all images from the questionnaire assets', async () => {
await act(async () => {
setup();
});

const images = screen.getAllByRole('img');
expect(images).toHaveLength(
mockQuestionnaireFeature?.properties?._hazmapper.questionnaire.assets
.length
);

mockQuestionnaireFeature?.properties?._hazmapper.questionnaire.assets.forEach(
(asset, index) => {
const image = images[index];
const expectedPreviewPath =
`${mockFeatureSource}/${asset.filename}`.replace(
/\.([^.]+)$/,
'.preview.$1'
);

expect(image.getAttribute('src')).toBe(expectedPreviewPath);
expect(image.getAttribute('alt')).toBe(asset.filename);
}
);
});

it('renders navigation buttons', async () => {
await act(async () => {
setup();
});

const prevButton = screen.getByTestId('button-push-left');
const nextButton = screen.getByTestId('button-push-right');

expect(prevButton).toBeTruthy();
expect(nextButton).toBeTruthy();
});

it('displays image filenames as captions', async () => {
await act(async () => {
setup();
});

mockQuestionnaireFeature?.properties?._hazmapper.questionnaire.assets.forEach(
(asset) => {
const caption = screen.getByText(asset.filename);
expect(caption).toBeTruthy();
}
);
});

it('handles carousel navigation', async () => {
await act(async () => {
setup();
});

const prevButton = screen.getByTestId('button-push-left');
const nextButton = screen.getByTestId('button-push-right');

await act(async () => {
fireEvent.click(nextButton);
});
expect(mockCarouselRef.next).toHaveBeenCalled();

await act(async () => {
fireEvent.click(prevButton);
});
expect(mockCarouselRef.prev).toHaveBeenCalled();
});

it('renders "No images available" when there are no assets', async () => {
const emptyFeature = {
...mockQuestionnaireFeature,
properties: {
_hazmapper: {
questionnaire: {
assets: [],
},
},
},
};

await act(async () => {
render(
<AssetQuestionnaire
feature={emptyFeature}
featureSource={mockFeatureSource}
/>
);
});

const noImagesText = screen.getByText('No images available');
expect(noImagesText).toBeTruthy();
});
});
Loading
Loading