[Guided onboarding] Add a config for test guide (#143973)

* [Guided onboarding] Add a guide config for testing and update the example plugin to use it instead of search

* [Guided onboarding] Update the API tests with the test guide config

* [Guided onboarding] Address CR comments

* [Guided onboarding] Delete unneeded code in examples

* Update src/plugins/guided_onboarding/public/constants/guides_config/test_guide.ts

Co-authored-by: Alison Goryachev <alisonmllr20@gmail.com>

Co-authored-by: Alison Goryachev <alisonmllr20@gmail.com>
This commit is contained in:
Yulia Čech 2022-10-27 15:19:01 +02:00 committed by GitHub
parent bbbf9f8985
commit 460cf89d5f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 233 additions and 287 deletions

View file

@ -59,7 +59,7 @@ export const GuidedOnboardingExampleApp = (props: GuidedOnboardingExampleAppDeps
<StepOne guidedOnboarding={guidedOnboarding} />
</Route>
<Route exact path="/stepTwo">
<StepTwo guidedOnboarding={guidedOnboarding} />
<StepTwo />
</Route>
<Route exact path="/stepThree">
<StepThree guidedOnboarding={guidedOnboarding} />

View file

@ -259,6 +259,7 @@ export const Main = (props: MainProps) => {
{ value: 'observability', text: 'observability' },
{ value: 'security', text: 'security' },
{ value: 'search', text: 'search' },
{ value: 'testGuide', text: 'test guide' },
]}
value={selectedGuide}
onChange={(e) => {
@ -294,7 +295,7 @@ export const Main = (props: MainProps) => {
<h3>
<FormattedMessage
id="guidedOnboardingExample.main.examplePages.title"
defaultMessage="Example pages"
defaultMessage="Example pages for test guide"
/>
</h3>
</EuiText>
@ -316,6 +317,14 @@ export const Main = (props: MainProps) => {
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={() => history.push('stepThree')}>
<FormattedMessage
id="guidedOnboardingExample.main.examplePages.stepThree.link"
defaultMessage="Step 3"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentBody>
</>

View file

@ -32,7 +32,7 @@ export const StepOne = ({ guidedOnboarding }: GuidedOnboardingExampleAppDeps) =>
const [isTourStepOpen, setIsTourStepOpen] = useState<boolean>(false);
const isTourActive = useObservable(
guidedOnboardingApi!.isGuideStepActive$('search', 'add_data'),
guidedOnboardingApi!.isGuideStepActive$('testGuide', 'step1'),
false
);
useEffect(() => {
@ -45,7 +45,7 @@ export const StepOne = ({ guidedOnboarding }: GuidedOnboardingExampleAppDeps) =>
<h2>
<FormattedMessage
id="guidedOnboardingExample.stepOne.title"
defaultMessage="Example step Add data"
defaultMessage="Example step 1"
/>
</h2>
</EuiTitle>
@ -56,7 +56,7 @@ export const StepOne = ({ guidedOnboarding }: GuidedOnboardingExampleAppDeps) =>
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.stepOne.explanation"
defaultMessage="The code on this page is listening to the guided setup state with a useObservable hook. If the state is set to
Search guide, step Add data, a EUI tour will be displayed, pointing to the button below."
Test guide, step 1, a EUI tour will be displayed, pointing to the button below."
/>
</p>
</EuiText>
@ -72,12 +72,12 @@ export const StepOne = ({ guidedOnboarding }: GuidedOnboardingExampleAppDeps) =>
onFinish={() => setIsTourStepOpen(false)}
step={1}
stepsTotal={1}
title="Step Add data"
title="Step 1"
anchorPosition="rightUp"
>
<EuiButton
onClick={async () => {
await guidedOnboardingApi?.completeGuideStep('search', 'add_data');
await guidedOnboardingApi?.completeGuideStep('testGuide', 'step1');
}}
>
Complete step 1

View file

@ -30,7 +30,7 @@ export const StepThree = (props: StepThreeProps) => {
useEffect(() => {
const subscription = guidedOnboardingApi
?.isGuideStepActive$('search', 'search_experience')
?.isGuideStepActive$('testGuide', 'step3')
.subscribe((isStepActive) => {
setIsTourStepOpen(isStepActive);
});
@ -53,9 +53,17 @@ export const StepThree = (props: StepThreeProps) => {
<EuiText>
<p>
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.stepThree.explanation"
defaultMessage="The code on this page is listening to the guided setup state using an Observable subscription. If the state is set to
Search guide, step Search experience, a EUI tour will be displayed, pointing to the button below."
id="guidedOnboardingExample.guidesSelection.stepThree.explanation1"
defaultMessage="The code on this page is listening to the guided setup state using an Observable subscription.
If the state is set to Test, step 3, a EUI tour will be displayed, pointing to the button below."
/>
</p>
<p>
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.stepTwo.explanation2"
defaultMessage="This page is used for the manual completion of Test guide, step 3. After clicking the page,
the manual completion popover
should appear on the header button 'Setup guide' to open the panel and mark the step done."
/>
</p>
</EuiText>
@ -73,12 +81,12 @@ export const StepThree = (props: StepThreeProps) => {
}}
step={1}
stepsTotal={1}
title="Step Build search experience"
title="Step 3"
anchorPosition="rightUp"
>
<EuiButton
onClick={async () => {
await guidedOnboardingApi?.completeGuideStep('search', 'search_experience');
await guidedOnboardingApi?.completeGuideStep('testGuide', 'step3');
}}
>
Complete step 3

View file

@ -6,37 +6,17 @@
* Side Public License, v 1.
*/
import React, { useEffect, useState } from 'react';
import React from 'react';
import { EuiButton, EuiSpacer, EuiText, EuiTitle, EuiTourStep } from '@elastic/eui';
import { EuiText, EuiTitle } from '@elastic/eui';
import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public/types';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiPageContentHeader_Deprecated as EuiPageContentHeader,
EuiPageContentBody_Deprecated as EuiPageContentBody,
} from '@elastic/eui';
interface StepTwoProps {
guidedOnboarding: GuidedOnboardingPluginStart;
}
export const StepTwo = (props: StepTwoProps) => {
const {
guidedOnboarding: { guidedOnboardingApi },
} = props;
const [isTourStepOpen, setIsTourStepOpen] = useState<boolean>(false);
useEffect(() => {
const subscription = guidedOnboardingApi
?.isGuideStepActive$('search', 'browse_docs')
.subscribe((isStepActive) => {
setIsTourStepOpen(isStepActive);
});
return () => subscription?.unsubscribe();
}, [guidedOnboardingApi]);
export const StepTwo = () => {
return (
<>
<EuiPageContentHeader>
@ -54,36 +34,11 @@ export const StepTwo = (props: StepTwoProps) => {
<p>
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.stepTwo.explanation"
defaultMessage="The code on this page is listening to the guided setup state using an Observable subscription. If the state is set to
Search guide, step Browse documents, a EUI tour will be displayed, pointing to the button below."
defaultMessage="This page is used for the manual completion of Test guide, step 2. The manual completion popover
should appear on the header button 'Setup guide' to open the panel and mark the step done."
/>
</p>
</EuiText>
<EuiSpacer />
<EuiTourStep
content={
<EuiText>
<p>Click this button to complete step 2.</p>
</EuiText>
}
isStepOpen={isTourStepOpen}
minWidth={300}
onFinish={() => {
setIsTourStepOpen(false);
}}
step={1}
stepsTotal={1}
title="Step Browse documents"
anchorPosition="rightUp"
>
<EuiButton
onClick={async () => {
await guidedOnboardingApi?.completeGuideStep('search', 'browse_docs');
}}
>
Complete step 2
</EuiButton>
</EuiTourStep>
</EuiPageContentBody>
</>
);

View file

@ -9,7 +9,6 @@
import React, { ReactNode } from 'react';
import { EuiCard, EuiText, EuiImage } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { GuideId } from '../../types';
type UseCaseConstants = {
[key in UseCase]: {
@ -53,7 +52,7 @@ const constants: UseCaseConstants = {
export type UseCase = 'search' | 'observability' | 'security';
export interface UseCaseCardProps {
useCase: GuideId;
useCase: UseCase;
title: string;
description: string;
footer: ReactNode;

View file

@ -6,13 +6,14 @@
* Side Public License, v 1.
*/
export type GuideId = 'observability' | 'security' | 'search';
export type GuideId = 'observability' | 'security' | 'search' | 'testGuide';
export type ObservabilityStepIds = 'add_data' | 'view_dashboard' | 'tour_observability';
export type SecurityStepIds = 'add_data' | 'rules' | 'alertsCases';
export type SearchStepIds = 'add_data' | 'browse_docs' | 'search_experience';
type ObservabilityStepIds = 'add_data' | 'view_dashboard' | 'tour_observability';
type SecurityStepIds = 'add_data' | 'rules' | 'alertsCases';
type SearchStepIds = 'add_data' | 'browse_docs' | 'search_experience';
type TestGuideIds = 'step1' | 'step2' | 'step3';
export type GuideStepIds = ObservabilityStepIds | SecurityStepIds | SearchStepIds;
export type GuideStepIds = ObservabilityStepIds | SecurityStepIds | SearchStepIds | TestGuideIds;
export interface GuideState {
guideId: GuideId;

View file

@ -10,9 +10,11 @@ import type { GuidesConfig } from '../../types';
import { securityConfig } from './security';
import { observabilityConfig } from './observability';
import { searchConfig } from './search';
import { testGuideConfig } from './test_guide';
export const guidesConfig: GuidesConfig = {
security: securityConfig,
observability: observabilityConfig,
search: searchConfig,
testGuide: testGuideConfig,
};

View file

@ -0,0 +1,68 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { GuideConfig } from '../../types';
export const testGuideConfig: GuideConfig = {
title: 'Test guide for development',
description: `This guide is used to test the guided onboarding UI while in development and to run automated tests for the API and UI components.`,
guideName: 'Testing example',
docs: {
text: 'Testing example docs',
url: 'example.com',
},
steps: [
{
id: 'step1',
title: 'Step 1 (completed via an API request)',
descriptionList: [
`This step is directly completed by clicking the button that uses the API function 'completeGuideStep`,
'Navigate to /guidedOnboardingExample/stepOne to complete the step.',
],
location: {
appID: 'guidedOnboardingExample',
path: 'stepOne',
},
integration: 'testIntegration',
},
{
id: 'step2',
title: 'Step 2 (manual completion after navigation)',
descriptionList: [
'This step is set to ready_to_complete on page navigation.',
'After that click the popover on the guide button in the header and mark the step done',
],
location: {
appID: 'guidedOnboardingExample',
path: 'stepTwo',
},
manualCompletion: {
title: 'Manual completion step title',
description:
'Mark the step complete by opening the panel and clicking the button "Mark done"',
readyToCompleteOnNavigation: true,
},
},
{
id: 'step3',
title: 'Step 3 (manual completion after click)',
descriptionList: [
'This step is completed by clicking a button on the page and then clicking the popover on the guide button in the header and marking the step done',
],
manualCompletion: {
title: 'Manual completion step title',
description:
'Mark the step complete by opening the panel and clicking the button "Mark done"',
},
location: {
appID: 'guidedOnboardingExample',
path: 'stepThree',
},
},
],
};

View file

@ -6,84 +6,78 @@
* Side Public License, v 1.
*/
import type { GuideState } from '@kbn/guided-onboarding';
import type { GuideState, GuideId, GuideStepIds } from '@kbn/guided-onboarding';
export const searchAddDataActiveState: GuideState = {
guideId: 'search',
export const testGuide: GuideId = 'testGuide';
export const testGuideFirstStep: GuideStepIds = 'step1';
export const testGuideManualCompletionStep = 'step2';
export const testGuideLastStep: GuideStepIds = 'step3';
export const testIntegration = 'testIntegration';
export const wrongIntegration = 'notTestIntegration';
export const testGuideStep1ActiveState: GuideState = {
guideId: 'testGuide',
isActive: true,
status: 'in_progress',
steps: [
{
id: 'add_data',
id: 'step1',
status: 'active',
},
{
id: 'browse_docs',
id: 'step2',
status: 'inactive',
},
{
id: 'search_experience',
id: 'step3',
status: 'inactive',
},
],
};
export const securityAddDataInProgressState: GuideState = {
guideId: 'security',
status: 'in_progress',
isActive: true,
export const testGuideStep1InProgressState: GuideState = {
...testGuideStep1ActiveState,
steps: [
{
id: 'add_data',
status: 'in_progress',
},
{
id: 'rules',
status: 'inactive',
},
{
id: 'alertsCases',
status: 'inactive',
id: testGuideStep1ActiveState.steps[0].id,
status: 'in_progress', // update the first step status
},
testGuideStep1ActiveState.steps[1],
testGuideStep1ActiveState.steps[2],
],
};
export const securityRulesActiveState: GuideState = {
guideId: 'security',
isActive: true,
status: 'in_progress',
export const testGuideStep2ActiveState: GuideState = {
...testGuideStep1ActiveState,
steps: [
{
id: 'add_data',
...testGuideStep1ActiveState.steps[0],
status: 'complete',
},
{
id: 'rules',
id: testGuideStep1ActiveState.steps[1].id,
status: 'active',
},
{
id: 'alertsCases',
status: 'inactive',
},
testGuideStep1ActiveState.steps[2],
],
};
export const noGuideActiveState: GuideState = {
guideId: 'security',
status: 'in_progress',
isActive: false,
export const testGuideStep2InProgressState: GuideState = {
...testGuideStep1ActiveState,
steps: [
{
id: 'add_data',
...testGuideStep1ActiveState.steps[0],
status: 'complete',
},
{
id: testGuideStep1ActiveState.steps[1].id,
status: 'in_progress',
},
{
id: 'rules',
status: 'inactive',
},
{
id: 'alertsCases',
status: 'inactive',
},
testGuideStep1ActiveState.steps[2],
],
};
export const testGuideNotActiveState: GuideState = {
...testGuideStep1ActiveState,
isActive: false,
};

View file

@ -12,20 +12,20 @@ import type { GuideState } from '@kbn/guided-onboarding';
import { firstValueFrom, Subscription } from 'rxjs';
import { API_BASE_PATH } from '../../common/constants';
import { guidesConfig } from '../constants/guides_config';
import { ApiService } from './api';
import {
noGuideActiveState,
searchAddDataActiveState,
securityAddDataInProgressState,
securityRulesActiveState,
testGuide,
testGuideFirstStep,
testGuideManualCompletionStep,
testGuideStep1ActiveState,
testGuideStep1InProgressState,
testGuideStep2ActiveState,
testGuideNotActiveState,
testIntegration,
wrongIntegration,
testGuideStep2InProgressState,
} from './api.mocks';
const searchGuide = 'search';
const firstStep = guidesConfig[searchGuide].steps[0].id;
const endpointIntegration = 'endpoint';
const kubernetesIntegration = 'kubernetes';
describe('GuidedOnboarding ApiService', () => {
let httpClient: jest.Mocked<HttpSetup>;
let apiService: ApiService;
@ -34,7 +34,7 @@ describe('GuidedOnboarding ApiService', () => {
beforeEach(() => {
httpClient = httpServiceMock.createStartContract({ basePath: '/base/path' });
httpClient.get.mockResolvedValue({
state: [securityAddDataInProgressState],
state: [testGuideStep1ActiveState],
});
apiService = new ApiService();
apiService.setup(httpClient);
@ -57,10 +57,10 @@ describe('GuidedOnboarding ApiService', () => {
});
it('broadcasts the updated state', async () => {
await apiService.activateGuide(searchGuide, searchAddDataActiveState);
await apiService.activateGuide(testGuide, testGuideStep1ActiveState);
const state = await firstValueFrom(apiService.fetchActiveGuideState$());
expect(state).toEqual(searchAddDataActiveState);
expect(state).toEqual(testGuideStep1ActiveState);
});
});
@ -74,12 +74,12 @@ describe('GuidedOnboarding ApiService', () => {
describe('deactivateGuide', () => {
it('deactivates an existing guide', async () => {
await apiService.deactivateGuide(searchAddDataActiveState);
await apiService.deactivateGuide(testGuideStep1ActiveState);
expect(httpClient.put).toHaveBeenCalledTimes(1);
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
body: JSON.stringify({
...searchAddDataActiveState,
...testGuideStep1ActiveState,
isActive: false,
}),
});
@ -88,17 +88,7 @@ describe('GuidedOnboarding ApiService', () => {
describe('updateGuideState', () => {
it('sends a request to the put API', async () => {
const updatedState: GuideState = {
...searchAddDataActiveState,
steps: [
{
id: searchAddDataActiveState.steps[0].id,
status: 'in_progress', // update the first step status
},
searchAddDataActiveState.steps[1],
searchAddDataActiveState.steps[2],
],
};
const updatedState: GuideState = testGuideStep1InProgressState;
await apiService.updateGuideState(updatedState, false);
expect(httpClient.put).toHaveBeenCalledTimes(1);
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
@ -109,20 +99,11 @@ describe('GuidedOnboarding ApiService', () => {
describe('isGuideStepActive$', () => {
it('returns true if the step has been started', (done) => {
const updatedState: GuideState = {
...searchAddDataActiveState,
steps: [
{
id: searchAddDataActiveState.steps[0].id,
status: 'in_progress',
},
searchAddDataActiveState.steps[1],
searchAddDataActiveState.steps[2],
],
};
const updatedState: GuideState = testGuideStep1InProgressState;
apiService.updateGuideState(updatedState, false);
subscription = apiService
.isGuideStepActive$(searchGuide, firstStep)
.isGuideStepActive$(testGuide, testGuideFirstStep)
.subscribe((isStepActive) => {
if (isStepActive) {
done();
@ -131,9 +112,8 @@ describe('GuidedOnboarding ApiService', () => {
});
it('returns false if the step is not been started', (done) => {
apiService.updateGuideState(searchAddDataActiveState, false);
subscription = apiService
.isGuideStepActive$(searchGuide, firstStep)
.isGuideStepActive$(testGuide, testGuideFirstStep)
.subscribe((isStepActive) => {
if (!isStepActive) {
done();
@ -144,56 +124,44 @@ describe('GuidedOnboarding ApiService', () => {
describe('activateGuide', () => {
it('activates a new guide', async () => {
await apiService.activateGuide(searchGuide);
// update the mock to no active guides
httpClient.get.mockResolvedValue({
state: [],
});
apiService.setup(httpClient);
await apiService.activateGuide(testGuide);
expect(httpClient.put).toHaveBeenCalledTimes(1);
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
body: JSON.stringify({
isActive: true,
status: 'not_started',
steps: [
{
id: 'add_data',
status: 'active',
},
{
id: 'browse_docs',
status: 'inactive',
},
{
id: 'search_experience',
status: 'inactive',
},
],
guideId: searchGuide,
}),
body: JSON.stringify({ ...testGuideStep1ActiveState, status: 'not_started' }),
});
});
it('reactivates a guide that has already been started', async () => {
await apiService.activateGuide(searchGuide, searchAddDataActiveState);
await apiService.activateGuide(testGuide, testGuideStep1ActiveState);
expect(httpClient.put).toHaveBeenCalledTimes(1);
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
body: JSON.stringify(searchAddDataActiveState),
body: JSON.stringify(testGuideStep1ActiveState),
});
});
});
describe('completeGuide', () => {
const readyToCompleteGuideState: GuideState = {
...searchAddDataActiveState,
...testGuideStep1ActiveState,
steps: [
{
id: 'add_data',
...testGuideStep1ActiveState.steps[0],
status: 'complete',
},
{
id: 'browse_docs',
...testGuideStep1ActiveState.steps[1],
status: 'complete',
},
{
id: 'search_experience',
...testGuideStep1ActiveState.steps[2],
status: 'complete',
},
],
@ -204,7 +172,7 @@ describe('GuidedOnboarding ApiService', () => {
});
it('updates the selected guide and marks it as complete', async () => {
await apiService.completeGuide(searchGuide);
await apiService.completeGuide(testGuide);
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
body: JSON.stringify({
@ -222,51 +190,39 @@ describe('GuidedOnboarding ApiService', () => {
it('returns undefined if the selected guide has uncompleted steps', async () => {
const incompleteGuideState: GuideState = {
...searchAddDataActiveState,
...testGuideStep1ActiveState,
steps: [
{
id: 'add_data',
...testGuideStep1ActiveState.steps[0],
status: 'complete',
},
{
id: 'browse_docs',
...testGuideStep1ActiveState.steps[1],
status: 'complete',
},
{
id: 'search_experience',
...testGuideStep1ActiveState.steps[2],
status: 'in_progress',
},
],
};
await apiService.updateGuideState(incompleteGuideState, false);
const completedState = await apiService.completeGuide(searchGuide);
const completedState = await apiService.completeGuide(testGuide);
expect(completedState).not.toBeDefined();
});
});
describe('startGuideStep', () => {
beforeEach(async () => {
await apiService.updateGuideState(searchAddDataActiveState, false);
await apiService.updateGuideState(testGuideStep1ActiveState, false);
});
it('updates the selected step and marks it as in_progress', async () => {
await apiService.startGuideStep(searchGuide, firstStep);
await apiService.startGuideStep(testGuide, testGuideFirstStep);
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
body: JSON.stringify({
...searchAddDataActiveState,
isActive: true,
status: 'in_progress',
steps: [
{
id: searchAddDataActiveState.steps[0].id,
status: 'in_progress',
},
searchAddDataActiveState.steps[1],
searchAddDataActiveState.steps[2],
],
}),
body: JSON.stringify(testGuideStep1InProgressState),
});
});
@ -278,76 +234,35 @@ describe('GuidedOnboarding ApiService', () => {
describe('completeGuideStep', () => {
it(`completes the step when it's in progress`, async () => {
const updatedState: GuideState = {
...searchAddDataActiveState,
steps: [
{
id: searchAddDataActiveState.steps[0].id,
status: 'in_progress', // Mark a step as in_progress in order to test the "completeGuideStep" behavior
},
searchAddDataActiveState.steps[1],
searchAddDataActiveState.steps[2],
],
};
await apiService.updateGuideState(updatedState, false);
await apiService.updateGuideState(testGuideStep1InProgressState, false);
await apiService.completeGuideStep(searchGuide, firstStep);
await apiService.completeGuideStep(testGuide, testGuideFirstStep);
// Once on update, once on complete
expect(httpClient.put).toHaveBeenCalledTimes(2);
// Verify the completed step now has a "complete" status, and the subsequent step is "active"
expect(httpClient.put).toHaveBeenLastCalledWith(`${API_BASE_PATH}/state`, {
body: JSON.stringify({
...updatedState,
steps: [
{
id: searchAddDataActiveState.steps[0].id,
status: 'complete',
},
{
id: searchAddDataActiveState.steps[1].id,
status: 'active',
},
searchAddDataActiveState.steps[2],
],
}),
body: JSON.stringify({ ...testGuideStep2ActiveState }),
});
});
it(`marks the step as 'ready_to_complete' if it's configured for manual completion`, async () => {
const securityRulesInProgressState = {
...securityRulesActiveState,
steps: [
securityRulesActiveState.steps[0],
{
id: securityRulesActiveState.steps[1].id,
status: 'in_progress',
},
securityRulesActiveState.steps[2],
],
};
httpClient.get.mockResolvedValue({
state: [securityRulesInProgressState],
state: [testGuideStep2InProgressState],
});
apiService.setup(httpClient);
await apiService.completeGuideStep('security', 'rules');
await apiService.completeGuideStep(testGuide, testGuideManualCompletionStep);
expect(httpClient.put).toHaveBeenCalledTimes(1);
// Verify the completed step now has a "ready_to_complete" status, and the subsequent step is "inactive"
expect(httpClient.put).toHaveBeenLastCalledWith(`${API_BASE_PATH}/state`, {
body: JSON.stringify({
...securityRulesInProgressState,
...testGuideStep2InProgressState,
steps: [
securityRulesInProgressState.steps[0],
{
id: securityRulesInProgressState.steps[1].id,
status: 'ready_to_complete',
},
{
id: securityRulesInProgressState.steps[2].id,
status: 'inactive',
},
testGuideStep2InProgressState.steps[0],
{ ...testGuideStep2InProgressState.steps[1], status: 'ready_to_complete' },
testGuideStep2InProgressState.steps[2],
],
}),
});
@ -359,12 +274,8 @@ describe('GuidedOnboarding ApiService', () => {
});
it('does nothing if the step is not in progress', async () => {
httpClient.get.mockResolvedValue({
state: [searchAddDataActiveState],
});
apiService.setup(httpClient);
await apiService.completeGuideStep(searchGuide, firstStep);
// by default the state set in beforeEach is test guide, step 1 active
await apiService.completeGuideStep(testGuide, testGuideFirstStep);
expect(httpClient.put).toHaveBeenCalledTimes(0);
});
});
@ -372,11 +283,11 @@ describe('GuidedOnboarding ApiService', () => {
describe('isGuidedOnboardingActiveForIntegration$', () => {
it('returns true if the integration is part of the active step', (done) => {
httpClient.get.mockResolvedValue({
state: [securityAddDataInProgressState],
state: [testGuideStep1InProgressState],
});
apiService.setup(httpClient);
subscription = apiService
.isGuidedOnboardingActiveForIntegration$(endpointIntegration)
.isGuidedOnboardingActiveForIntegration$(testIntegration)
.subscribe((isIntegrationInGuideStep) => {
if (isIntegrationInGuideStep) {
done();
@ -384,13 +295,13 @@ describe('GuidedOnboarding ApiService', () => {
});
});
it('returns false if another integration is part of the active step', (done) => {
it('returns false if the current step has a different integration', (done) => {
httpClient.get.mockResolvedValue({
state: [securityAddDataInProgressState],
state: [testGuideStep1InProgressState],
});
apiService.setup(httpClient);
subscription = apiService
.isGuidedOnboardingActiveForIntegration$(kubernetesIntegration)
.isGuidedOnboardingActiveForIntegration$(wrongIntegration)
.subscribe((isIntegrationInGuideStep) => {
if (!isIntegrationInGuideStep) {
done();
@ -400,11 +311,11 @@ describe('GuidedOnboarding ApiService', () => {
it('returns false if no guide is active', (done) => {
httpClient.get.mockResolvedValue({
state: [noGuideActiveState],
state: [testGuideNotActiveState],
});
apiService.setup(httpClient);
subscription = apiService
.isGuidedOnboardingActiveForIntegration$(endpointIntegration)
.isGuidedOnboardingActiveForIntegration$(testIntegration)
.subscribe((isIntegrationInGuideStep) => {
if (!isIntegrationInGuideStep) {
done();
@ -416,35 +327,35 @@ describe('GuidedOnboarding ApiService', () => {
describe('completeGuidedOnboardingForIntegration', () => {
it(`completes the step if it's active for the integration`, async () => {
httpClient.get.mockResolvedValue({
state: [securityAddDataInProgressState],
state: [testGuideStep1InProgressState],
});
apiService.setup(httpClient);
await apiService.completeGuidedOnboardingForIntegration(endpointIntegration);
await apiService.completeGuidedOnboardingForIntegration(testIntegration);
expect(httpClient.put).toHaveBeenCalledTimes(1);
// this assertion depends on the guides config
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
body: JSON.stringify(securityRulesActiveState),
body: JSON.stringify(testGuideStep2ActiveState),
});
});
it(`does nothing if the step has a different integration`, async () => {
httpClient.get.mockResolvedValue({
state: [securityAddDataInProgressState],
state: [testGuideStep1InProgressState],
});
apiService.setup(httpClient);
await apiService.completeGuidedOnboardingForIntegration(kubernetesIntegration);
await apiService.completeGuidedOnboardingForIntegration(wrongIntegration);
expect(httpClient.put).not.toHaveBeenCalled();
});
it(`does nothing if no guide is active`, async () => {
httpClient.get.mockResolvedValue({
state: [noGuideActiveState],
state: [testGuideNotActiveState],
});
apiService.setup(httpClient);
await apiService.completeGuidedOnboardingForIntegration(endpointIntegration);
await apiService.completeGuidedOnboardingForIntegration(testIntegration);
expect(httpClient.put).not.toHaveBeenCalled();
});
});

View file

@ -147,10 +147,10 @@ export class ApiService implements GuidedOnboardingApi {
});
const updatedGuide: GuideState = {
guideId,
isActive: true,
status: 'not_started',
steps: updatedSteps,
guideId,
};
return await this.updateGuideState(updatedGuide, true);

View file

@ -6,51 +6,50 @@
* Side Public License, v 1.
*/
import { guidesConfig } from '../constants/guides_config';
import { isIntegrationInGuideStep, isLastStep } from './helpers';
import {
noGuideActiveState,
securityAddDataInProgressState,
securityRulesActiveState,
testGuide,
testGuideFirstStep,
testGuideLastStep,
testGuideNotActiveState,
testGuideStep1InProgressState,
testGuideStep2InProgressState,
testIntegration,
wrongIntegration,
} from './api.mocks';
const searchGuide = 'search';
const firstStep = guidesConfig[searchGuide].steps[0].id;
const lastStep = guidesConfig[searchGuide].steps[guidesConfig[searchGuide].steps.length - 1].id;
describe('GuidedOnboarding ApiService helpers', () => {
// this test suite depends on the guides config
describe('isLastStepActive', () => {
it('returns true if the passed params are for the last step', () => {
const result = isLastStep(searchGuide, lastStep);
const result = isLastStep(testGuide, testGuideLastStep);
expect(result).toBe(true);
});
it('returns false if the passed params are not for the last step', () => {
const result = isLastStep(searchGuide, firstStep);
const result = isLastStep(testGuide, testGuideFirstStep);
expect(result).toBe(false);
});
});
describe('isIntegrationInGuideStep', () => {
it('return true if the integration is defined in the guide step config', () => {
const result = isIntegrationInGuideStep(securityAddDataInProgressState, 'endpoint');
const result = isIntegrationInGuideStep(testGuideStep1InProgressState, testIntegration);
expect(result).toBe(true);
});
it('returns false if a different integration is defined in the guide step', () => {
const result = isIntegrationInGuideStep(securityAddDataInProgressState, 'kubernetes');
const result = isIntegrationInGuideStep(testGuideStep1InProgressState, wrongIntegration);
expect(result).toBe(false);
});
it('returns false if no integration is defined in the guide step', () => {
const result = isIntegrationInGuideStep(securityRulesActiveState, 'endpoint');
const result = isIntegrationInGuideStep(testGuideStep2InProgressState, testIntegration);
expect(result).toBe(false);
});
it('returns false if no guide is active', () => {
const result = isIntegrationInGuideStep(noGuideActiveState, 'endpoint');
const result = isIntegrationInGuideStep(testGuideNotActiveState, testIntegration);
expect(result).toBe(false);
});
it('returns false if no integration passed', () => {
const result = isIntegrationInGuideStep(securityAddDataInProgressState);
const result = isIntegrationInGuideStep(testGuideStep1InProgressState);
expect(result).toBe(false);
});
});