dev: core profiler xstate v5 migration (#48135)
* updated core profiler to xstate v5
This commit is contained in:
parent
2583289197
commit
52e2e9f864
|
@ -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: [
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
||||
} ) => {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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'
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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();
|
||||
} );
|
||||
} );
|
|
@ -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',
|
||||
},
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Updated Core Profilers XState version to V5
|
121
pnpm-lock.yaml
121
pnpm-lock.yaml
|
@ -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: {}
|
||||
|
||||
|
|
Loading…
Reference in New Issue