Skip to content

Commit

Permalink
task/WG-389-Asset-Panel-Questionnaire (#300)
Browse files Browse the repository at this point in the history
* task/WG-389-Asset-Panel-Questionnaire
- Initial commit, no tests

* Questionnaire and other Asset test changes
- addresses errors on geometry and asset AssetDetail
- adds questionnaire fixture
- adds questionnaire tests

* - Linting and asset style fixes
- Adds padding to questionnaire image caption
- Adds better scrolling behavior to geometry table

* - React linting fix

* Removes metadata tables for questionnaires based on RAPP feedback

* - Stops restricting carousel height in Questionnaire

* - Reverts style change from last commit
  • Loading branch information
sophia-massie authored Jan 13, 2025
1 parent 1537879 commit a06303c
Show file tree
Hide file tree
Showing 10 changed files with 354 additions and 42 deletions.
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

0 comments on commit a06303c

Please sign in to comment.