Skip to content

Commit

Permalink
Merge pull request #27 from equinor/feat/TutorialProviderLoadingImpro…
Browse files Browse the repository at this point in the history
…vements

Feat/tutorial provider loading improvements
  • Loading branch information
aslakihle authored Aug 12, 2024
2 parents c11d7ff + d9d49cb commit eda812d
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 44 deletions.
17 changes: 17 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
trailingComma: 'es5'
tabWidth: 2
useTabs: false
semi: true
singleQuote: true
jsxSingleQuote: false
bracketSpacing: true
bracketSameLine: false
arrowParens: 'always'
overrides: [
{
files: '**/*.hbs',
options: {
parser: 'html',
},
},
]
19 changes: 0 additions & 19 deletions .prettierrc.js

This file was deleted.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@equinor/subsurface-app-management",
"version": "1.1.4",
"version": "1.1.5",
"description": "React Typescript components/hooks to communicate with equinor/sam",
"types": "dist/index.d.ts",
"type": "module",
Expand Down
6 changes: 3 additions & 3 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ export { request } from './core/request';
export type { OpenAPIConfig } from './core/OpenAPI';

export type { AmplifyApplication } from './models/AmplifyApplication';
export { ApplicationCategory} from './models/ApplicationCategory';
export type { ContentTab} from './models/ContentTab';
export type { AccessRoles} from './models/AccessRoles';
export { ApplicationCategory } from './models/ApplicationCategory';
export type { ContentTab } from './models/ContentTab';
export type { AccessRoles } from './models/AccessRoles';
export type { FeatureAPIType } from 'src/api/models/FeatureAPIType';
export type { FeatureToggleDto } from './models/FeatureToggleDto';
export type { GraphUser } from './models/GraphUser';
Expand Down
7 changes: 6 additions & 1 deletion src/providers/TutorialProvider/TutorialProvider.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import { Button, Typography } from '@equinor/eds-core-react';
import { StoryFn } from '@storybook/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

import { TutorialProvider } from './TutorialProvider';
import { CustomTutorialComponent } from './TutorialProvider.types';
import { Tutorial } from 'src/api';
import { TutorialProvider } from 'src/providers';
import {
GET_TUTORIALS_FOR_APP,
GET_TUTORIALS_SAS_TOKEN,
} from 'src/providers/TutorialProvider/TutorialProvider.const';

import styled, { keyframes } from 'styled-components';

Expand Down Expand Up @@ -171,6 +175,7 @@ export const Primary: StoryFn = () => {
<TutorialProvider
tutorials={[tutorialForStory]}
customStepComponents={customStepComponents}
ignoredQueryKeys={[GET_TUTORIALS_FOR_APP, GET_TUTORIALS_SAS_TOKEN]}
>
<Button onClick={handleOnStartClick}>Run tutorial</Button>
<Wrapper>
Expand Down
57 changes: 43 additions & 14 deletions src/providers/TutorialProvider/TutorialProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { waitFor } from '@testing-library/react';

import { render, renderHook, screen, userEvent } from '../../tests/test-utils';
import { TutorialProvider } from './TutorialProvider';
import {
DIALOG_EDGE_MARGIN,
GET_TUTORIALS_FOR_APP,
GET_TUTORIALS_SAS_TOKEN,
TUTORIAL_HIGHLIGHTER_DATATEST_ID,
TUTORIAL_LOCALSTORAGE_VALUE_STRING,
} from './TutorialProvider.const';
import { CancelablePromise, Step, Tutorial, TutorialPosition } from 'src/api';
import { TutorialProvider } from 'src/providers';
import { useTutorial } from 'src/providers/TutorialProvider/TutorialProvider.hooks';
import { EnvironmentType } from 'src/types';

Expand All @@ -21,6 +23,7 @@ const TEST_TUTORIAL_SHORT_NAME = 'test-tutorial';
const TEST_TUTORIAL_FROM_BACKEND_SHORT_NAME = 'test-tutorial';
const TEST_TUTORIAL_CUSTOM_STEP_KEY = 'custom-step';
const TEST_TUTORIAL_SAS_TOKEN = 'thisIsASasToken';
const TEST_WRONG_CUSTOM_KEY = 'thisIsTheWrongKey';

const getMarginCss = (type: string) => {
return `margin-${type}: ${DIALOG_EDGE_MARGIN}px;`;
Expand Down Expand Up @@ -106,7 +109,7 @@ vi.mock('src/api/services/TutorialService', () => {
} else {
resolve([fakeTutorial({ tutorialFromBackendHook: true })]);
}
}, 500)
}, 200)
);
}

Expand All @@ -118,7 +121,7 @@ vi.mock('src/api/services/TutorialService', () => {
} else {
resolve(TEST_TUTORIAL_SAS_TOKEN);
}
}, 500)
}, 200)
);
}
}
Expand All @@ -133,6 +136,7 @@ interface GetMemoryRouterProps {
withWrongCustomComponentKeyString?: boolean;
withNoTutorialsOnPath?: boolean;
withPathForTutorialFromHook?: boolean;
withIgnoredQueryKeys?: boolean;
forceInProd?: boolean;
}

