@automattic/launchpad 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/.storybook/main.ts +16 -0
  2. package/.storybook/preview.ts +6 -0
  3. package/README.md +11 -2
  4. package/dist/cjs/action-components/index.js +6 -0
  5. package/dist/cjs/action-components/index.js.map +1 -0
  6. package/dist/cjs/action-components/share-site-modal/index.js +44 -0
  7. package/dist/cjs/action-components/share-site-modal/index.js.map +1 -0
  8. package/dist/cjs/action-components/share-site-modal/style.scss +159 -0
  9. package/dist/cjs/badge/index.js +2 -2
  10. package/dist/cjs/badge/index.js.map +1 -1
  11. package/dist/cjs/checklist/index.js +16 -8
  12. package/dist/cjs/checklist/index.js.map +1 -1
  13. package/dist/cjs/checklist/index.stories.js +29 -0
  14. package/dist/cjs/checklist/index.stories.js.map +1 -0
  15. package/dist/cjs/checklist-item/index.js +30 -13
  16. package/dist/cjs/checklist-item/index.js.map +1 -1
  17. package/dist/cjs/checklist-item/index.stories.js +79 -0
  18. package/dist/cjs/checklist-item/index.stories.js.map +1 -0
  19. package/dist/cjs/checklist-item/style.scss +142 -22
  20. package/dist/cjs/index.js +6 -1
  21. package/dist/cjs/index.js.map +1 -1
  22. package/dist/cjs/launchpad-container/index.js +11 -0
  23. package/dist/cjs/launchpad-container/index.js.map +1 -0
  24. package/dist/cjs/launchpad-container/style.scss +59 -0
  25. package/dist/cjs/launchpad-internal.js +40 -0
  26. package/dist/cjs/launchpad-internal.js.map +1 -0
  27. package/dist/cjs/launchpad-internal.stories.js +45 -0
  28. package/dist/cjs/launchpad-internal.stories.js.map +1 -0
  29. package/dist/cjs/launchpad.js +39 -10
  30. package/dist/cjs/launchpad.js.map +1 -1
  31. package/dist/cjs/msw/link-in-bio.js +68 -0
  32. package/dist/cjs/msw/link-in-bio.js.map +1 -0
  33. package/dist/cjs/msw/resolver.js +19 -0
  34. package/dist/cjs/msw/resolver.js.map +1 -0
  35. package/dist/cjs/msw/setup-free.js +88 -0
  36. package/dist/cjs/msw/setup-free.js.map +1 -0
  37. package/dist/cjs/setup-actions.js +51 -54
  38. package/dist/cjs/setup-actions.js.map +1 -1
  39. package/dist/cjs/use-tracking/index.js +72 -0
  40. package/dist/cjs/use-tracking/index.js.map +1 -0
  41. package/dist/esm/action-components/index.js +2 -0
  42. package/dist/esm/action-components/index.js.map +1 -0
  43. package/dist/esm/action-components/share-site-modal/index.js +42 -0
  44. package/dist/esm/action-components/share-site-modal/index.js.map +1 -0
  45. package/dist/esm/action-components/share-site-modal/style.scss +159 -0
  46. package/dist/esm/badge/index.js +2 -2
  47. package/dist/esm/badge/index.js.map +1 -1
  48. package/dist/esm/checklist/index.js +14 -8
  49. package/dist/esm/checklist/index.js.map +1 -1
  50. package/dist/esm/checklist/index.stories.js +25 -0
  51. package/dist/esm/checklist/index.stories.js.map +1 -0
  52. package/dist/esm/checklist-item/index.js +29 -14
  53. package/dist/esm/checklist-item/index.js.map +1 -1
  54. package/dist/esm/checklist-item/index.stories.js +75 -0
  55. package/dist/esm/checklist-item/index.stories.js.map +1 -0
  56. package/dist/esm/checklist-item/style.scss +142 -22
  57. package/dist/esm/index.js +3 -0
  58. package/dist/esm/index.js.map +1 -1
  59. package/dist/esm/launchpad-container/index.js +8 -0
  60. package/dist/esm/launchpad-container/index.js.map +1 -0
  61. package/dist/esm/launchpad-container/style.scss +59 -0
  62. package/dist/esm/launchpad-internal.js +37 -0
  63. package/dist/esm/launchpad-internal.js.map +1 -0
  64. package/dist/esm/launchpad-internal.stories.js +41 -0
  65. package/dist/esm/launchpad-internal.stories.js.map +1 -0
  66. package/dist/esm/launchpad.js +41 -13
  67. package/dist/esm/launchpad.js.map +1 -1
  68. package/dist/esm/msw/link-in-bio.js +66 -0
  69. package/dist/esm/msw/link-in-bio.js.map +1 -0
  70. package/dist/esm/msw/resolver.js +14 -0
  71. package/dist/esm/msw/resolver.js.map +1 -0
  72. package/dist/esm/msw/setup-free.js +86 -0
  73. package/dist/esm/msw/setup-free.js.map +1 -0
  74. package/dist/esm/setup-actions.js +51 -54
  75. package/dist/esm/setup-actions.js.map +1 -1
  76. package/dist/esm/use-tracking/index.js +68 -0
  77. package/dist/esm/use-tracking/index.js.map +1 -0
  78. package/dist/tsconfig-cjs.tsbuildinfo +1 -1
  79. package/dist/tsconfig.tsbuildinfo +1 -1
  80. package/dist/types/action-components/index.d.ts +2 -0
  81. package/dist/types/action-components/index.d.ts.map +1 -0
  82. package/dist/types/action-components/share-site-modal/index.d.ts +9 -0
  83. package/dist/types/action-components/share-site-modal/index.d.ts.map +1 -0
  84. package/dist/types/badge/index.d.ts.map +1 -1
  85. package/dist/types/checklist/index.d.ts +6 -7
  86. package/dist/types/checklist/index.d.ts.map +1 -1
  87. package/dist/types/checklist/index.stories.d.ts +8 -0
  88. package/dist/types/checklist/index.stories.d.ts.map +1 -0
  89. package/dist/types/checklist-item/index.d.ts +11 -8
  90. package/dist/types/checklist-item/index.d.ts.map +1 -1
  91. package/dist/types/checklist-item/index.stories.d.ts +14 -0
  92. package/dist/types/checklist-item/index.stories.d.ts.map +1 -0
  93. package/dist/types/index.d.ts +3 -0
  94. package/dist/types/index.d.ts.map +1 -1
  95. package/dist/types/launchpad-container/index.d.ts +15 -0
  96. package/dist/types/launchpad-container/index.d.ts.map +1 -0
  97. package/dist/types/launchpad-internal.d.ts +20 -0
  98. package/dist/types/launchpad-internal.d.ts.map +1 -0
  99. package/dist/types/launchpad-internal.stories.d.ts +8 -0
  100. package/dist/types/launchpad-internal.stories.d.ts.map +1 -0
  101. package/dist/types/launchpad.d.ts +10 -9
  102. package/dist/types/launchpad.d.ts.map +1 -1
  103. package/dist/types/msw/link-in-bio.d.ts +35 -0
  104. package/dist/types/msw/link-in-bio.d.ts.map +1 -0
  105. package/dist/types/msw/resolver.d.ts +3 -0
  106. package/dist/types/msw/resolver.d.ts.map +1 -0
  107. package/dist/types/msw/setup-free.d.ts +35 -0
  108. package/dist/types/msw/setup-free.d.ts.map +1 -0
  109. package/dist/types/setup-actions.d.ts +1 -1
  110. package/dist/types/setup-actions.d.ts.map +1 -1
  111. package/dist/types/types.d.ts +19 -9
  112. package/dist/types/types.d.ts.map +1 -1
  113. package/dist/types/use-tracking/index.d.ts +14 -0
  114. package/dist/types/use-tracking/index.d.ts.map +1 -0
  115. package/package.json +24 -16
  116. package/src/action-components/index.ts +1 -0
  117. package/src/action-components/share-site-modal/index.tsx +100 -0
  118. package/src/action-components/share-site-modal/style.scss +159 -0
  119. package/src/badge/index.tsx +2 -4
  120. package/src/checklist/index.stories.tsx +45 -0
  121. package/src/checklist/index.tsx +22 -21
  122. package/src/checklist/test/index.tsx +26 -0
  123. package/src/checklist-item/index.stories.tsx +92 -0
  124. package/src/checklist-item/index.tsx +62 -22
  125. package/src/checklist-item/style.scss +142 -22
  126. package/src/checklist-item/test/index.tsx +232 -0
  127. package/src/index.ts +3 -0
  128. package/src/launchpad-container/index.tsx +45 -0
  129. package/src/launchpad-container/style.scss +59 -0
  130. package/src/launchpad-container/test/index.tsx +72 -0
  131. package/src/launchpad-internal.stories.tsx +49 -0
  132. package/src/launchpad-internal.tsx +85 -0
  133. package/src/launchpad.tsx +80 -30
  134. package/src/msw/link-in-bio.ts +66 -0
  135. package/src/msw/resolver.ts +18 -0
  136. package/src/msw/setup-free.ts +85 -0
  137. package/src/setup-actions.ts +67 -54
  138. package/src/test/launchpad.tsx +51 -24
  139. package/src/test/lib/fixtures.ts +7 -2
  140. package/src/types.ts +24 -9
  141. package/src/use-tracking/index.tsx +87 -0
  142. package/src/use-tracking/test/index.tsx +116 -0
  143. package/static/mockServiceWorker.js +287 -0
  144. package/src/test/checklist-item.tsx +0 -96
  145. package/src/test/checklist.tsx +0 -25
