Skip to content
Draft
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
@@ -0,0 +1,73 @@
import React from 'react';
import SvgIcon from '@mui/material/SvgIcon';
import { render, screen } from 'test-utils/functions';
import AnalysisAndVisualizations from './AnalysisAndVisualizations';
import { CLOUD_WORKSPACES_SLIDE, BIOMARKERS_SLIDE, VISUALIZE_DATA_SLIDE } from './config';

// Mock SVG-based icons that fail in test environment due to SVG file mocking
jest.mock('js/shared-styles/icons', () => {
const actual = jest.requireActual('js/shared-styles/icons/icons');

Check failure on line 9 in context/app/static/js/components/home/AnalysisAndVisualizations/AnalysisAndVisualizations.spec.tsx

View workflow job for this annotation

GitHub Actions / build

Unsafe assignment of an `any` value
return {
...Object.fromEntries(Object.keys(actual).map((key) => [key, SvgIcon])),

Check failure on line 11 in context/app/static/js/components/home/AnalysisAndVisualizations/AnalysisAndVisualizations.spec.tsx

View workflow job for this annotation

GitHub Actions / build

Unsafe argument of type `any` assigned to a parameter of type `{}`
};
});

// Mock useMediaQuery to default to desktop
jest.mock('@mui/material/useMediaQuery', () => ({
__esModule: true,
default: () => true,
}));

