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

chore: Use synthetic focus tracking in substeps #1375

Merged
merged 4 commits into from
Jul 27, 2023
Merged
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
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
Loading