+ );
+};
+
+export const formatCaptionContent = (stepNumber: number, totalSteps?: number) =>
+ i18n.translate('xpack.synthetics.monitor.stepOfSteps', {
+ defaultMessage: 'Step: {stepNumber} of {totalSteps}',
+ values: {
+ stepNumber,
+ totalSteps,
+ },
+ });
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_step_screenshot_with_label.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_step_screenshot_with_label.tsx
new file mode 100644
index 0000000000000..12dcd4db95f00
--- /dev/null
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/journey_step_screenshot_with_label.tsx
@@ -0,0 +1,50 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { CSSProperties } from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from '@elastic/eui';
+import { JourneyStep } from '../../../../../../common/runtime_types';
+import { JourneyStepScreenshotContainer } from './journey_step_screenshot_container';
+import { getTextColorForMonitorStatus, parseBadgeStatus } from './status_badge';
+
+interface Props {
+ step: JourneyStep;
+ stepLabels?: string[];
+ allStepsLoaded?: boolean;
+ compactView?: boolean;
+}
+
+export const JourneyStepScreenshotWithLabel = ({
+ step,
+ stepLabels = [],
+ compactView,
+ allStepsLoaded,
+}: Props) => {
+ const { euiTheme } = useEuiTheme();
+ const status = parseBadgeStatus(step.synthetics.step?.status ?? '');
+ const textColor = euiTheme.colors[getTextColorForMonitorStatus(status)] as CSSProperties['color'];
+
+ return (
+
+
+
+
+
+
+ {step.synthetics?.step?.name}
+
+
+
+ );
+};
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/screenshot_overlay_footer.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/screenshot_overlay_footer.test.tsx
new file mode 100644
index 0000000000000..f38084971114b
--- /dev/null
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/screenshot_overlay_footer.test.tsx
@@ -0,0 +1,103 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { fireEvent, waitFor } from '@testing-library/react';
+import React from 'react';
+import { render } from '../../../utils/testing';
+import { ScreenshotOverlayFooter, ScreenshotOverlayFooterProps } from './screenshot_overlay_footer';
+import { getShortTimeStamp } from '../../../utils/monitor_test_result/timestamp';
+import moment from 'moment';
+import { mockRef } from '../../../utils/testing/__mocks__/screenshot_ref.mock';
+
+describe('ScreenshotOverlayFooter', () => {
+ let defaultProps: ScreenshotOverlayFooterProps;
+
+ beforeEach(() => {
+ defaultProps = {
+ captionContent: 'test caption content',
+ imgSrc: 'http://sample.com/sampleImageSrc.png',
+ maxSteps: 3,
+ setStepNumber: jest.fn(),
+ stepNumber: 2,
+ label: getShortTimeStamp(moment('2020-11-26T15:28:56.896Z')),
+ onVisible: jest.fn(),
+ isLoading: false,
+ };
+ });
+
+ it('labels prev and next buttons', () => {
+ const { getByLabelText } = render();
+
+ expect(getByLabelText('Previous step'));
+ expect(getByLabelText('Next step'));
+ });
+
+ it('increments step number on next click', async () => {
+ const { getByLabelText } = render();
+
+ const nextButton = getByLabelText('Next step');
+
+ fireEvent.click(nextButton);
+
+ await waitFor(() => {
+ expect(defaultProps.setStepNumber).toHaveBeenCalledTimes(1);
+ expect(defaultProps.setStepNumber).toHaveBeenCalledWith(3);
+ });
+ });
+
+ it('decrements step number on prev click', async () => {
+ const { getByLabelText } = render();
+
+ const nextButton = getByLabelText('Previous step');
+
+ fireEvent.click(nextButton);
+
+ await waitFor(() => {
+ expect(defaultProps.setStepNumber).toHaveBeenCalledTimes(1);
+ expect(defaultProps.setStepNumber).toHaveBeenCalledWith(1);
+ });
+ });
+
+ it('disables `next` button on final step', () => {
+ defaultProps.stepNumber = 3;
+
+ const { getByLabelText } = render();
+
+ // getByLabelText('Next step');
+ expect(getByLabelText('Next step')).toHaveAttribute('disabled');
+ expect(getByLabelText('Previous step')).not.toHaveAttribute('disabled');
+ });
+
+ it('disables `prev` button on final step', () => {
+ defaultProps.stepNumber = 1;
+
+ const { getByLabelText } = render();
+
+ expect(getByLabelText('Next step')).not.toHaveAttribute('disabled');
+ expect(getByLabelText('Previous step')).toHaveAttribute('disabled');
+ });
+
+ it('renders a timestamp', () => {
+ const { getByText } = render();
+
+ getByText('Nov 26, 2020 10:28:56 AM');
+ });
+
+ it('renders caption content', () => {
+ const { getByText } = render();
+
+ getByText('test caption content');
+ });
+
+ it('renders caption content for screenshot ref data', async () => {
+ const { getByText } = render(
+
+ );
+
+ getByText('test caption content');
+ });
+});
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/screenshot_overlay_footer.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/screenshot_overlay_footer.tsx
new file mode 100644
index 0000000000000..3a01a74721d40
--- /dev/null
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/monitor_test_result/screenshot_overlay_footer.tsx
@@ -0,0 +1,132 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { MouseEvent, useEffect } from 'react';
+import { css } from '@emotion/react';
+import {
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiText,
+ useEuiTheme,
+ useIsWithinMaxBreakpoint,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { ScreenshotRefImageData } from '../../../../../../common/runtime_types';
+
+export interface ScreenshotOverlayFooterProps {
+ captionContent: string;
+ imgSrc?: string;
+ imgRef?: ScreenshotRefImageData;
+ maxSteps?: number;
+ setStepNumber: React.Dispatch>;
+ stepNumber: number;
+ label?: string;
+ onVisible: (val: boolean) => void;
+ isLoading: boolean;
+}
+
+export const ScreenshotOverlayFooter: React.FC = ({
+ captionContent,
+ imgRef,
+ imgSrc,
+ maxSteps,
+ setStepNumber,
+ stepNumber,
+ isLoading,
+ label,
+ onVisible,
+}) => {
+ const { euiTheme } = useEuiTheme();
+
+ useEffect(() => {
+ onVisible(true);
+ return () => {
+ onVisible(false);
+ };
+ // Empty deps to only trigger effect once on init
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const isSmall = useIsWithinMaxBreakpoint('m');
+
+ return (
+
{
+ // we don't want this to be captured by row click which leads to step list page
+ evt.stopPropagation();
+ }}
+ onKeyDown={(evt) => {
+ // Just to satisfy ESLint
+ }}
+ >
+