describe('AnalysisAndVisualizations', () => {
test('renders the section header', () => {
render(<AnalysisAndVisualizations />);
expect(screen.getByText('Analysis and Visualizations')).toBeInTheDocument();
});

test('renders the introductory description', () => {
render(<AnalysisAndVisualizations />);
expect(

Check failure on line 29 in context/app/static/js/components/home/AnalysisAndVisualizations/AnalysisAndVisualizations.spec.tsx

View workflow job for this annotation

GitHub Actions / build

Replace `⏎······screen.getByText(/See·how·researchers·use·HuBMAP's·data·and·tools/),⏎····` with `screen.getByText(/See·how·researchers·use·HuBMAP's·data·and·tools/)`
screen.getByText(/See how researchers use HuBMAP's data and tools/),
).toBeInTheDocument();
});

test('renders the Cloud Workspaces slide', () => {
render(<AnalysisAndVisualizations />);
expect(screen.getByText(CLOUD_WORKSPACES_SLIDE.title)).toBeInTheDocument();
});

test('renders the Biomarkers slide', () => {
render(<AnalysisAndVisualizations />);
expect(screen.getByText(BIOMARKERS_SLIDE.title)).toBeInTheDocument();
});

test('renders the Visualize Data slide', () => {
render(<AnalysisAndVisualizations />);
expect(screen.getByText(VISUALIZE_DATA_SLIDE.sectionTitle)).toBeInTheDocument();
});

test('renders all three slides as regions', () => {
render(<AnalysisAndVisualizations />);
const regions = screen.getAllByRole('region');
// Main section + 3 slide regions
expect(regions.length).toBeGreaterThanOrEqual(3);
});

test('Cloud Workspaces slide has Sign Up and Launch Workspaces buttons', () => {
render(<AnalysisAndVisualizations />);
expect(screen.getByRole('link', { name: 'Sign Up' })).toHaveAttribute('href', '/register');
expect(screen.getByRole('link', { name: 'Launch Workspaces' })).toHaveAttribute('href', '/workspaces');
});

test('Biomarkers slide has Launch Advanced Search button', () => {
render(<AnalysisAndVisualizations />);
expect(screen.getByRole('link', { name: 'Launch Advanced Search' })).toHaveAttribute('href', '/cells');
});

test('Visualize Data slide has all three view CTAs', () => {
render(<AnalysisAndVisualizations />);
expect(screen.getByRole('link', { name: 'View Visualizations' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'Visualize Cell Populations' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'Explore Data' })).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Container from '@mui/material/Container';
import { QueryStatsRounded } from '@mui/icons-material';

import { SectionHeader } from 'js/pages/Home/style';
import ParallaxSlide from './ParallaxSlide';
import VisualizeDataSlide from './VisualizeDataSlide';
import { CLOUD_WORKSPACES_SLIDE, BIOMARKERS_SLIDE, VISUALIZE_DATA_SLIDE } from './config';

function AnalysisAndVisualizations() {
return (
<Box component="section" aria-label="Analysis and Visualizations">
<Container maxWidth="lg" sx={{ mb: 2 }}>
<SectionHeader variant="h2" component="h3" icon={QueryStatsRounded}>
Analysis and Visualizations
</SectionHeader>
<Typography variant="body1" color="text.secondary">
See how researchers use HuBMAP&apos;s data and tools to map human organs, cell types, and biomarkers.
</Typography>
</Container>

{/* Parallax scroll container - tall enough for all 3 slides to scroll through */}
<Box>
<ParallaxSlide config={CLOUD_WORKSPACES_SLIDE} zIndex={1} />
<ParallaxSlide config={BIOMARKERS_SLIDE} zIndex={2} />
<VisualizeDataSlide config={VISUALIZE_DATA_SLIDE} zIndex={3} />
</Box>
</Box>
);
}

export default AnalysisAndVisualizations;
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import { render, screen } from 'test-utils/functions';
import ParallaxImage from './ParallaxImage';

describe('ParallaxImage', () => {
test('renders image with correct alt text', () => {
render(<ParallaxImage src="https://example.com/test.jpg" alt="Test image" progress={0.5} isReducedMotion={false} />);

Check failure on line 7 in context/app/static/js/components/home/AnalysisAndVisualizations/ParallaxImage/ParallaxImage.spec.tsx

View workflow job for this annotation

GitHub Actions / build

Replace `<ParallaxImage·src="https://example.com/test.jpg"·alt="Test·image"·progress={0.5}·isReducedMotion={false}·/>` with `⏎······<ParallaxImage·src="https://example.com/test.jpg"·alt="Test·image"·progress={0.5}·isReducedMotion={false}·/>,⏎····`
expect(screen.getByAltText('Test image')).toBeInTheDocument();
});

test('renders image with correct src', () => {
render(<ParallaxImage src="https://example.com/test.jpg" alt="Test image" progress={0.5} isReducedMotion={false} />);

Check failure on line 12 in context/app/static/js/components/home/AnalysisAndVisualizations/ParallaxImage/ParallaxImage.spec.tsx

View workflow job for this annotation

GitHub Actions / build

Replace `<ParallaxImage·src="https://example.com/test.jpg"·alt="Test·image"·progress={0.5}·isReducedMotion={false}·/>` with `⏎······<ParallaxImage·src="https://example.com/test.jpg"·alt="Test·image"·progress={0.5}·isReducedMotion={false}·/>,⏎····`
const img = screen.getByAltText('Test image');
expect(img).toHaveAttribute('src', 'https://example.com/test.jpg');
});

test('applies lazy loading', () => {
render(<ParallaxImage src="https://example.com/test.jpg" alt="Test image" progress={0.5} isReducedMotion={false} />);

Check failure on line 18 in context/app/static/js/components/home/AnalysisAndVisualizations/ParallaxImage/ParallaxImage.spec.tsx

View workflow job for this annotation

GitHub Actions / build

Replace `<ParallaxImage·src="https://example.com/test.jpg"·alt="Test·image"·progress={0.5}·isReducedMotion={false}·/>` with `⏎······<ParallaxImage·src="https://example.com/test.jpg"·alt="Test·image"·progress={0.5}·isReducedMotion={false}·/>,⏎····`
const img = screen.getByAltText('Test image');
expect(img).toHaveAttribute('loading', 'lazy');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import { useSpring } from '@react-spring/web';

import { SlideImage } from '../types';
import { ImageContainer, AnimatedImage } from './styles';

interface ParallaxImageProps extends SlideImage {
/** Scroll progress value from 0 to 1 */
progress: number;
/** Whether user prefers reduced motion */
isReducedMotion: boolean;
}

function ParallaxImage({ src, alt, delay = 0, progress, isReducedMotion }: ParallaxImageProps) {
// Adjust progress to account for stagger delay
const adjustedProgress = Math.max(0, Math.min(1, (progress - delay) / (1 - delay)));

const springProps = useSpring({
opacity: isReducedMotion ? 1 : adjustedProgress,
transform: isReducedMotion ? 'translateY(0px)' : `translateY(${(1 - adjustedProgress) * 40}px)`,
config: { tension: 280, friction: 60 },
});

return (
<ImageContainer>
<AnimatedImage src={src} alt={alt} loading="lazy" style={springProps} />
</ImageContainer>
);
}

export default React.memo(ParallaxImage);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './ParallaxImage';
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { styled } from '@mui/material/styles';
import { animated } from '@react-spring/web';

export const ImageContainer = styled('div')({
position: 'relative',
width: '100%',
});

export const AnimatedImage = styled(animated.img)(({ theme }) => ({
width: '100%',
height: 'auto',
display: 'block',
borderRadius: theme.shape.borderRadius * 2,
boxShadow: theme.shadows[4],
willChange: 'opacity, transform',
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from 'react';
import SvgIcon from '@mui/material/SvgIcon';
import { render, screen } from 'test-utils/functions';
import ParallaxSlide from './ParallaxSlide';
import { CLOUD_WORKSPACES_SLIDE, BIOMARKERS_SLIDE } from '../config';

// Mock SVG-based icons that fail in test environment due to SVG file mocking
jest.mock('js/shared-styles/icons', () => {
const actual = jest.requireActual('js/shared-styles/icons/icons');

Check failure on line 9 in context/app/static/js/components/home/AnalysisAndVisualizations/ParallaxSlide/ParallaxSlide.spec.tsx

View workflow job for this annotation

GitHub Actions / build

Unsafe assignment of an `any` value
return {
...Object.fromEntries(Object.keys(actual).map((key) => [key, SvgIcon])),

Check failure on line 11 in context/app/static/js/components/home/AnalysisAndVisualizations/ParallaxSlide/ParallaxSlide.spec.tsx

View workflow job for this annotation

GitHub Actions / build

Unsafe argument of type `any` assigned to a parameter of type `{}`
};
});

describe('ParallaxSlide', () => {
test('renders slide title', () => {
render(<ParallaxSlide config={CLOUD_WORKSPACES_SLIDE} zIndex={3} />);
expect(screen.getByText(CLOUD_WORKSPACES_SLIDE.title)).toBeInTheDocument();
});

test('renders slide description', () => {
render(<ParallaxSlide config={CLOUD_WORKSPACES_SLIDE} zIndex={3} />);
expect(screen.getByText(CLOUD_WORKSPACES_SLIDE.description)).toBeInTheDocument();
});

test('renders bullet points', () => {
render(<ParallaxSlide config={CLOUD_WORKSPACES_SLIDE} zIndex={3} />);
CLOUD_WORKSPACES_SLIDE.bulletPoints!.forEach((point) => {
expect(screen.getByText(point)).toBeInTheDocument();
});
});

test('renders all CTA buttons with correct hrefs', () => {
render(<ParallaxSlide config={CLOUD_WORKSPACES_SLIDE} zIndex={3} />);
CLOUD_WORKSPACES_SLIDE.ctaButtons.forEach((button) => {
const link = screen.getByRole('link', { name: button.label });
expect(link).toHaveAttribute('href', button.href);
});
});

test('renders contained and outlined button variants', () => {
render(<ParallaxSlide config={CLOUD_WORKSPACES_SLIDE} zIndex={3} />);
const signUpButton = screen.getByRole('link', { name: 'Sign Up' });
const launchButton = screen.getByRole('link', { name: 'Launch Workspaces' });
expect(signUpButton.className).toContain('contained');

Check failure on line 45 in context/app/static/js/components/home/AnalysisAndVisualizations/ParallaxSlide/ParallaxSlide.spec.tsx

View workflow job for this annotation

GitHub Actions / build

Prefer .toHaveClass() over checking element className
expect(launchButton.className).toContain('outlined');

Check failure on line 46 in context/app/static/js/components/home/AnalysisAndVisualizations/ParallaxSlide/ParallaxSlide.spec.tsx

View workflow job for this annotation

GitHub Actions / build

Prefer .toHaveClass() over checking element className
});

test('renders images with correct alt text', () => {
render(<ParallaxSlide config={CLOUD_WORKSPACES_SLIDE} zIndex={3} />);
CLOUD_WORKSPACES_SLIDE.images.forEach((image) => {
expect(screen.getByAltText(image.alt)).toBeInTheDocument();
});
});

test('has correct aria-label for the region', () => {
render(<ParallaxSlide config={CLOUD_WORKSPACES_SLIDE} zIndex={3} />);
expect(screen.getByRole('region', { name: CLOUD_WORKSPACES_SLIDE.title })).toBeInTheDocument();
});

test('renders biomarkers slide with different layout', () => {
render(<ParallaxSlide config={BIOMARKERS_SLIDE} zIndex={2} />);
expect(screen.getByText(BIOMARKERS_SLIDE.title)).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'Launch Advanced Search' })).toHaveAttribute('href', '/cells');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React, { useRef } from 'react';
import Typography from '@mui/material/Typography';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';

import { trackEvent } from 'js/helpers/trackers';
import { useScrollProgress, usePrefersReducedMotion } from '../hooks';
import { SlideConfig } from '../types';
import ParallaxImage from '../ParallaxImage';
import { ScrollRunway, StickySlideContent, GradientBackground, SlideGrid, TextContent, ImageGroup } from './styles';

interface ParallaxSlideProps {
config: SlideConfig;
zIndex: number;
}

function ParallaxSlide({ config, zIndex }: ParallaxSlideProps) {
const runwayRef = useRef<HTMLDivElement>(null);
const progress = useScrollProgress(runwayRef);
const isReducedMotion = usePrefersReducedMotion();

const { theme, icon: Icon, title, description, bulletPoints, ctaButtons, images, layout } = config;

return (
<ScrollRunway ref={runwayRef} $zIndex={zIndex}>
<StickySlideContent role="region" aria-label={title}>
<GradientBackground $theme={theme} />
<SlideGrid $layout={layout}>
<TextContent>
<Icon color={theme} sx={{ fontSize: '2.5rem', mb: 1 }} />
<Typography variant="h4" component="h3" gutterBottom fontWeight={400}>
{title}
</Typography>
<Typography variant="body1" color="text.secondary" paragraph>
{description}
</Typography>
{bulletPoints && (
<Stack component="ul" spacing={0.5} sx={{ pl: 2.5, mb: 2, listStyleType: 'disc' }}>
{bulletPoints.map((point) => (
<Typography component="li" variant="body2" key={point} sx={{ display: 'list-item' }}>
{point}
</Typography>
))}
</Stack>
)}
<Stack direction="row" spacing={2} sx={{ mt: 1 }}>
{ctaButtons.map((button) => (
<Button
key={button.label}
variant={button.variant}
color={theme}
href={button.href}
onClick={() =>
trackEvent({
category: 'Homepage',
action: `Analysis and Visualizations / ${config.id}`,
label: button.trackingLabel,
})
}
>
{button.label}
</Button>
))}
</Stack>
</TextContent>
<ImageGroup $layout={layout}>
{images.map((image) => (
<ParallaxImage key={image.alt} {...image} progress={progress} isReducedMotion={isReducedMotion} />
))}
</ImageGroup>
</SlideGrid>
</StickySlideContent>
</ScrollRunway>
);
}

export default ParallaxSlide;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './ParallaxSlide';
Loading