dev: core profiler xstate v5 migration (#48135)

* updated core profiler to xstate v5
This commit is contained in:
RJ 2024-06-07 17:06:40 +10:00 committed by GitHub
parent 2583289197
commit 52e2e9f864
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1079 additions and 3557 deletions

View File

@ -1,9 +1,6 @@
module.exports = {
extends: [
'plugin:@woocommerce/eslint-plugin/recommended',
'plugin:xstate/all',
],
plugins: [ 'xstate', 'import' ],
extends: [ 'plugin:@woocommerce/eslint-plugin/recommended' ],
plugins: [ 'import' ],
root: true,
overrides: [
{

View File

@ -7,15 +7,15 @@ import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import { CoreProfilerStateMachineContext } from '..';
import {
CoreProfilerStateMachineContext,
UserProfileEvent,
BusinessInfoEvent,
PluginsLearnMoreLinkClicked,
PluginsLearnMoreLinkClickedEvent,
PluginsInstallationCompletedWithErrorsEvent,
PluginsInstallationCompletedEvent,
PluginsInstallationRequestedEvent,
} from '..';
} from '../events';
import { POSSIBLY_DEFAULT_STORE_NAMES } from '../pages/BusinessInfo';
import {
InstalledPlugin,
@ -23,25 +23,15 @@ import {
} from '../services/installAndActivatePlugins';
import { getPluginTrackKey, getTimeFrame } from '~/utils';
const recordTracksStepViewed = (
_context: unknown,
_event: unknown,
{ action }: { action: unknown }
) => {
const { step } = action as { step: string };
const recordTracksStepViewed = ( _: unknown, params: { step: string } ) => {
recordEvent( 'coreprofiler_step_view', {
step,
step: params.step,
wc_version: getSetting( 'wcVersion' ),
} );
};
const recordTracksStepSkipped = (
_context: unknown,
_event: unknown,
{ action }: { action: unknown }
) => {
const { step } = action as { step: string };
recordEvent( `coreprofiler_${ step }_skip` );
const recordTracksStepSkipped = ( _: unknown, params: { step: string } ) => {
recordEvent( `coreprofiler_${ params.step }_skip` );
};
const recordTracksIntroCompleted = () => {
@ -51,10 +41,11 @@ const recordTracksIntroCompleted = () => {
} );
};
const recordTracksUserProfileCompleted = (
_context: CoreProfilerStateMachineContext,
event: Extract< UserProfileEvent, { type: 'USER_PROFILE_COMPLETED' } >
) => {
const recordTracksUserProfileCompleted = ( {
event,
}: {
event: Extract< UserProfileEvent, { type: 'USER_PROFILE_COMPLETED' } >;
} ) => {
recordEvent( 'coreprofiler_step_complete', {
step: 'user_profile',
wc_version: getSetting( 'wcVersion' ),
@ -76,10 +67,13 @@ const recordTracksSkipBusinessLocationCompleted = () => {
} );
};
const recordTracksIsEmailChanged = (
context: CoreProfilerStateMachineContext,
event: BusinessInfoEvent
) => {
const recordTracksIsEmailChanged = ( {
context,
event,
}: {
context: CoreProfilerStateMachineContext;
event: BusinessInfoEvent;
} ) => {
let emailSource, isEmailChanged;
if ( context.onboardingProfile.store_email ) {
emailSource = 'onboarding_profile_store_email'; // from previous entry
@ -102,10 +96,13 @@ const recordTracksIsEmailChanged = (
} );
};
const recordTracksBusinessInfoCompleted = (
context: CoreProfilerStateMachineContext,
event: Extract< BusinessInfoEvent, { type: 'BUSINESS_INFO_COMPLETED' } >
) => {
const recordTracksBusinessInfoCompleted = ( {
context,
event,
}: {
context: CoreProfilerStateMachineContext;
event: Extract< BusinessInfoEvent, { type: 'BUSINESS_INFO_COMPLETED' } >;
} ) => {
recordEvent( 'coreprofiler_step_complete', {
step: 'business_info',
wc_version: getSetting( 'wcVersion' ),
@ -124,13 +121,14 @@ const recordTracksBusinessInfoCompleted = (
} );
};
const recordTracksPluginsInstallationRequest = (
_context: CoreProfilerStateMachineContext,
const recordTracksPluginsInstallationRequest = ( {
event,
}: {
event: Extract<
PluginsInstallationRequestedEvent,
{ type: 'PLUGINS_INSTALLATION_REQUESTED' }
>
) => {
>;
} ) => {
recordEvent( 'coreprofiler_store_extensions_continue', {
shown: event.payload.pluginsShown || [],
selected: event.payload.pluginsSelected || [],
@ -139,22 +137,21 @@ const recordTracksPluginsInstallationRequest = (
};
const recordTracksPluginsLearnMoreLinkClicked = (
_context: unknown,
_event: PluginsLearnMoreLinkClicked,
{ action }: { action: unknown }
{ event }: { event: PluginsLearnMoreLinkClickedEvent },
params: { step: string }
) => {
const { step } = action as { step: string };
recordEvent( `coreprofiler_${ step }_learn_more_link_clicked`, {
plugin: _event.payload.plugin,
link: _event.payload.learnMoreLink,
recordEvent( `coreprofiler_${ params.step }_learn_more_link_clicked`, {
plugin: event.payload.plugin,
link: event.payload.learnMoreLink,
} );
};
const recordFailedPluginInstallations = (
_context: unknown,
_event: PluginsInstallationCompletedWithErrorsEvent
) => {
const failedExtensions = _event.payload.errors.map(
const recordFailedPluginInstallations = ( {
event,
}: {
event: PluginsInstallationCompletedWithErrorsEvent;
} ) => {
const failedExtensions = event.payload.errors.map(
( error: PluginInstallError ) => getPluginTrackKey( error.plugin )
);
@ -163,7 +160,7 @@ const recordFailedPluginInstallations = (
failed_extensions: failedExtensions,
} );
_event.payload.errors.forEach( ( error: PluginInstallError ) => {
event.payload.errors.forEach( ( error: PluginInstallError ) => {
recordEvent( 'coreprofiler_store_extension_installed_and_activated', {
success: false,
extension: getPluginTrackKey( error.plugin ),
@ -172,12 +169,13 @@ const recordFailedPluginInstallations = (
} );
};
const recordSuccessfulPluginInstallation = (
_context: unknown,
_event: PluginsInstallationCompletedEvent
) => {
const recordSuccessfulPluginInstallation = ( {
event,
}: {
event: PluginsInstallationCompletedEvent;
} ) => {
const installationCompletedResult =
_event.payload.installationCompletedResult;
event.payload.installationCompletedResult;
const trackData: {
success: boolean;

View File

@ -0,0 +1,126 @@
/**
* External dependencies
*/
import { CountryStateOption } from '@woocommerce/onboarding';
/**
* Internal dependencies
*/
import { IndustryChoice } from './pages/BusinessInfo';
import {
InstallationCompletedResult,
PluginInstallError,
} from './services/installAndActivatePlugins';
import { CoreProfilerStateMachineContext } from '.';
export type InitializationCompleteEvent = {
type: 'INITIALIZATION_COMPLETE';
payload: { optInDataSharing: boolean };
};
export type IntroOptInEvent = IntroCompletedEvent | IntroSkippedEvent;
export type IntroCompletedEvent = {
type: 'INTRO_COMPLETED';
payload: { optInDataSharing: boolean };
}; // can be true or false depending on whether the user opted in or not
export type IntroSkippedEvent = {
type: 'INTRO_SKIPPED';
payload: { optInDataSharing: false };
}; // always false for now
export type UserProfileEvent =
| {
type: 'USER_PROFILE_COMPLETED';
payload: {
userProfile: CoreProfilerStateMachineContext[ 'userProfile' ];
};
}
| {
type: 'USER_PROFILE_SKIPPED';
payload: { userProfile: { skipped: true } };
};
export type BusinessInfoEvent = {
type: 'BUSINESS_INFO_COMPLETED';
payload: {
storeName?: string;
industry?: IndustryChoice;
storeLocation: CountryStateOption[ 'key' ];
geolocationOverruled: boolean;
isOptInMarketing: boolean;
storeEmailAddress: string;
};
};
export type BusinessLocationEvent = {
type: 'BUSINESS_LOCATION_COMPLETED';
payload: {
storeLocation: CountryStateOption[ 'key' ];
};
};
export type PluginsInstallationRequestedEvent = {
type: 'PLUGINS_INSTALLATION_REQUESTED';
payload: {
pluginsShown: string[];
pluginsSelected: string[];
pluginsUnselected: string[];
};
};
export type PluginsLearnMoreLinkClickedEvent = {
type: 'PLUGINS_LEARN_MORE_LINK_CLICKED';
payload: {
plugin: string;
learnMoreLink: string;
};
};
export type PluginsPageSkippedEvent = {
type: 'PLUGINS_PAGE_SKIPPED';
};
export type PluginInstalledAndActivatedEvent = {
type: 'PLUGIN_INSTALLED_AND_ACTIVATED';
payload: {
progressPercentage: number;
};
};
export type PluginsInstallationCompletedEvent = {
type: 'PLUGINS_INSTALLATION_COMPLETED';
payload: {
installationCompletedResult: InstallationCompletedResult;
};
};
export type PluginsInstallationCompletedWithErrorsEvent = {
type: 'PLUGINS_INSTALLATION_COMPLETED_WITH_ERRORS';
payload: {
errors: PluginInstallError[];
};
};
export type ExternalUrlUpdateEvent = {
type: 'EXTERNAL_URL_UPDATE';
};
export type RedirectToWooHomeEvent = {
type: 'REDIRECT_TO_WOO_HOME';
};
export type CoreProfilerEvents =
| InitializationCompleteEvent
| IntroOptInEvent
| UserProfileEvent
| BusinessInfoEvent
| BusinessLocationEvent
| PluginsInstallationRequestedEvent
| PluginsLearnMoreLinkClickedEvent
| PluginsPageSkippedEvent
| PluginInstalledAndActivatedEvent
| PluginsInstallationCompletedEvent
| PluginsInstallationCompletedWithErrorsEvent
| ExternalUrlUpdateEvent
| RedirectToWooHomeEvent;

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,8 @@ import clsx from 'clsx';
/**
* Internal dependencies
*/
import { CoreProfilerStateMachineContext, BusinessInfoEvent } from '../index';
import { CoreProfilerStateMachineContext } from '../index';
import { BusinessInfoEvent } from '../events';
import { CountryStateOption } from '../services/country';
import { Heading } from '../components/heading/heading';
import { Navigation } from '../components/navigation/navigation';

View File

@ -10,10 +10,8 @@ import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import {
BusinessLocationEvent,
CoreProfilerStateMachineContext,
} from '../index';
import { CoreProfilerStateMachineContext } from '../index';
import { BusinessLocationEvent } from '../events';
import { CountryStateOption } from '../services/country';
import { Heading } from '../components/heading/heading';
import { Navigation } from '../components/navigation/navigation';

View File

@ -10,7 +10,7 @@ import { Link } from '@woocommerce/components';
/**
* Internal dependencies
*/
import { IntroOptInEvent } from '../index';
import { IntroOptInEvent } from '../events';
import { Heading } from '../components/heading/heading';
import { Navigation } from '../components/navigation/navigation';

View File

@ -11,11 +11,12 @@ import { useState } from 'react';
/**
* Internal dependencies
*/
import { CoreProfilerStateMachineContext } from '../index';
import {
CoreProfilerStateMachineContext,
PluginsLearnMoreLinkClicked,
} from '../index';
import { PluginsInstallationRequestedEvent, PluginsPageSkippedEvent } from '..';
PluginsLearnMoreLinkClickedEvent,
PluginsInstallationRequestedEvent,
PluginsPageSkippedEvent,
} from '../events';
import { Heading } from '../components/heading/heading';
import { Navigation } from '../components/navigation/navigation';
import { PluginCard } from '../components/plugin-card/plugin-card';
@ -45,7 +46,7 @@ export const Plugins = ( {
payload:
| PluginsInstallationRequestedEvent
| PluginsPageSkippedEvent
| PluginsLearnMoreLinkClicked
| PluginsLearnMoreLinkClickedEvent
) => void;
navigationProgress: number;
} ) => {

View File

@ -11,7 +11,8 @@ import clsx from 'clsx';
/**
* Internal dependencies
*/
import { UserProfileEvent, CoreProfilerStateMachineContext } from '../index';
import { CoreProfilerStateMachineContext } from '../index';
import { UserProfileEvent } from '../events';
import { Navigation } from '../components/navigation/navigation';
import { Heading } from '../components/heading/heading';
import { Choice } from '../components/choice/choice';

View File

@ -9,12 +9,13 @@ import {
} from '@woocommerce/data';
import { dispatch } from '@wordpress/data';
import {
DoneActorEvent,
ErrorActorEvent,
assign,
createMachine,
fromPromise,
sendParent,
actions,
DoneInvokeEvent,
} from 'xstate';
} from 'xstate5';
/**
* Internal dependencies
@ -23,9 +24,7 @@ import {
PluginInstalledAndActivatedEvent,
PluginsInstallationCompletedEvent,
PluginsInstallationCompletedWithErrorsEvent,
} from '..';
const { pure } = actions;
} from '../events';
export type InstalledPlugin = {
plugin: string;
@ -61,13 +60,11 @@ const createInstallationCompletedEvent = (
} );
const createPluginInstalledAndActivatedEvent = (
pluginsCount: number,
installedPluginIndex: number
progressPercentage: number
): PluginInstalledAndActivatedEvent => ( {
type: 'PLUGIN_INSTALLED_AND_ACTIVATED',
payload: {
pluginsCount,
installedPluginIndex,
progressPercentage,
},
} );
@ -81,39 +78,53 @@ export type PluginInstallerMachineContext = {
errors: PluginInstallError[];
};
type InstallAndActivateErrorResponse = DoneInvokeEvent< {
export type InstallAndActivateErrorResponse = {
error: string;
message: string;
} >;
};
type InstallAndActivateSuccessResponse = DoneInvokeEvent< {
installed: PluginNames[];
results: Record< PluginNames, boolean >;
install_time: Record< PluginNames, number >;
} >;
type InstallAndActivateSuccessResponse = {
data: {
installed: PluginNames[];
results: Record< PluginNames, boolean >;
install_time: Record< PluginNames, number >;
};
};
export const INSTALLATION_TIMEOUT = 30000; // milliseconds; queue remaining plugin installations to async after this timeout
export const pluginInstallerMachine = createMachine(
{
id: 'plugin-installer',
predictableActionArguments: true,
initial: 'installing',
context: {
selectedPlugins: [] as PluginNames[],
pluginsAvailable: [] as ExtensionList[ 'plugins' ] | [],
pluginsInstallationQueue: [] as PluginNames[],
installedPlugins: [] as InstalledPlugin[],
startTime: 0,
installationDuration: 0,
errors: [] as PluginInstallError[],
} as PluginInstallerMachineContext,
types: {} as {
context: PluginInstallerMachineContext;
},
context: ( {
input,
}: {
input: Pick<
PluginInstallerMachineContext,
'selectedPlugins' | 'pluginsAvailable'
>;
} ) => {
return {
selectedPlugins:
input?.selectedPlugins || ( [] as PluginNames[] ),
pluginsAvailable:
input?.pluginsAvailable ||
( [] as ExtensionList[ 'plugins' ] | [] ),
pluginsInstallationQueue: [] as PluginNames[],
installedPlugins: [] as InstalledPlugin[],
startTime: 0,
installationDuration: 0,
errors: [] as PluginInstallError[],
} as PluginInstallerMachineContext;
},
states: {
installing: {
initial: 'installer',
entry: [
'populateDefaults',
'assignPluginsInstallationQueue',
'assignStartTime',
],
entry: [ 'assignPluginsInstallationQueue', 'assignStartTime' ],
after: {
INSTALLATION_TIMEOUT: 'timedOut',
},
@ -123,7 +134,9 @@ export const pluginInstallerMachine = createMachine(
states: {
installing: {
invoke: {
systemId: 'installPlugin',
src: 'installPlugin',
input: ( { context } ) => context,
onDone: {
actions: [
'assignInstallationSuccessDetails',
@ -145,7 +158,7 @@ export const pluginInstallerMachine = createMachine(
always: [
{
target: 'installing',
cond: 'hasPluginsToInstall',
guard: 'hasPluginsToInstall',
},
{ target: '#installation-finished' },
],
@ -158,14 +171,19 @@ export const pluginInstallerMachine = createMachine(
id: 'installation-finished',
entry: [ 'assignInstallationDuration' ],
always: [
{ target: 'reportErrors', cond: 'hasErrors' },
{ target: 'reportErrors', guard: 'hasErrors' },
{ target: 'reportSuccess' },
],
},
timedOut: {
entry: [ 'assignInstallationDuration' ],
invoke: {
systemId: 'queueRemainingPluginsAsync',
src: 'queueRemainingPluginsAsync',
input: ( { context } ) => ( {
pluginsInstallationQueue:
context.pluginsInstallationQueue,
} ),
onDone: {
target: 'reportSuccess',
},
@ -181,28 +199,19 @@ export const pluginInstallerMachine = createMachine(
},
{
delays: {
INSTALLATION_TIMEOUT: 30000,
INSTALLATION_TIMEOUT,
},
actions: {
// populateDefaults needed because passing context from the parents overrides the
// full context object of the child. Not needed in xstatev5 because it
// becomes a merge
populateDefaults: assign( {
installedPlugins: [],
errors: [],
startTime: 0,
installationDuration: 0,
} ),
assignPluginsInstallationQueue: assign( {
pluginsInstallationQueue: ( ctx ) => {
pluginsInstallationQueue: ( { context } ) => {
// Sort the plugins by install_priority so that the smaller plugins are installed first
// install_priority is set by plugin's size
// Lower install_prioirty means the plugin is smaller
return ctx.selectedPlugins.slice().sort( ( a, b ) => {
const aIndex = ctx.pluginsAvailable.find(
return context.selectedPlugins.slice().sort( ( a, b ) => {
const aIndex = context.pluginsAvailable.find(
( plugin ) => plugin.key === a
);
const bIndex = ctx.pluginsAvailable.find(
const bIndex = context.pluginsAvailable.find(
( plugin ) => plugin.key === b
);
return (
@ -216,79 +225,106 @@ export const pluginInstallerMachine = createMachine(
startTime: () => window.performance.now(),
} ),
assignInstallationDuration: assign( {
installationDuration: ( ctx ) =>
window.performance.now() - ctx.startTime,
installationDuration: ( { context } ) =>
window.performance.now() - context.startTime,
} ),
assignInstallationSuccessDetails: assign( {
installedPlugins: ( ctx, evt ) => {
const plugin = ctx.pluginsInstallationQueue[ 0 ];
installedPlugins: ( { context, event } ) => {
const plugin = context.pluginsInstallationQueue[ 0 ];
return [
...ctx.installedPlugins,
...context.installedPlugins,
{
plugin,
installTime:
(
evt as DoneInvokeEvent< InstallAndActivateSuccessResponse >
).data.data.install_time[ plugin ] || 0,
event as DoneActorEvent< InstallAndActivateSuccessResponse >
).output.data.install_time[ plugin ] || 0,
},
];
},
} ),
assignInstallationErrorDetails: assign( {
errors: ( ctx, evt ) => {
errors: ( { context, event } ) => {
return [
...ctx.errors,
...context.errors,
{
plugin: ctx.pluginsInstallationQueue[ 0 ],
plugin: context.pluginsInstallationQueue[ 0 ],
error: (
evt as DoneInvokeEvent< InstallAndActivateErrorResponse >
).data.data.message,
event as ErrorActorEvent< InstallAndActivateErrorResponse >
).error.message,
},
];
},
} ),
removePluginFromQueue: assign( {
pluginsInstallationQueue: ( ctx ) => {
return ctx.pluginsInstallationQueue.slice( 1 );
pluginsInstallationQueue: ( { context } ) => {
return context.pluginsInstallationQueue.slice( 1 );
},
} ),
updateParentWithPluginProgress: pure( ( ctx ) =>
sendParent(
createPluginInstalledAndActivatedEvent(
ctx.selectedPlugins.length,
ctx.selectedPlugins.length -
ctx.pluginsInstallationQueue.length
updateParentWithPluginProgress: sendParent( ( { context } ) => {
const installedCount =
context.selectedPlugins.length -
context.pluginsInstallationQueue.length;
const pluginsToInstallCount = context.selectedPlugins.length;
const percentageOfPluginsInstalled = Math.round(
( installedCount / pluginsToInstallCount ) * 100
);
const elapsed = window.performance.now() - context.startTime;
const percentageOfTimePassed = Math.round(
( elapsed / INSTALLATION_TIMEOUT ) * 100
);
return createPluginInstalledAndActivatedEvent(
Math.max(
percentageOfPluginsInstalled,
percentageOfTimePassed
)
)
);
} ),
updateParentWithInstallationErrors: sendParent( ( { context } ) =>
createInstallationCompletedWithErrorsEvent( context.errors )
),
updateParentWithInstallationErrors: sendParent( ( ctx ) =>
createInstallationCompletedWithErrorsEvent( ctx.errors )
),
updateParentWithInstallationSuccess: sendParent( ( ctx ) =>
updateParentWithInstallationSuccess: sendParent( ( { context } ) =>
createInstallationCompletedEvent( {
installedPlugins: ctx.installedPlugins,
totalTime: ctx.installationDuration,
installedPlugins: context.installedPlugins,
totalTime: context.installationDuration,
} )
),
},
guards: {
hasErrors: ( ctx ) => ctx.errors.length > 0,
hasPluginsToInstall: ( ctx ) =>
ctx.pluginsInstallationQueue.length > 0,
hasErrors: ( { context } ) => context.errors.length > 0,
hasPluginsToInstall: ( { context } ) =>
context.pluginsInstallationQueue.length > 0,
},
services: {
installPlugin: ( ctx ) => {
return dispatch( PLUGINS_STORE_NAME ).installAndActivatePlugins(
[ ctx.pluginsInstallationQueue[ 0 ] ]
);
},
queueRemainingPluginsAsync: ( ctx ) => {
return dispatch(
ONBOARDING_STORE_NAME
).installAndActivatePluginsAsync(
ctx.pluginsInstallationQueue
);
},
actors: {
installPlugin: fromPromise(
async ( {
input: { pluginsInstallationQueue },
}: {
input: { pluginsInstallationQueue: PluginNames[] };
} ) => {
return dispatch(
PLUGINS_STORE_NAME
).installAndActivatePlugins( [
pluginsInstallationQueue[ 0 ],
] );
}
),
queueRemainingPluginsAsync: fromPromise(
async ( {
input: { pluginsInstallationQueue },
}: {
input: { pluginsInstallationQueue: PluginNames[] };
} ) => {
return dispatch(
ONBOARDING_STORE_NAME
).installAndActivatePluginsAsync(
pluginsInstallationQueue
);
}
),
},
}
);

View File

@ -1,40 +1,25 @@
/**
* External dependencies
*/
import { PluginNames } from '@woocommerce/data';
import { interpret } from 'xstate';
import { createActor, fromPromise, waitFor, SimulatedClock } from 'xstate5';
/**
* Internal dependencies
*/
import {
pluginInstallerMachine,
InstalledPlugin,
PluginInstallError,
} from '../installAndActivatePlugins';
import { pluginInstallerMachine } from '../installAndActivatePlugins';
describe( 'pluginInstallerMachine', () => {
const dispatchInstallPluginMock = jest.fn();
const defaultContext = {
selectedPlugins: [] as PluginNames[],
pluginsInstallationQueue: [] as PluginNames[],
installedPlugins: [] as InstalledPlugin[],
startTime: 0,
installationDuration: 0,
errors: [] as PluginInstallError[],
};
const mockConfig = {
delays: {
INSTALLATION_DELAY: 1000,
INSTALLATION_TIMEOUT: 1000,
},
actions: {
updateParentWithPluginProgress: jest.fn(),
updateParentWithInstallationErrors: jest.fn(),
updateParentWithInstallationSuccess: jest.fn(),
},
services: {
installPlugin: dispatchInstallPluginMock,
queueRemainingPluginsAsync: jest.fn(),
actors: {
queueRemainingPluginsAsync: fromPromise( jest.fn() ),
},
};
@ -42,57 +27,263 @@ describe( 'pluginInstallerMachine', () => {
jest.resetAllMocks();
} );
it( 'when given one plugin it should call the installPlugin service once', ( done ) => {
const machineUnderTest = pluginInstallerMachine
.withConfig( mockConfig )
.withContext( {
...defaultContext,
it( 'when given one plugin it should call the installPlugin service once', async () => {
const mockInstallPlugin = jest.fn();
mockInstallPlugin.mockResolvedValueOnce( {
data: {
install_time: {
'woocommerce-payments': 1000,
},
},
} );
const mockActors = {
installPlugin: fromPromise( mockInstallPlugin ),
};
const machineUnderTest = pluginInstallerMachine.provide( {
...mockConfig,
actors: mockActors,
} );
const service = createActor( machineUnderTest, {
input: {
selectedPlugins: [ 'woocommerce-payments' ],
pluginsAvailable: [],
} );
},
} ).start();
await waitFor( service, ( snap ) => snap.matches( 'reportSuccess' ) );
dispatchInstallPluginMock.mockImplementationOnce( ( context ) => {
expect( context.pluginsInstallationQueue ).toEqual( [
'woocommerce-payments',
] );
return Promise.resolve( {
expect(
mockConfig.actions.updateParentWithInstallationSuccess.mock
.calls[ 0 ][ 0 ]
).toMatchObject( {
context: {
installedPlugins: [
{
plugin: 'woocommerce-payments',
installTime: 1000,
},
],
},
} );
expect( mockInstallPlugin ).toHaveBeenCalledTimes( 1 );
expect(
mockConfig.actions.updateParentWithPluginProgress
).toHaveBeenCalledTimes( 1 );
} );
it( 'when given multiple plugins it should call the installPlugin service the equivalent number of times', async () => {
const mockInstallPlugin = jest.fn();
mockInstallPlugin
.mockResolvedValueOnce( {
data: {
install_time: {
'woocommerce-payments': 1000,
},
},
} )
.mockResolvedValueOnce( {
data: {
install_time: {
jetpack: 1000,
},
},
} );
const mockActors = {
installPlugin: fromPromise( mockInstallPlugin ),
};
const machineUnderTest = pluginInstallerMachine.provide( {
...mockConfig,
actors: mockActors,
} );
const service = interpret( machineUnderTest ).start();
service.onTransition( ( state ) => {
if ( state.matches( 'reportSuccess' ) ) {
expect(
mockConfig.actions.updateParentWithInstallationSuccess
).toHaveBeenCalledWith(
// context param
expect.objectContaining( {
installedPlugins: [
{
plugin: 'woocommerce-payments',
installTime: 1000,
},
],
} ),
// event param
expect.any( Object ),
// meta param
expect.any( Object )
);
done();
}
const service = createActor( machineUnderTest, {
input: {
selectedPlugins: [ 'woocommerce-payments', 'jetpack' ],
pluginsAvailable: [],
},
} ).start();
await waitFor( service, ( snap ) => snap.matches( 'reportSuccess' ) );
expect(
mockConfig.actions.updateParentWithInstallationSuccess.mock
.calls[ 0 ][ 0 ]
).toMatchObject( {
context: {
installedPlugins: [
{
plugin: 'woocommerce-payments',
installTime: 1000,
},
{
plugin: 'jetpack',
installTime: 1000,
},
],
},
} );
expect( mockInstallPlugin ).toHaveBeenCalledTimes( 2 );
expect(
mockConfig.actions.updateParentWithPluginProgress
).toHaveBeenCalledTimes( 2 );
} );
it( 'when a plugin install errors it should report it accordingly', async () => {
const mockInstallPlugin = jest.fn();
mockInstallPlugin
.mockResolvedValueOnce( {
data: {
install_time: {
'woocommerce-payments': 1000,
},
},
} )
.mockRejectedValueOnce( {
message: 'error message installing jetpack',
} );
const mockActors = {
installPlugin: fromPromise( mockInstallPlugin ),
};
const machineUnderTest = pluginInstallerMachine.provide( {
...mockConfig,
actors: mockActors,
} );
const service = createActor( machineUnderTest, {
input: {
selectedPlugins: [ 'woocommerce-payments', 'jetpack' ],
pluginsAvailable: [],
},
} ).start();
await waitFor( service, ( snap ) => snap.matches( 'reportErrors' ) );
expect(
mockConfig.actions.updateParentWithInstallationErrors.mock
.calls[ 0 ][ 0 ]
).toMatchObject( {
context: {
installedPlugins: [
{
plugin: 'woocommerce-payments',
installTime: 1000,
},
],
errors: [
{
error: 'error message installing jetpack',
plugin: 'jetpack',
},
],
},
} );
expect( mockInstallPlugin ).toHaveBeenCalledTimes( 2 );
expect(
mockConfig.actions.updateParentWithPluginProgress
).toHaveBeenCalledTimes( 2 );
} );
it( 'when plugins take longer to install than the timeout, it should queue them async', async () => {
const clock = new SimulatedClock();
const mockInstallPlugin = jest.fn();
mockInstallPlugin
.mockResolvedValueOnce( {
data: {
install_time: {
'woocommerce-payments': 1000,
},
},
} )
.mockImplementationOnce( async () => {
clock.increment( 1500 ); // simulate time passed by 1500ms before this call returns
return {
data: {
install_time: {
jetpack: 1500,
},
},
};
} );
const mockInstallPluginAsync = jest.fn();
mockInstallPluginAsync.mockResolvedValueOnce( {
data: {
job_id: 'foo',
status: 'pending',
plugins: [ { status: 'pending', errors: [] } ],
},
} );
const mockActors = {
installPlugin: fromPromise( mockInstallPlugin ),
queueRemainingPluginsAsync: fromPromise( mockInstallPluginAsync ),
};
const machineUnderTest = pluginInstallerMachine.provide( {
...mockConfig,
actors: mockActors,
} );
const service = createActor( machineUnderTest, {
input: {
selectedPlugins: [
'woocommerce-payments',
'jetpack',
'woocommerce-services',
],
pluginsAvailable: [],
},
clock,
} ).start();
await waitFor( service, ( snap ) => snap.matches( 'reportSuccess' ) );
expect(
mockConfig.actions.updateParentWithInstallationSuccess.mock
.calls[ 0 ][ 0 ]
).toMatchObject( {
context: {
installedPlugins: [
{
plugin: 'woocommerce-payments',
installTime: 1000,
},
],
},
} );
expect( mockInstallPlugin ).toHaveBeenCalledTimes( 2 );
expect(
mockInstallPluginAsync.mock.calls[ 0 ][ 0 ].input
.pluginsInstallationQueue
).toEqual( [ 'jetpack', 'woocommerce-services' ] );
expect( mockInstallPluginAsync ).toHaveBeenCalledTimes( 1 );
expect(
mockInstallPluginAsync.mock.calls[ 0 ][ 0 ].input
.pluginsInstallationQueue
).toEqual( [ 'jetpack', 'woocommerce-services' ] );
expect(
mockConfig.actions.updateParentWithPluginProgress
).toHaveBeenCalledTimes( 1 );
expect(
mockConfig.actions.updateParentWithPluginProgress.mock
.calls[ 0 ][ 0 ]
).toMatchObject( {
context: {
installedPlugins: [
{
plugin: 'woocommerce-payments',
installTime: 1000,
},
],
},
} );
} );
} );
// TODO: write more tests, I ran out of time and it's friday night
// we need tests for:
// 1. when given multiple plugins it should call the installPlugin service multiple times with the right plugins
// 2. when given multiple plugins and a mocked delay using the config, we can mock the installs to take longer than the timeout and then some plugins should not finish installing, then it should add the remaining to async queue
// 3. when a plugin gives an error it should report the error to the parents. we can check this by mocking 'updateParentWithInstallationErrors'
// 4. it should update parent with the plugin installation progress, we can check this by mocking the action 'updateParentWithPluginProgress'

View File

@ -1,158 +0,0 @@
/**
* External dependencies
*/
import {
fireEvent,
render,
RenderResult,
waitFor,
} from '@testing-library/react';
import { createModel } from '@xstate/test';
import { createMachine } from 'xstate';
/**
* Internal dependencies
*/
import { preFetchActions, CoreProfilerController } from '../';
import recordTracksActions from '../actions/tracks';
const preFetchActionsMocks = Object.fromEntries(
Object.entries( preFetchActions ).map( ( [ key ] ) => [ key, jest.fn() ] )
);
const recordTracksActionsMocks = Object.fromEntries(
Object.entries( recordTracksActions ).map( ( [ key ] ) => [
key,
jest.fn(),
] )
);
// mock out the external dependencies which we don't want to test here
const actionOverrides = {
...preFetchActionsMocks,
...recordTracksActionsMocks,
updateQueryStep: jest.fn(),
updateTrackingOption: jest.fn(),
updateOnboardingProfileOption: jest.fn(),
redirectToWooHome: jest.fn(),
};
const servicesOverrides = {
getAllowTrackingOption: jest.fn().mockResolvedValue( 'yes' ),
getCountries: jest
.fn()
.mockResolvedValue( [
{ code: 'US', name: 'United States', states: [] },
] ),
getGeolocation: jest.fn().mockResolvedValue( {} ),
getOnboardingProfileOption: jest.fn().mockResolvedValue( {} ),
};
/**
* Test model's machine is not the application's model!
* In other words, the events and states don't have to match up.
* We can insert other states to test the pre and post-conditions as well.
*/
describe( 'All states in CoreProfilerMachine should be reachable', () => {
beforeEach( () => {
jest.clearAllMocks();
} );
const testMachine = createMachine( {
id: 'coreProfilerTestMachine',
initial: 'initializing',
predictableActionArguments: true,
states: {
initializing: {
on: {
INITIALIZATION_COMPLETE: 'introOptIn',
},
meta: {
test: async ( page: ReturnType< typeof render > ) => {
await page.findByTestId(
'core-profiler-loading-screen'
);
expect( page ).toMatchSnapshot();
},
},
},
introOptIn: {
on: {
INTRO_COMPLETED: 'userProfile',
INTRO_SKIPPED: 'skipFlowBusinessLocation',
},
meta: {
test: async ( page: ReturnType< typeof render > ) => {
expect(
await page.findByTestId(
'core-profiler-intro-opt-in-screen'
)
).toBeVisible();
expect( page ).toMatchSnapshot();
},
},
},
skipFlowBusinessLocation: {
meta: {
test: async ( page: ReturnType< typeof render > ) => {
expect(
await page.findByTestId(
'core-profiler-business-location'
)
).toBeVisible();
expect( page ).toMatchSnapshot();
},
},
},
userProfile: {
meta: {
test: async ( page: ReturnType< typeof render > ) => {
expect(
await page.findByTestId(
'core-profiler-user-profile'
)
).toBeVisible();
expect( page ).toMatchSnapshot();
},
},
},
},
} );
const coreProfilerMachineModel = createModel( testMachine ).withEvents( {
INITIALIZATION_COMPLETE: () => {},
INTRO_SKIPPED: async ( page ) => {
await fireEvent.click(
( page as RenderResult ).getByText( 'Skip guided setup' )
);
},
INTRO_COMPLETED: async ( page ) => {
await fireEvent.click(
( page as RenderResult ).getByText( 'Set up my store' )
);
},
SKIP_FLOW_BUSINESS_LOCATION: () => {},
} );
const testPlans = coreProfilerMachineModel.getSimplePathPlans();
testPlans.forEach( ( plan ) => {
describe( `${ plan.description }`, () => {
afterEach( () => jest.clearAllMocks() );
plan.paths.forEach( ( path ) => {
test( `${ path.description }`, async () => {
const rendered = render(
<CoreProfilerController
// @ts-expect-error -- types are wrong
actionOverrides={ actionOverrides }
servicesOverrides={ servicesOverrides }
/>
);
return waitFor( () => {
// waitFor here so that the render settles before we test
return path.test( rendered );
} );
} );
} );
} );
} );
it( 'coverage', () => {
coreProfilerMachineModel.testCoverage();
} );
} );

View File

@ -465,7 +465,6 @@ export const customizeStoreStateMachineDefinition = createMachine( {
invoke: [
{
src: 'markTaskComplete',
// eslint-disable-next-line xstate/no-ondone-outside-compound-state
onDone: {
target: '#customizeStore.transitionalScreen',
},

View File

@ -42,6 +42,8 @@ This stores the name of the store, which is used in the store header and in the
selling_platforms: ("amazon" | "adobe_commerce" | "big_cartel" | "big_commerce" | "ebay" | "ecwid" | "etsy" | "facebook_marketplace" | "google_shopping" | "pinterest" | "shopify" | "square" | "squarespace" | "wix" | "wordpress")[] | undefined
is_store_country_set: true | false
industry: "clothing_and_accessories" | "health_and_beauty" | "food_and_drink" | "home_furniture_and_garden" | "education_and_learning" | "electronics_and_computers" | "arts_and_crafts" | "sports_and_recreation" | "other"
store_email: string
is_agree_marketing: true | false
}
```
@ -55,6 +57,33 @@ This stores the location that the WooCommerce store believes it is in. This is u
This determines whether we return telemetry to Automattic.
### Currency options
These options are set by looking up the currency data from `@woocommerce/currency` after the user has selected their country.
- `woocommerce_currency`
- `woocommerce_currency_pos`
- `woocommerce_price_thousand_sep`
- `woocommerce_price_decimal_sep`
- `woocommerce_price_num_decimals`
Refer to [Shop currency documentation](https://woocommerce.com/document/shop-currency/) and [class-wc-settings-general.php](https://woocommerce.github.io/code-reference/files/woocommerce-includes-admin-settings-class-wc-settings-general.html) for the full details of the currency settings.
### Coming soon options
These options are set by the API call `coreProfilerCompleted()` on exit of the Core Profiler, and they set the store to private mode until the store is launched.
If the site previously had non-WooCommerce-store related pages, only the store pages will be set to private.
- `woocommerce_coming_soon`: 'yes'
- `woocommerce_store_pages_only`: 'yes' | 'no'
- `woocommerce_private_link`: 'no'
- `woocommerce_share_key`: string (randomly generated by the API)
As this information is not automatically updated, it would be best to refer directly to the data types present in the source code for the most up to date information.
### API Calls
@ -81,6 +110,10 @@ This is used to determine whether the store is connected to Jetpack.
This is used to retrieve the URL that the browser should be redirected to in order to connect to Jetpack.
- `resolveSelect( ONBOARDING_STORE_NAME ).coreProfilerCompleted()`
This is used to indicate to WooCommerce Admin that the Core Profiler has been completed, and this sets the Store's coming-soon mode to true. This hides the store pages from the public until the store is ready.
### Extensions Installation
The Core Profiler has a loading screen that is shown after the Extensions page. This loading screen is meant to hide the installation of Extensions, while also giving the user a sense of progress. At the same time, some extensions take extremely long to install, and thus we have a 30 second timeout.

View File

@ -98,7 +98,7 @@
"react-visibility-sensor": "^5.1.1",
"redux": "^4.2.1",
"xstate": "4.37.1",
"xstate5": "npm:xstate@^5.9.1",
"xstate5": "npm:xstate@^5.13.1",
"zod": "^3.22.4"
},
"devDependencies": {
@ -115,7 +115,7 @@
"@babel/runtime": "^7.23.5",
"@octokit/core": "^3.6.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
"@statelyai/inspect": "^0.2.5",
"@statelyai/inspect": "^0.3.1",
"@testing-library/dom": "8.11.3",
"@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.3",
@ -195,7 +195,6 @@
"eslint-import-resolver-webpack": "^0.13.8",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-xstate": "^1.1.3",
"expose-loader": "^3.1.0",
"fork-ts-checker-webpack-plugin": "^8.0.0",
"fs-extra": "11.1.1",

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Updated Core Profilers XState version to V5

View File

@ -3345,8 +3345,8 @@ importers:
specifier: 4.37.1
version: 4.37.1
xstate5:
specifier: npm:xstate@^5.9.1
version: xstate@5.9.1
specifier: npm:xstate@^5.13.1
version: xstate@5.13.1
zod:
specifier: ^3.22.4
version: 3.22.4
@ -3391,8 +3391,8 @@ importers:
specifier: ^0.5.11
version: 0.5.11(@types/webpack@4.41.38)(react-refresh@0.14.0)(type-fest@2.19.0)(webpack-dev-server@4.15.1(debug@4.3.4)(webpack-cli@4.10.0)(webpack@5.89.0))(webpack-hot-middleware@2.25.4)(webpack@5.89.0(webpack-cli@4.10.0))
'@statelyai/inspect':
specifier: ^0.2.5
version: 0.2.5(ws@8.15.0)(xstate@4.37.1)
specifier: ^0.3.1
version: 0.3.1(ws@8.15.0)(xstate@4.37.1)
'@testing-library/dom':
specifier: 8.11.3
version: 8.11.3
@ -3627,9 +3627,6 @@ importers:
eslint-plugin-react:
specifier: ^7.33.2
version: 7.33.2(eslint@8.55.0)
eslint-plugin-xstate:
specifier: ^1.1.3
version: 1.1.3(eslint@8.55.0)
expose-loader:
specifier: ^3.1.0
version: 3.1.0(webpack@5.89.0(webpack-cli@4.10.0))
@ -8083,8 +8080,8 @@ packages:
resolution: {integrity: sha512-UTX7EKWEf1MQ6+p//4KX7tNTbvzS2W9dbhd2hYk4Lt0mfXf9khe6ZYRYPnV7QBycYcZ3t6FJRJAB55GTcccZ/A==}
engines: {node: '>= 12.13.0', npm: '>= 6.12.0'}
'@statelyai/inspect@0.2.5':
resolution: {integrity: sha512-cm8MuyjLlGA4r9/LzvFLdpAmZg6JYgbmGFc4sHzG1NLHX/kybyLVuYLNc2It6i02tSPmu6eae97JV3nr/hIVIA==}
'@statelyai/inspect@0.3.1':
resolution: {integrity: sha512-KW3owf5UbPs1+/xOGJoSV4D69hP5xTX7PCzARr2R1senwfUIwyGP8yEsB8dvkMvekYvgFS0qa6lmg1eszYr2tw==}
peerDependencies:
xstate: ^5.5.1
@ -14854,12 +14851,6 @@ packages:
peerDependencies:
eslint: '>=5'
eslint-plugin-xstate@1.1.3:
resolution: {integrity: sha512-FF2D/K+M4nenz+OJrNQjqnFwaKU2K8ZW9uJbNU053MnhwO4uVchF/Nj4oZgHRP/AQt9trplMPyHjPwDcVOr8Gw==}
engines: {node: '>=8.0.0'}
peerDependencies:
eslint: ^7.1.0||^8.17.0
eslint-plugin-you-dont-need-lodash-underscore@6.12.0:
resolution: {integrity: sha512-WF4mNp+k2532iswT6iUd1BX6qjd3AV4cFy/09VC82GY9SsRtvkxhUIx7JNGSe0/bLyd57oTr4inPFiIaENXhGw==}
engines: {node: '>=4.0'}
@ -18405,9 +18396,6 @@ packages:
lodash.assign@4.2.0:
resolution: {integrity: sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==}
lodash.camelcase@4.3.0:
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
@ -18477,9 +18465,6 @@ packages:
lodash.shuffle@4.2.0:
resolution: {integrity: sha512-V/rTAABKLFjoecTZjKSv+A1ZomG8hZg8hlgeG6wwQVD9AGv+10zqqSf6mFq2tVA703Zd5R0YhSuSlXA+E/Ei+Q==}
lodash.snakecase@4.1.1:
resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
lodash.sortby@4.7.0:
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
@ -18498,9 +18483,6 @@ packages:
lodash.uniq@4.5.0:
resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==}
lodash.upperfirst@4.3.1:
resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==}
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
@ -21873,6 +21855,7 @@ packages:
rimraf@2.6.3:
resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rimraf@2.7.1:
@ -21971,30 +21954,6 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
sanctuary-def@0.22.0:
resolution: {integrity: sha512-lywS27TfuPHzelMh6M1YWBcB//Lom4Gko/tGjKYbDPkPet1B5KplsSdjY+5sF4mwp6+PDUodsk/Y3Uskrc/ebw==}
sanctuary-either@2.1.0:
resolution: {integrity: sha512-AsQCma2yGAVS7Dlxme/09NZWQfcXBNylVmkuvarp8uOe9otSwClOgQyyePN2OxO4hTf3Au1Ck4ggm+97Je06kA==}
sanctuary-maybe@2.1.0:
resolution: {integrity: sha512-xCmvaEkYaIz5klWUrsgvgORKTL3bVYxATp7HyGBY0ZCu9tHJtSYDTpwnqMkRknHFIU986Kr2M/dJdiFvuc6UCQ==}
sanctuary-pair@2.1.0:
resolution: {integrity: sha512-yY1JzzIJ/Ex7rjN8NmQLB7yv6GVzNeaMvxoYSW8w1ReLQI0n2sGOVCnAVipZuCv7wmfd+BavSXp59rklJeYytw==}
sanctuary-show@2.0.0:
resolution: {integrity: sha512-REj4ZiioUXnDLj6EpJ9HcYDIEGaEexmB9Fg5o6InZR9f0x5PfnnC21QeU9SZ9E7G8zXSZPNjy8VRUK4safbesw==}
sanctuary-type-classes@12.1.0:
resolution: {integrity: sha512-oWP071Q88dEgJwxHLZp8tc7DoS+mWmYhYOuhP85zznPpIxV1ZjJfyRvcR59YCazyFxyFeBif7bJx1giLhDZW0Q==}
sanctuary-type-identifiers@3.0.0:
resolution: {integrity: sha512-YFXYcG0Ura1dSPd/1xLYtE2XAWUEsBHhMTZvYBOvwT8MeFQwdUOCMm2DC+r94z6H93FVq0qxDac8/D7QpJj6Mg==}
sanctuary@3.1.0:
resolution: {integrity: sha512-yTpWmslb5Q2jXcHYeIfCzJksIpe3pngBaFBDpjdPXONWyVSB3hBVDZZ8EmitohaMZqjLB+JREeEL0YOVtdxILA==}
sane@4.1.0:
resolution: {integrity: sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==}
engines: {node: 6.* || 8.* || >= 10.*}
@ -24645,8 +24604,8 @@ packages:
xstate@4.37.1:
resolution: {integrity: sha512-MuB7s01nV5vG2CzaBg2msXLGz7JuS+x/NBkQuZAwgEYCnWA8iQMiRz2VGxD3pcFjZAOih3fOgDD3kDaFInEx+g==}
xstate@5.9.1:
resolution: {integrity: sha512-85edx7iMqRJSRlEPevDwc98EWDYUlT5zEQ54AXuRVR+G76gFbcVTAUdtAeqOVxy8zYnUr9FBB5114iK6enljjw==}
xstate@5.13.1:
resolution: {integrity: sha512-saBUxsTb29Vq8bjq1TjLdGCYs2pneGMzQ7pqQyXh1nqZaSnKHkCkxf3EV+EDYbLnQioxK9HNMYPQrz4whj3RJQ==}
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
@ -32199,7 +32158,7 @@ snapshots:
transitivePeerDependencies:
- debug
'@statelyai/inspect@0.2.5(ws@8.15.0)(xstate@4.37.1)':
'@statelyai/inspect@0.3.1(ws@8.15.0)(xstate@4.37.1)':
dependencies:
fast-safe-stringify: 2.1.1
isomorphic-ws: 5.0.0(ws@8.15.0)
@ -37390,7 +37349,7 @@ snapshots:
'@wordpress/date': 4.47.0
'@wordpress/deprecated': 3.47.0
'@wordpress/dom': 3.47.0
'@wordpress/element': 5.34.0
'@wordpress/element': 5.24.0
'@wordpress/escape-html': 2.47.0
'@wordpress/hooks': 3.47.0
'@wordpress/html-entities': 3.47.0
@ -41240,7 +41199,7 @@ snapshots:
'@wordpress/server-side-render@3.10.0(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react-with-direction@1.4.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2)':
dependencies:
'@babel/runtime': 7.23.6
'@babel/runtime': 7.23.5
'@wordpress/api-fetch': 6.21.0
'@wordpress/blocks': 11.21.0(react@17.0.2)
'@wordpress/components': 19.17.0(@types/react@17.0.71)(react-dom@17.0.2(react@17.0.2))(react-with-direction@1.4.0(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2)
@ -46443,14 +46402,6 @@ snapshots:
dependencies:
eslint: 8.55.0
eslint-plugin-xstate@1.1.3(eslint@8.55.0):
dependencies:
eslint: 8.55.0
lodash.camelcase: 4.3.0
lodash.snakecase: 4.1.1
lodash.upperfirst: 4.3.1
sanctuary: 3.1.0
eslint-plugin-you-dont-need-lodash-underscore@6.12.0:
dependencies:
kebab-case: 1.0.2
@ -52358,8 +52309,6 @@ snapshots:
lodash.assign@4.2.0: {}
lodash.camelcase@4.3.0: {}
lodash.debounce@4.0.8: {}
lodash.differencewith@4.5.0: {}
@ -52419,8 +52368,6 @@ snapshots:
lodash.shuffle@4.2.0: {}
lodash.snakecase@4.1.1: {}
lodash.sortby@4.7.0: {}
lodash.template@4.5.0:
@ -52438,8 +52385,6 @@ snapshots:
lodash.uniq@4.5.0: {}
lodash.upperfirst@4.3.1: {}
lodash@4.17.21: {}
log-symbols@3.0.0:
@ -57524,46 +57469,6 @@ snapshots:
safer-buffer@2.1.2: {}
sanctuary-def@0.22.0:
dependencies:
sanctuary-either: 2.1.0
sanctuary-show: 2.0.0
sanctuary-type-classes: 12.1.0
sanctuary-type-identifiers: 3.0.0
sanctuary-either@2.1.0:
dependencies:
sanctuary-show: 2.0.0
sanctuary-type-classes: 12.1.0
sanctuary-maybe@2.1.0:
dependencies:
sanctuary-show: 2.0.0
sanctuary-type-classes: 12.1.0
sanctuary-pair@2.1.0:
dependencies:
sanctuary-show: 2.0.0
sanctuary-type-classes: 12.1.0
sanctuary-show@2.0.0: {}
sanctuary-type-classes@12.1.0:
dependencies:
sanctuary-type-identifiers: 3.0.0
sanctuary-type-identifiers@3.0.0: {}
sanctuary@3.1.0:
dependencies:
sanctuary-def: 0.22.0
sanctuary-either: 2.1.0
sanctuary-maybe: 2.1.0
sanctuary-pair: 2.1.0
sanctuary-show: 2.0.0
sanctuary-type-classes: 12.1.0
sanctuary-type-identifiers: 3.0.0
sane@4.1.0:
dependencies:
'@cnakazawa/watch': 1.0.4
@ -61604,7 +61509,7 @@ snapshots:
xstate@4.37.1: {}
xstate@5.9.1: {}
xstate@5.13.1: {}
xtend@4.0.2: {}