Expand All @@ -145,6 +149,7 @@ const getMemoryRouter = (props: GetMemoryRouterProps) => {
withWrongCustomComponentKeyString,
withNoTutorialsOnPath,
withPathForTutorialFromHook,
withIgnoredQueryKeys,
forceInProd,
} = props;
const queryClient = new QueryClient();
Expand Down Expand Up @@ -172,12 +177,17 @@ const getMemoryRouter = (props: GetMemoryRouterProps) => {
: [
{
key: withWrongCustomComponentKeyString
? 'thisIsTheWrongKey'
? TEST_WRONG_CUSTOM_KEY
: TEST_TUTORIAL_CUSTOM_STEP_KEY,
element: <div>{TEST_TUTORIAL_CUSTOM_STEP_KEY}</div>,
},
]
}
ignoredQueryKeys={
withIgnoredQueryKeys
? [GET_TUTORIALS_FOR_APP, GET_TUTORIALS_SAS_TOKEN]
: []
}
>
{tutorialSteps.map((step, index) => {
if (withMissingElementToHighlight && index > 3) return null;
Expand Down Expand Up @@ -245,10 +255,10 @@ describe('TutorialProvider', () => {
const router = getMemoryRouter({ tutorial });
render(<RouterProvider router={router} />);

await waitForBackendCall();
const highlighterElement = screen.queryByTestId(
TUTORIAL_HIGHLIGHTER_DATATEST_ID
);

expect(highlighterElement).toBeInTheDocument();

const skipButton = screen.getByText(/skip/i);
Expand All @@ -264,10 +274,11 @@ describe('TutorialProvider', () => {
const tutorial = fakeTutorial();
const user = userEvent.setup();
render(<RouterProvider router={getMemoryRouter({ tutorial })} />);

await waitForBackendCall();
const highlighterElement = screen.queryByTestId(
TUTORIAL_HIGHLIGHTER_DATATEST_ID
);

expect(highlighterElement).toBeInTheDocument();
const steps = tutorial.steps;

Expand Down Expand Up @@ -309,7 +320,11 @@ describe('TutorialProvider', () => {
withNoCustomSteps: true,
});
const user = userEvent.setup();
render(<RouterProvider router={getMemoryRouter({ tutorial })} />);
render(
<RouterProvider
router={getMemoryRouter({ tutorial, withIgnoredQueryKeys: true })}
/>
);

const stepOneTitle = screen.queryByText(
getStepTitleOrKey(tutorial.steps[0])
Expand Down Expand Up @@ -347,17 +362,18 @@ describe('TutorialProvider', () => {

expect(highlighterElement).not.toBeInTheDocument();
});

test('will show tutorial from useGetTutorialsForApp hook', async () => {
render(
<RouterProvider
router={getMemoryRouter({ withPathForTutorialFromHook: true })}
/>
);

await waitForBackendCall();
const highlighterElement = screen.queryByTestId(
TUTORIAL_HIGHLIGHTER_DATATEST_ID
);

expect(highlighterElement).toBeInTheDocument();
});

Expand Down Expand Up @@ -394,8 +410,8 @@ describe('TutorialProvider', () => {
/>
);

await new Promise((resolve) => setTimeout(resolve, 400));
expect(spy).toHaveBeenCalledTimes(1);
await new Promise((resolve) => setTimeout(resolve, 600));
expect(spy).toHaveBeenCalledTimes(9);

const errorDialogText = screen.getByText(
/there was a problem starting this tutorial./i
Expand All @@ -410,7 +426,7 @@ describe('TutorialProvider', () => {
expect(closeButton).not.toBeInTheDocument();
}, 10000);

test('shows error dialog when having wrong custom components, if tutorial started from searchparam', () => {
test('shows error dialog when having wrong custom components, if tutorial started from searchparam', async () => {
window.localStorage.setItem(
TEST_TUTORIAL_SHORT_NAME,
TUTORIAL_LOCALSTORAGE_VALUE_STRING
Expand All @@ -425,7 +441,14 @@ describe('TutorialProvider', () => {
})}
/>
);
expect(spy).toHaveBeenCalledTimes(1);
await waitForBackendCall();

expect(spy).toHaveBeenCalledWith(
expect.stringContaining('Could not find the custom'),
expect.arrayContaining([TEST_TUTORIAL_CUSTOM_STEP_KEY]),
expect.stringContaining('However in the custom'),
expect.arrayContaining([TEST_WRONG_CUSTOM_KEY])
);

const errorDialogText = screen.getByText(
/there was a problem starting this tutorial./i
Expand All @@ -450,7 +473,10 @@ describe('TutorialProvider', () => {
);

await waitForBackendCall();
expect(spy).toHaveBeenCalledTimes(3); // Two extra for act() warnings
expect(spy).toHaveBeenCalledWith(
expect.stringContaining('Could not find all'),
expect.arrayContaining([null])
);

const errorDialogText = screen.getByText(
/there was a problem starting this tutorial./i
Expand All @@ -472,7 +498,10 @@ describe('TutorialProvider', () => {
);

await waitForBackendCall();
expect(spy).toHaveBeenCalledTimes(3); // Two extra for act() warnings
expect(spy).toHaveBeenCalledWith(
expect.stringContaining('Could not find all'),
expect.arrayContaining([null])
);

const errorDialogText = screen.queryByText(
/there was a problem starting this tutorial./i
Expand Down
41 changes: 37 additions & 4 deletions src/providers/TutorialProvider/TutorialProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
} from 'react';
import { useSearchParams } from 'react-router-dom';

import { useIsFetching } from '@tanstack/react-query';

import { TUTORIAL_SEARCH_PARAM_KEY } from './TutorialProvider.const';
import { CustomTutorialComponent } from './TutorialProvider.types';
import { getAllElementsToHighlight } from './TutorialProvider.utils';
Expand Down Expand Up @@ -55,18 +57,32 @@ interface TutorialProviderProps {
overrideEnvironmentName?: EnvironmentType;
customStepComponents?: CustomTutorialComponent[];
tutorials?: Tutorial[];
ignoredQueryKeys?: string[];
}

/**
* Tutorial provider expects to be within a QueryClientProvider
* @param children Expects to wrap the application globally, typically in a providers file with multiple providers
* @param overrideAppName Overrides the "NAME" env variable, which is used to fetch the relevant tutorials for your app
* @param overrideEnvironmentName Overrides the "ENVIRONMENT_NAME" env variable, which is used for the possibility to hide tutorials in "production"
* @param customStepComponents Adds custom steps components with a key that can be used to link it to a step in a tutorial
* @param tutorials Passing tutorial object directly. This does not replace any tutorials found from API call, but rather is appended to them
* @param ignoredQueryKeys An array of query keys TutorialProviders will not wait to finish loading before looking for elements to highlight
* @constructor
*/

export const TutorialProvider: FC<TutorialProviderProps> = ({
children,
overrideAppName,
overrideEnvironmentName,
customStepComponents,
tutorials,
ignoredQueryKeys,
}) => {
const [activeTutorial, setActiveTutorial] = useState<Tutorial | undefined>(
undefined
);

const [tutorialError, setTutorialError] = useState(false);
const [searchParams, setSearchParams] = useSearchParams();
const [shortNameFromParams, setShortNameFromParams] = useState<
Expand All @@ -77,11 +93,22 @@ export const TutorialProvider: FC<TutorialProviderProps> = ({
HTMLElement[] | undefined
>(undefined);
const [viewportWidth, setViewportWidth] = useState(window.innerWidth);
const appIsFetching =
useIsFetching({
predicate: (query) => {
return !ignoredQueryKeys?.some((ignoredKey) =>
query.queryKey.includes(ignoredKey)
);
},
}) > 0;

const dialogRef = useRef<HTMLDialogElement | null>(null);

const appName = overrideAppName ?? getAppName(import.meta.env.VITE_NAME);
const environmentName =
overrideEnvironmentName ??
getEnvironmentName(import.meta.env.VITE_ENVIRONMENT_NAME);

const currentStepObject = useMemo(() => {
if (!activeTutorial) return;
return activeTutorial.steps.at(currentStep);
Expand Down Expand Up @@ -124,7 +151,7 @@ export const TutorialProvider: FC<TutorialProviderProps> = ({
// Try to find all elements to highlight, and set it to a state for further use.
// If not found, set error state to true, and give console.error
useEffect(() => {
if (!activeTutorial || tutorialError) return;
if (!activeTutorial || tutorialError || appIsFetching) return;

const handleTryToGetElementsAgain = async () => {
// Wait for 300ms before trying again
Expand Down Expand Up @@ -155,13 +182,19 @@ export const TutorialProvider: FC<TutorialProviderProps> = ({
console.error('Error trying to get elements to highlight', error);
});
}
}, [activeTutorial, currentStep, tutorialError, shortNameFromParams]);
}, [
activeTutorial,
currentStep,
tutorialError,
shortNameFromParams,
appIsFetching,
]);

// CUSTOM COMPONENT CHECK
// Check to see if the tutorial has the custom components for any custom steps it has.
// Sets tutorialError to true if it does not find a match for all potential custom steps
useEffect(() => {
if (!activeTutorial || tutorialError) return;
if (!activeTutorial || tutorialError || appIsFetching) return;
const customKeysFromSteps = activeTutorial.steps
.filter((step) => step.key !== undefined && step.key !== null)
// Writing 'customStep.key as string' for coverage, we know its string since we filter out right before the map
Expand Down Expand Up @@ -197,7 +230,7 @@ export const TutorialProvider: FC<TutorialProviderProps> = ({
);
setTutorialError(true);
}
}, [activeTutorial, customStepComponents, tutorialError]);
}, [activeTutorial, appIsFetching, customStepComponents, tutorialError]);

return (
<TutorialContext.Provider
Expand Down

0 comments on commit eda812d

Please sign in to comment.