@@ -1,9 +1,13 @@
1
- import { render, screen } from '@testing-library/react';
2
- import React from 'react';
3
- import Launchpad from '../launchpad';
1
+ import { recordTracksEvent } from '@automattic/calypso-analytics';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import React, { ComponentProps } from 'react';
5
+ import LaunchpadInternal from '../launchpad-internal';
4
6
  import '@testing-library/jest-dom';
5
7
  import type { Task } from '../types';
6
8
 
9
+ jest.mock( '@automattic/calypso-analytics' );
10
+
7
11
  jest.mock( '@automattic/data-stores', () => {
8
12
  const { buildTask } = require( './lib/fixtures' );
9
13
  return {
@@ -12,9 +16,9 @@ jest.mock( '@automattic/data-stores', () => {
12
16
  isFetchedAfterMount: true,
13
17
  data: {
14
18
  checklist: [
15
- buildTask( { id: 'task1' } ),
16
- buildTask( { id: 'task2' } ),
17
- buildTask( { id: 'task3' } ),
19
+ buildTask( { id: 'task1', title: 'Task 1', completed: false, disabled: false } ),
20
+ buildTask( { id: 'task2', title: 'Task 2' } ),
21
+ buildTask( { id: 'task3', title: 'Task 3' } ),
18
22
  ],
19
23
  },
20
24
  };
@@ -22,28 +26,51 @@ jest.mock( '@automattic/data-stores', () => {
22
26
  };
23
27
  } );
24
28
 
25
- describe( 'Launchpad', () => {
26
- describe( 'when no taskFilter is provided', () => {
27
- it( 'then all tasks from useLaunchpad are rendered', () => {
28
- render( <Launchpad siteSlug="any site" /> );
29
- const checklistItems = screen.queryAllByRole( 'listitem' );
30
- expect( checklistItems.length ).toBe( 3 );
31
- } );
29
+ describe( 'LaunchpadInternal', () => {
30
+ const defaultProps = {
31
+ siteSlug: 'any site',
32
+ checklistSlug: 'some-site',
33
+ launchpadContext: 'some-context',
34
+ };
35
+ const renderComponent = ( props: Partial< ComponentProps< typeof LaunchpadInternal > > = {} ) => {
36
+ return render( <LaunchpadInternal { ...defaultProps } { ...props } /> );
37
+ };
38
+
39
+ it( 'renders the tasks', () => {
40
+ renderComponent();
41
+ const checklistItems = screen.queryAllByRole( 'listitem' );
42
+ expect( checklistItems.length ).toBe( 3 );
32
43
  } );
33
44
 
34
- describe( 'when taskFilter is provided', () => {
35
- it( 'then render only tasks returned from the callback', () => {
36
- const filter = ( tasks: Task[] ) => {
37
- // return only the first task
38
- return [ tasks[ 0 ] ];
39
- };
45
+ it( 'records the checklist item click', async () => {
46
+ renderComponent();
40
47
 
41
- render( <Launchpad siteSlug="any site" taskFilter={ filter } /> );
42
- const checklistItems = screen.queryAllByRole( 'listitem' );
43
- expect( checklistItems.length ).toBe( 1 );
48
+ jest.clearAllMocks();
49
+ await userEvent.click( screen.getByRole( 'button', { name: /Task 1/ } ) );
44
50
 
45
- const taskId = checklistItems[ 0 ].querySelector( 'button' )?.getAttribute( 'data-task' );
46
- expect( taskId ).toBe( 'task1' );
51
+ await waitFor( () => {
52
+ expect( recordTracksEvent ).toHaveBeenCalledWith( 'calypso_launchpad_task_clicked', {
53
+ checklist_completed: false,
54
+ checklist_slug: 'some-site',
55
+ context: 'some-context',
56
+ is_completed: false,
57
+ order: undefined,
58
+ task_id: 'task1',
59
+ } );
47
60
  } );
48
61
  } );
62
+
63
+ it( 'renders only the tasks returned taskFilter', () => {
64
+ const filter = ( tasks: Task[] ) => {
65
+ // return only the first task
66
+ return [ tasks[ 0 ] ];
67
+ };
68
+
69
+ renderComponent( { taskFilter: filter } );
70
+ const checklistItems = screen.queryAllByRole( 'listitem' );
71
+ expect( checklistItems.length ).toBe( 1 );
72
+
73
+ const taskId = checklistItems[ 0 ].querySelector( 'button' )?.getAttribute( 'data-task' );
74
+ expect( taskId ).toBe( 'task1' );
75
+ } );
49
76
  } );
@@ -139,7 +139,6 @@ export const defaultSiteDetails: SiteDetails = {
139
139
  was_created_with_blank_canvas_design: false,
140
140
  videopress_storage_used: 0,
141
141
  is_difm_lite_in_progress: false,
142
- difm_lite_site_options: {},
143
142
  site_intent: 'link-in-bio',
144
143
  site_vertical_id: null,
145
144
  launchpad_screen: 'full',
@@ -212,6 +211,7 @@ export const defaultDomainResponse: any = {
212
211
  aftermarketAuction: false,
213
212
  aftermarketAuctionEnd: null,
214
213
  aftermarketAuctionStart: null,
214
+ authCodeRequired: true,
215
215
  autoRenewalDate: '',
216
216
  autoRenewing: false,
217
217
  beginTransferUntilDate: '',
@@ -223,6 +223,8 @@ export const defaultDomainResponse: any = {
223
223
  cannotManageNameServersReason: null,
224
224
  cannotUpdateContactInfoReason: null,
225
225
  canSetAsPrimary: false,
226
+ canTransferToOtherSite: true,
227
+ canTransferToAnyUser: true,
226
228
  canUpdateContactInfo: false,
227
229
  connectionMode: '',
228
230
  contactInfoDisclosed: false,
@@ -232,6 +234,7 @@ export const defaultDomainResponse: any = {
232
234
  currentUserCanManage: false,
233
235
  currentUserCannotAddEmailReason: null,
234
236
  currentUserIsOwner: false,
237
+ dnssecRecords: undefined,
235
238
  domain: '',
236
239
  domainLockingAvailable: false,
237
240
  domainRegistrationAgreementUrl: null,
@@ -245,7 +248,10 @@ export const defaultDomainResponse: any = {
245
248
  hasWpcomNameservers: false,
246
249
  hasZone: false,
247
250
  isAutoRenewing: false,
251
+ isDnssecEnabled: false,
252
+ isDnssecSupported: false,
248
253
  isEligibleForInboundTransfer: false,
254
+ isGravatarDomain: false,
249
255
  isIcannVerificationSuspended: false,
250
256
  isLocked: false,
251
257
  isPendingIcannVerification: false,
@@ -311,7 +317,6 @@ export const MOCK_USE_QUERY_RESULT = {
311
317
  isLoading: false,
312
318
  isLoadingError: false,
313
319
  isPlaceholderData: false,
314
- isPreviousData: false,
315
320
  isRefetchError: false,
316
321
  isRefetching: false,
317
322
  isStale: false,
package/src/types.ts CHANGED
@@ -1,13 +1,16 @@
1
+ import { ReactNode } from 'react';
2
+
1
3
  export interface TaskExtraData {
2
4
  about_page_id?: number;
3
5
  }
6
+
4
7
  export interface Task {
5
8
  id: string;
6
9
  completed: boolean;
7
10
  disabled: boolean;
8
- title?: string;
11
+ title?: ReactNode | string;
9
12
  subtitle?: string | React.ReactNode | null;
10
- badge_text?: string;
13
+ badge_text?: ReactNode | string;
11
14
  actionDispatch?: () => void;
12
15
  isLaunchTask?: boolean;
13
16
  extra_data?: TaskExtraData;
@@ -15,6 +18,19 @@ export interface Task {
15
18
  target_repetitions?: number;
16
19
  repetition_count?: number;
17
20
  order?: number;
21
+ useCalypsoPath?: boolean;
22
+ actionUrl?: string;
23
+ }
24
+
25
+ export interface ExpandableAction {
26
+ label: string;
27
+ onClick: () => void;
28
+ }
29
+
30
+ export interface Expandable {
31
+ isOpen: boolean;
32
+ content: JSX.Element;
33
+ action?: ExpandableAction;
18
34
  }
19
35
 
20
36
  export type LaunchpadChecklist = Task[];
@@ -49,22 +65,21 @@ export interface LaunchpadStatuses {
49
65
  domain_upsell_deferred?: boolean;
50
66
  }
51
67
 
52
- export interface LaunchpadResponse {
53
- site_intent: string;
54
- launchpad_screen: boolean | string;
55
- checklist_statuses: LaunchpadStatuses[];
56
- }
57
-
58
68
  export interface PermittedActions {
59
69
  setShareSiteModalIsOpen?: ( isOpen: boolean ) => void;
60
- siteLaunched?: () => void;
61
70
  setActiveChecklist: ( siteSlug: string, activeChecklistSlug: string ) => void;
62
71
  }
63
72
 
73
+ export type EventHandlers = {
74
+ onSiteLaunched?: () => void;
75
+ onTaskClick?: ( task: Task ) => void;
76
+ };
77
+
64
78
  export interface LaunchpadTaskActionsProps {
65
79
  siteSlug: string | null;
66
80
  tasks: Task[];
67
81
  tracksData: LaunchpadTracksData;
68
82
  extraActions: PermittedActions;
69
83
  uiContext?: 'calypso';
84
+ eventHandlers?: EventHandlers;
70
85
  }
@@ -0,0 +1,87 @@
1
+ import { recordTracksEvent } from '@automattic/calypso-analytics';
2
+ import { useEffect, useMemo } from 'react';
3
+ import type { Task } from '../types';
4
+ import type { SiteDetails } from '@automattic/data-stores';
5
+
6
+ interface Params {
7
+ tasks: Task[];
8
+ checklistSlug: string | null;
9
+ context: string;
10
+ site: SiteDetails | null | undefined;
11
+ flow?: string;
12
+ }
13
+
14
+ const stringifyTasks = ( tasks: Task[] ) => {
15
+ const prefix = ',';
16
+ const suffix = ',';
17
+ const separator = ',';
18
+
19
+ const taskIds = tasks.map( ( task ) => task.id ).join( separator );
20
+ return `${ prefix }${ taskIds }${ suffix }`;
21
+ };
22
+
23
+ export const useTracking = ( params: Params ) => {
24
+ const { tasks, checklistSlug, context, site, flow } = params;
25
+ const completedSteps = useMemo( () => tasks.filter( ( task ) => task.completed ), [ tasks ] );
26
+ const taskNames = useMemo( () => stringifyTasks( tasks ), [ tasks ] );
27
+ const numberOfSteps = tasks.length;
28
+ const numberOfCompletedSteps = completedSteps.length;
29
+ const isCheckListCompleted = completedSteps.length === tasks.length;
30
+ const isSiteAvailable = !! site;
31
+ const hasNoTask = tasks.length === 0;
32
+
33
+ // We skip the view events until we have fetched the site details to avoid sending incomplete data
34
+ const shoulSkipTracking = hasNoTask || ! isSiteAvailable;
35
+
36
+ useEffect( () => {
37
+ if ( shoulSkipTracking ) {
38
+ return;
39
+ }
40
+
41
+ const siteIntent = site?.options?.site_intent;
42
+
43
+ recordTracksEvent( 'calypso_launchpad_tasklist_viewed', {
44
+ number_of_steps: numberOfSteps,
45
+ number_of_completed_steps: numberOfCompletedSteps,
46
+ is_completed: isCheckListCompleted,
47
+ tasks: taskNames,
48
+ checklist_slug: checklistSlug,
49
+ context: context,
50
+ site_intent: siteIntent,
51
+ flow,
52
+ } );
53
+
54
+ tasks.forEach( ( task: Task ) => {
55
+ recordTracksEvent( 'calypso_launchpad_task_view', {
56
+ checklist_slug: checklistSlug,
57
+ task_id: task.id,
58
+ is_completed: task.completed,
59
+ context: context,
60
+ order: task.order,
61
+ site_intent: siteIntent,
62
+ flow,
63
+ } );
64
+ } );
65
+ // Array of tasks requires deep comparison
66
+ // eslint-disable-next-line react-hooks/exhaustive-deps
67
+ }, [
68
+ JSON.stringify( tasks.map( ( { id, completed, order } ) => ( { id, completed, order } ) ) ),
69
+ shoulSkipTracking,
70
+ ] );
71
+
72
+ const trackTaskClick = ( task: Task ) => {
73
+ recordTracksEvent( 'calypso_launchpad_task_clicked', {
74
+ checklist_completed: isCheckListCompleted,
75
+ is_completed: task.completed,
76
+ checklist_slug: checklistSlug,
77
+ context: context,
78
+ task_id: task.id,
79
+ flow,
80
+ order: task.order,
81
+ } );
82
+ };
83
+
84
+ return {
85
+ trackTaskClick,
86
+ };
87
+ };
@@ -0,0 +1,116 @@
1
+ import { recordTracksEvent } from '@automattic/calypso-analytics';
2
+ import { renderHook } from '@testing-library/react';
3
+ import { useTracking } from '..';
4
+ import { buildTask } from '../../test/lib/fixtures';
5
+ import type { SiteDetails } from '@automattic/data-stores';
6
+
7
+ jest.mock( '@automattic/calypso-analytics' );
8
+
9
+ describe( 'useTracking', () => {
10
+ beforeEach( () => jest.clearAllMocks() );
11
+
12
+ const buildDefaultProps = ( options = {} ) => ( {
13
+ checklistSlug: 'site-setup-checklist',
14
+ context: 'customer-home',
15
+ site: { options: { site_intent: 'build' } } as SiteDetails,
16
+ tasks: [
17
+ buildTask( { id: 'task-1', completed: true } ),
18
+ buildTask( { id: 'task-2', completed: false } ),
19
+ ],
20
+ ...options,
21
+ } );
22
+
23
+ it( 'tracks the view event on load', () => {
24
+ renderHook( () => useTracking( buildDefaultProps() ) );
25
+
26
+ expect( recordTracksEvent ).toHaveBeenCalledWith( 'calypso_launchpad_tasklist_viewed', {
27
+ checklist_slug: 'site-setup-checklist',
28
+ tasks: ',task-1,task-2,',
29
+ is_completed: false,
30
+ number_of_steps: 2,
31
+ number_of_completed_steps: 1,
32
+ context: 'customer-home',
33
+ site_intent: 'build',
34
+ } );
35
+ } );
36
+
37
+ it( 'use comma as prefix and suffix for the task names to simplify queries on the logs tool', () => {
38
+ renderHook( () => useTracking( buildDefaultProps() ) );
39
+
40
+ const tasks = jest.mocked( recordTracksEvent ).mock.calls[ 0 ][ 1 ].tasks;
41
+ expect( tasks ).toBe( ',task-1,task-2,' );
42
+ } );
43
+
44
+ it( 'tracks the view event by task', () => {
45
+ const tasks = [
46
+ buildTask( { id: 'task-1', completed: true, order: 3 } ),
47
+ buildTask( { id: 'task-2', completed: false, order: 4 } ),
48
+ ];
49
+
50
+ renderHook( () => useTracking( buildDefaultProps( { tasks } ) ) );
51
+
52
+ expect( recordTracksEvent ).toHaveBeenNthCalledWith( 2, 'calypso_launchpad_task_view', {
53
+ checklist_slug: 'site-setup-checklist',
54
+ is_completed: true,
55
+ context: 'customer-home',
56
+ site_intent: 'build',
57
+ order: 3,
58
+ task_id: 'task-1',
59
+ } );
60
+ } );
61
+
62
+ it( 'prevent calls multiple times during rerender', () => {
63
+ const params = buildDefaultProps();
64
+ const { rerender } = renderHook( () => useTracking( params ) );
65
+
66
+ rerender();
67
+
68
+ // 3 calls: 1 for the view and 2 for the tasks
69
+ expect( recordTracksEvent ).toHaveBeenCalledTimes( 3 );
70
+ } );
71
+
72
+ it( 'logs the load events only when the site info is available', () => {
73
+ const { rerender } = renderHook( ( site = null ) =>
74
+ useTracking( buildDefaultProps( { site } ) )
75
+ );
76
+
77
+ expect( recordTracksEvent ).toHaveBeenCalledTimes( 0 );
78
+
79
+ rerender( { options: { site_intent: 'some-site-intent' } } as SiteDetails );
80
+
81
+ expect( recordTracksEvent ).toHaveBeenCalledWith(
82
+ 'calypso_launchpad_tasklist_viewed',
83
+ expect.objectContaining( {
84
+ site_intent: 'some-site-intent',
85
+ } )
86
+ );
87
+
88
+ expect( recordTracksEvent ).toHaveBeenCalledWith(
89
+ 'calypso_launchpad_task_view',
90
+ expect.objectContaining( {
91
+ site_intent: 'some-site-intent',
92
+ } )
93
+ );
94
+ } );
95
+
96
+ it( 'tracks the click event', () => {
97
+ const tasks = [
98
+ buildTask( { id: 'task-1', completed: true, order: 3 } ),
99
+ buildTask( { id: 'task-2', completed: false, order: 4 } ),
100
+ ];
101
+
102
+ jest.clearAllMocks();
103
+ const { result } = renderHook( () => useTracking( buildDefaultProps() ) );
104
+
105
+ result.current.trackTaskClick( tasks[ 1 ] );
106
+
107
+ expect( recordTracksEvent ).toHaveBeenCalledWith( 'calypso_launchpad_task_clicked', {
108
+ checklist_slug: 'site-setup-checklist',
109
+ task_id: 'task-2',
110
+ checklist_completed: false,
111
+ is_completed: false,
112
+ order: 4,
113
+ context: 'customer-home',
114
+ } );
115
+ } );
116
+ } );
@@ -0,0 +1,287 @@
1
+ /* eslint-disable */
2
+ /* tslint:disable */
3
+
4
+ /**
5
+ * Mock Service Worker (2.1.7).
6
+ * @see https://github.com/mswjs/msw
7
+ * - Please do NOT modify this file.
8
+ * - Please do NOT serve this file on production.
9
+ */
10
+
11
+ const INTEGRITY_CHECKSUM = '223d191a56023cd36aa88c802961b911';
12
+ const IS_MOCKED_RESPONSE = Symbol( 'isMockedResponse' );
13
+ const activeClientIds = new Set();
14
+
15
+ self.addEventListener( 'install', function () {
16
+ self.skipWaiting();
17
+ } );
18
+
19
+ self.addEventListener( 'activate', function ( event ) {
20
+ event.waitUntil( self.clients.claim() );
21
+ } );
22
+
23
+ self.addEventListener( 'message', async function ( event ) {
24
+ const clientId = event.source.id;
25
+
26
+ if ( ! clientId || ! self.clients ) {
27
+ return;
28
+ }
29
+
30
+ const client = await self.clients.get( clientId );
31
+
32
+ if ( ! client ) {
33
+ return;
34
+ }
35
+
36
+ const allClients = await self.clients.matchAll( {
37
+ type: 'window',
38
+ } );
39
+
40
+ switch ( event.data ) {
41
+ case 'KEEPALIVE_REQUEST': {
42
+ sendToClient( client, {
43
+ type: 'KEEPALIVE_RESPONSE',
44
+ } );
45
+ break;
46
+ }
47
+
48
+ case 'INTEGRITY_CHECK_REQUEST': {
49
+ sendToClient( client, {
50
+ type: 'INTEGRITY_CHECK_RESPONSE',
51
+ payload: INTEGRITY_CHECKSUM,
52
+ } );
53
+ break;
54
+ }
55
+
56
+ case 'MOCK_ACTIVATE': {
57
+ activeClientIds.add( clientId );
58
+
59
+ sendToClient( client, {
60
+ type: 'MOCKING_ENABLED',
61
+ payload: true,
62
+ } );
63
+ break;
64
+ }
65
+
66
+ case 'MOCK_DEACTIVATE': {
67
+ activeClientIds.delete( clientId );
68
+ break;
69
+ }
70
+
71
+ case 'CLIENT_CLOSED': {
72
+ activeClientIds.delete( clientId );
73
+
74
+ const remainingClients = allClients.filter( ( client ) => {
75
+ return client.id !== clientId;
76
+ } );
77
+
78
+ // Unregister itself when there are no more clients
79
+ if ( remainingClients.length === 0 ) {
80
+ self.registration.unregister();
81
+ }
82
+
83
+ break;
84
+ }
85
+ }
86
+ } );
87
+
88
+ self.addEventListener( 'fetch', function ( event ) {
89
+ const { request } = event;
90
+ if ( ! request.url.includes( '/wpcom/v2/' ) ) {
91
+ // Do not propagate this event to other listeners (from MSW)
92
+ event.stopImmediatePropagation();
93
+ }
94
+ // Bypass navigation requests.
95
+ if ( request.mode === 'navigate' ) {
96
+ return;
97
+ }
98
+
99
+ // Opening the DevTools triggers the "only-if-cached" request
100
+ // that cannot be handled by the worker. Bypass such requests.
101
+ if ( request.cache === 'only-if-cached' && request.mode !== 'same-origin' ) {
102
+ return;
103
+ }
104
+
105
+ // Bypass all requests when there are no active clients.
106
+ // Prevents the self-unregistered worked from handling requests
107
+ // after it's been deleted (still remains active until the next reload).
108
+ if ( activeClientIds.size === 0 ) {
109
+ return;
110
+ }
111
+
112
+ // Generate unique request ID.
113
+ const requestId = crypto.randomUUID();
114
+ event.respondWith( handleRequest( event, requestId ) );
115
+ } );
116
+
117
+ async function handleRequest( event, requestId ) {
118
+ const client = await resolveMainClient( event );
119
+ const response = await getResponse( event, client, requestId );
120
+
121
+ // Send back the response clone for the "response:*" life-cycle events.
122
+ // Ensure MSW is active and ready to handle the message, otherwise
123
+ // this message will pend indefinitely.
124
+ if ( client && activeClientIds.has( client.id ) ) {
125
+ ( async function () {
126
+ const responseClone = response.clone();
127
+
128
+ sendToClient(
129
+ client,
130
+ {
131
+ type: 'RESPONSE',
132
+ payload: {
133
+ requestId,
134
+ isMockedResponse: IS_MOCKED_RESPONSE in response,
135
+ type: responseClone.type,
136
+ status: responseClone.status,
137
+ statusText: responseClone.statusText,
138
+ body: responseClone.body,
139
+ headers: Object.fromEntries( responseClone.headers.entries() ),
140
+ },
141
+ },
142
+ [ responseClone.body ]
143
+ );
144
+ } )();
145
+ }
146
+
147
+ return response;
148
+ }
149
+
150
+ // Resolve the main client for the given event.
151
+ // Client that issues a request doesn't necessarily equal the client
152
+ // that registered the worker. It's with the latter the worker should
153
+ // communicate with during the response resolving phase.
154
+ async function resolveMainClient( event ) {
155
+ const client = await self.clients.get( event.clientId );
156
+
157
+ if ( client?.frameType === 'top-level' ) {
158
+ return client;
159
+ }
160
+
161
+ const allClients = await self.clients.matchAll( {
162
+ type: 'window',
163
+ } );
164
+
165
+ return allClients
166
+ .filter( ( client ) => {
167
+ // Get only those clients that are currently visible.
168
+ return client.visibilityState === 'visible';
169
+ } )
170
+ .find( ( client ) => {
171
+ // Find the client ID that's recorded in the
172
+ // set of clients that have registered the worker.
173
+ return activeClientIds.has( client.id );
174
+ } );
175
+ }
176
+
177
+ async function getResponse( event, client, requestId ) {
178
+ const { request } = event;
179
+
180
+ // Clone the request because it might've been already used
181
+ // (i.e. its body has been read and sent to the client).
182
+ const requestClone = request.clone();
183
+
184
+ function passthrough() {
185
+ const headers = Object.fromEntries( requestClone.headers.entries() );
186
+
187
+ // Remove internal MSW request header so the passthrough request
188
+ // complies with any potential CORS preflight checks on the server.
189
+ // Some servers forbid unknown request headers.
190
+ delete headers[ 'x-msw-intention' ];
191
+
192
+ return fetch( requestClone, { headers } );
193
+ }
194
+
195
+ // Bypass mocking when the client is not active.
196
+ if ( ! client ) {
197
+ return passthrough();
198
+ }
199
+
200
+ // Bypass initial page load requests (i.e. static assets).
201
+ // The absence of the immediate/parent client in the map of the active clients
202
+ // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
203
+ // and is not ready to handle requests.
204
+ if ( ! activeClientIds.has( client.id ) ) {
205
+ return passthrough();
206
+ }
207
+
208
+ // Bypass requests with the explicit bypass header.
209
+ // Such requests can be issued by "ctx.fetch()".
210
+ const mswIntention = request.headers.get( 'x-msw-intention' );
211
+ if ( [ 'bypass', 'passthrough' ].includes( mswIntention ) ) {
212
+ return passthrough();
213
+ }
214
+
215
+ // Notify the client that a request has been intercepted.
216
+ const requestBuffer = await request.arrayBuffer();
217
+ const clientMessage = await sendToClient(
218
+ client,
219
+ {
220
+ type: 'REQUEST',
221
+ payload: {
222
+ id: requestId,
223
+ url: request.url,
224
+ mode: request.mode,
225
+ method: request.method,
226
+ headers: Object.fromEntries( request.headers.entries() ),
227
+ cache: request.cache,
228
+ credentials: request.credentials,
229
+ destination: request.destination,
230
+ integrity: request.integrity,
231
+ redirect: request.redirect,
232
+ referrer: request.referrer,
233
+ referrerPolicy: request.referrerPolicy,
234
+ body: requestBuffer,
235
+ keepalive: request.keepalive,
236
+ },
237
+ },
238
+ [ requestBuffer ]
239
+ );
240
+
241
+ switch ( clientMessage.type ) {
242
+ case 'MOCK_RESPONSE': {
243
+ return respondWithMock( clientMessage.data );
244
+ }
245
+
246
+ case 'MOCK_NOT_FOUND': {
247
+ return passthrough();
248
+ }
249
+ }
250
+
251
+ return passthrough();
252
+ }
253
+
254
+ function sendToClient( client, message, transferrables = [] ) {
255
+ return new Promise( ( resolve, reject ) => {
256
+ const channel = new MessageChannel();
257
+
258
+ channel.port1.onmessage = ( event ) => {
259
+ if ( event.data && event.data.error ) {
260
+ return reject( event.data.error );
261
+ }
262
+
263
+ resolve( event.data );
264
+ };
265
+
266
+ client.postMessage( message, [ channel.port2 ].concat( transferrables.filter( Boolean ) ) );
267
+ } );
268
+ }
269
+
270
+ async function respondWithMock( response ) {
271
+ // Setting response status code to 0 is a no-op.
272
+ // However, when responding with a "Response.error()", the produced Response
273
+ // instance will have status code set to 0. Since it's not possible to create
274
+ // a Response instance with status code 0, handle that use-case separately.
275
+ if ( response.status === 0 ) {
276
+ return Response.error();
277
+ }
278
+
279
+ const mockedResponse = new Response( response.body, response );
280
+
281
+ Reflect.defineProperty( mockedResponse, IS_MOCKED_RESPONSE, {
282
+ value: true,
283
+ enumerable: true,
284
+ } );
285
+
286
+ return mockedResponse;
287
+ }