Skip to content

Commit

Permalink
chore: Use synthetic focus tracking in substeps (#1375)
Browse files Browse the repository at this point in the history
  • Loading branch information
connorlanigan authored Jul 27, 2023
1 parent 4b2c67b commit c203e09
Show file tree
Hide file tree
Showing 9 changed files with 359 additions and 52 deletions.
21 changes: 20 additions & 1 deletion pages/form/simple.page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import React, { useState } from 'react';
import Header from '~components/header';
import Form from '~components/form';
import Link from '~components/link';
Expand All @@ -11,8 +11,11 @@ import SpaceBetween from '~components/space-between';
import Container from '~components/container';
import ScreenshotArea from '../utils/screenshot-area';
import styles from './styles.scss';
import Tiles from '~components/tiles';

export default function FormScenario() {
const [value, setValue] = useState<string>('bar');

return (
<ScreenshotArea>
<div className={styles['form-container']}>
Expand Down Expand Up @@ -54,6 +57,22 @@ export default function FormScenario() {
<FormField label="Second field">
<Input value="" readOnly={true} />
</FormField>
<FormField label="Some tiles">
<Tiles
value={value}
onChange={event => setValue(event.detail.value)}
columns={3}
items={[
{ label: 'Foo', value: 'foo' },
{ label: 'Bar', value: 'bar' },
{ label: 'Baz', value: 'baz', disabled: true },
{ label: 'Boo', value: 'boo' },
]}
/>
</FormField>
<FormField label="Third field">
<Input value="" readOnly={true} />
</FormField>
</SpaceBetween>
</Container>
</SpaceBetween>
Expand Down
40 changes: 27 additions & 13 deletions src/container/__tests__/analytics.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { render, fireEvent, act } from '@testing-library/react';
import { render, act } from '@testing-library/react';

import Container from '../../../lib/components/container';

Expand All @@ -18,6 +18,7 @@ import {
describe('Funnel Analytics', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers();
mockFunnelMetrics();
});

Expand All @@ -38,7 +39,7 @@ describe('Funnel Analytics', () => {
expect(getByTestId('container')).toHaveAttribute(DATA_ATTR_FUNNEL_SUBSTEP, expect.any(String));
});

test('sends funnelSubStepStart and funnelSubStepComplete metric when focussed and blurred', () => {
test('sends funnelSubStepStart and funnelSubStepComplete metric when focussed and blurred', async () => {
const { getByTestId } = render(
<AnalyticsFunnel funnelType="single-page" optionalStepNumbers={[]} totalFunnelSteps={1}>
<AnalyticsFunnelStep stepNumber={2} stepNameSelector=".step-name-selector">
Expand All @@ -51,16 +52,17 @@ describe('Funnel Analytics', () => {

expect(FunnelMetrics.funnelSubStepStart).toHaveBeenCalledTimes(0);

fireEvent.focus(getByTestId('input'));
getByTestId('input').focus();
await runPendingPromises();
expect(FunnelMetrics.funnelSubStepStart).toHaveBeenCalledTimes(1);
expect(FunnelMetrics.funnelSubStepComplete).not.toHaveBeenCalled();

fireEvent.blur(getByTestId('input'));
getByTestId('input').blur();
expect(FunnelMetrics.funnelSubStepStart).toHaveBeenCalledTimes(1);
expect(FunnelMetrics.funnelSubStepComplete).toHaveBeenCalledTimes(1);
});

test('moving the focus inside one container does not emit metrics', () => {
test('moving the focus inside one container does not emit metrics', async () => {
const { getByTestId } = render(
<AnalyticsFunnel funnelType="single-page" optionalStepNumbers={[]} totalFunnelSteps={1}>
<AnalyticsFunnelStep stepNumber={2} stepNameSelector=".step-name-selector">
Expand All @@ -75,15 +77,17 @@ describe('Funnel Analytics', () => {

expect(FunnelMetrics.funnelSubStepStart).not.toHaveBeenCalled();

act(() => getByTestId('input-one').focus());
act(() => getByTestId('input-two').focus());
act(() => getByTestId('input-one').focus());
getByTestId('input-one').focus();
getByTestId('input-two').focus();
getByTestId('input-one').focus();

await runPendingPromises();

expect(FunnelMetrics.funnelSubStepStart).toHaveBeenCalledTimes(1);
expect(FunnelMetrics.funnelSubStepComplete).not.toHaveBeenCalled();
});

test('nested containers do not send their own events', () => {
test('nested containers do not send their own events', async () => {
const { getByTestId } = render(
<AnalyticsFunnel funnelType="single-page" optionalStepNumbers={[]} totalFunnelSteps={1}>
<AnalyticsFunnelStep stepNumber={2} stepNameSelector=".step-name-selector">
Expand All @@ -100,15 +104,17 @@ describe('Funnel Analytics', () => {

expect(FunnelMetrics.funnelSubStepStart).not.toHaveBeenCalled();

act(() => getByTestId('input-one').focus());
act(() => getByTestId('input-two').focus());
act(() => getByTestId('input-one').focus());
getByTestId('input-one').focus();
getByTestId('input-two').focus();
getByTestId('input-one').focus();

await runPendingPromises();

expect(FunnelMetrics.funnelSubStepStart).toHaveBeenCalledTimes(1);
expect(FunnelMetrics.funnelSubStepComplete).not.toHaveBeenCalled();
});

test('sibling containers send their own events', () => {
test('sibling containers send their own events', async () => {
const { getByTestId } = render(
<AnalyticsFunnel funnelType="single-page" optionalStepNumbers={[]} totalFunnelSteps={1}>
<AnalyticsFunnelStep stepNumber={2} stepNameSelector=".step-name-selector">
Expand All @@ -125,15 +131,23 @@ describe('Funnel Analytics', () => {
expect(FunnelMetrics.funnelSubStepStart).not.toHaveBeenCalled();

act(() => getByTestId('input-one').focus());
await runPendingPromises();
expect(FunnelMetrics.funnelSubStepStart).toHaveBeenCalledTimes(1);
expect(FunnelMetrics.funnelSubStepComplete).not.toHaveBeenCalled();

act(() => getByTestId('input-two').focus());
await runPendingPromises();
expect(FunnelMetrics.funnelSubStepStart).toHaveBeenCalledTimes(2);
expect(FunnelMetrics.funnelSubStepComplete).toHaveBeenCalledTimes(1);

act(() => getByTestId('input-one').focus());
await runPendingPromises();
expect(FunnelMetrics.funnelSubStepStart).toHaveBeenCalledTimes(3);
expect(FunnelMetrics.funnelSubStepComplete).toHaveBeenCalledTimes(2);
});
});

const runPendingPromises = async () => {
jest.runAllTimers();
await Promise.resolve();
};
14 changes: 11 additions & 3 deletions src/expandable-section/__tests__/analytics.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
describe('Expandable section funnel analytics', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers();
mockFunnelMetrics();
});

Expand All @@ -41,7 +42,7 @@ describe('Expandable section funnel analytics', () => {
expect(getByTestId('container')).toHaveAttribute(DATA_ATTR_FUNNEL_SUBSTEP, expect.any(String));
});

test('sends funnelSubStepStart and funnelSubStepComplete metric when focussed and blurred', () => {
test('sends funnelSubStepStart and funnelSubStepComplete metric when focussed and blurred', async () => {
const { getByTestId } = render(
<AnalyticsFunnel funnelType="single-page" optionalStepNumbers={[]} totalFunnelSteps={1}>
<AnalyticsFunnelStep stepNumber={2} stepNameSelector=".step-name-selector">
Expand All @@ -54,8 +55,10 @@ describe('Expandable section funnel analytics', () => {

expect(FunnelMetrics.funnelSubStepStart).toHaveBeenCalledTimes(0);

fireEvent.focus(getByTestId('input'));
fireEvent.blur(getByTestId('input'));
getByTestId('input').focus();
await runPendingPromises();

getByTestId('input').blur();

expect(FunnelMetrics.funnelSubStepStart).toHaveBeenCalledTimes(1);
expect(FunnelMetrics.funnelSubStepComplete).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -98,3 +101,8 @@ describe('Expandable section funnel analytics', () => {
});
});
});

const runPendingPromises = async () => {
jest.runAllTimers();
await Promise.resolve();
};
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ describe('AnalyticsFunnel', () => {
describe('AnalyticsFunnelStep', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers();
mockFunnelMetrics();
});

Expand Down Expand Up @@ -421,6 +422,7 @@ describe('AnalyticsFunnelStep', () => {
describe('AnalyticsFunnelSubStep', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers();
mockFunnelMetrics();
});

Expand Down Expand Up @@ -448,7 +450,7 @@ describe('AnalyticsFunnelSubStep', () => {
expect(FunnelMetrics.funnelSubStepComplete).not.toHaveBeenCalled();
});

test('calls funnelSubStepStart with the correct arguments when the substep is focused', () => {
test('calls funnelSubStepStart with the correct arguments when the substep is focused', async () => {
// ChildComponent is a sample component that renders a button to call funnelSubmit
const ChildComponent = () => {
const { subStepRef, funnelSubStepProps } = useFunnelSubStep();
Expand All @@ -472,7 +474,9 @@ describe('AnalyticsFunnelSubStep', () => {
</AnalyticsFunnel>
);

fireEvent.focus(getByTestId('input'));
getByTestId('input').focus();

await runPendingPromises();

expect(FunnelMetrics.funnelSubStepStart).toHaveBeenCalledTimes(1);
expect(FunnelMetrics.funnelSubStepStart).toHaveBeenCalledWith({
Expand All @@ -485,7 +489,7 @@ describe('AnalyticsFunnelSubStep', () => {
});
});

test('calls funnelSubStepComplete with the correct arguments when the substep loses focus', () => {
test('calls funnelSubStepComplete with the correct arguments when the substep loses focus by keyboard', async () => {
const ChildComponent = () => {
const { subStepRef, funnelSubStepProps } = useFunnelSubStep();

Expand All @@ -509,7 +513,57 @@ describe('AnalyticsFunnelSubStep', () => {
</AnalyticsFunnel>
);

fireEvent.blur(getByTestId('input'));
getByTestId('input').focus();

await runPendingPromises();

getByTestId('input').blur();

expect(FunnelMetrics.funnelSubStepComplete).toHaveBeenCalledTimes(1);
expect(FunnelMetrics.funnelSubStepComplete).toHaveBeenCalledWith({
funnelInteractionId: mockedFunnelInteractionId,
stepNumber,
stepNameSelector,
subStepAllSelector: expect.any(String),
subStepSelector: expect.any(String),
subStepNameSelector: expect.any(String),
});
});

test('calls funnelSubStepComplete with the correct arguments when the substep loses focus by mouse', async () => {
const ChildComponent = () => {
const { subStepRef, funnelSubStepProps } = useFunnelSubStep();

return (
<div ref={subStepRef} {...funnelSubStepProps}>
<input data-testid="input" />
</div>
);
};

const stepNumber = 1;
const stepNameSelector = '.step-name-selector';

const { getByTestId } = render(
<>
<AnalyticsFunnel funnelType="single-page" optionalStepNumbers={[]} totalFunnelSteps={1}>
<AnalyticsFunnelStep stepNumber={stepNumber} stepNameSelector={stepNameSelector}>
<AnalyticsFunnelSubStep>
<ChildComponent />
</AnalyticsFunnelSubStep>
</AnalyticsFunnelStep>
</AnalyticsFunnel>
<input data-testid="outside" />
</>
);

simulateUserClick(getByTestId('input'));

await runPendingPromises();

simulateUserClick(getByTestId('outside'));

await runPendingPromises();

expect(FunnelMetrics.funnelSubStepComplete).toHaveBeenCalledTimes(1);
expect(FunnelMetrics.funnelSubStepComplete).toHaveBeenCalledWith({
Expand All @@ -522,3 +576,16 @@ describe('AnalyticsFunnelSubStep', () => {
});
});
});

const simulateUserClick = (element: HTMLElement) => {
// See https://testing-library.com/docs/guide-events/
fireEvent.mouseDown(element);
element.focus();
fireEvent.mouseUp(element);
fireEvent.click(element);
};

const runPendingPromises = async () => {
jest.runAllTimers();
await Promise.resolve();
};
Loading

0 comments on commit c203e09

Please sign in to comment.