From f4464af32e839cd5b5543ea4bcc05bca99979810 Mon Sep 17 00:00:00 2001 From: aslakihle Date: Thu, 8 Aug 2024 11:25:43 +0200 Subject: [PATCH 1/6] :sparkles: Added logic for tutorial provider to wait for queries to have loaded --- .../TutorialProvider/TutorialProvider.tsx | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/providers/TutorialProvider/TutorialProvider.tsx b/src/providers/TutorialProvider/TutorialProvider.tsx index 13727ce..bdf41e8 100644 --- a/src/providers/TutorialProvider/TutorialProvider.tsx +++ b/src/providers/TutorialProvider/TutorialProvider.tsx @@ -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'; @@ -55,14 +57,27 @@ 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 = ({ children, overrideAppName, overrideEnvironmentName, customStepComponents, tutorials, + ignoredQueryKeys }) => { const [activeTutorial, setActiveTutorial] = useState( undefined @@ -77,11 +92,17 @@ export const TutorialProvider: FC = ({ 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(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); @@ -124,7 +145,7 @@ export const TutorialProvider: FC = ({ // 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 @@ -155,13 +176,13 @@ export const TutorialProvider: FC = ({ 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 @@ -197,7 +218,7 @@ export const TutorialProvider: FC = ({ ); setTutorialError(true); } - }, [activeTutorial, customStepComponents, tutorialError]); + }, [activeTutorial, appIsFetching, customStepComponents, tutorialError]); return ( Date: Mon, 12 Aug 2024 10:07:37 +0200 Subject: [PATCH 2/6] :bookmark: 1.1.5 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 26cda96..f4ef69f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@equinor/subsurface-app-management", - "version": "1.1.4", + "version": "1.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@equinor/subsurface-app-management", - "version": "1.1.4", + "version": "1.1.5", "license": "ISC", "dependencies": { "@azure/msal-browser": "3.10.0", diff --git a/package.json b/package.json index d5c003f..d1a9a8f 100644 --- a/package.json +++ b/package.json @@ -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", From 9f464423e39136d7957ddd8be000b97a74a89f26 Mon Sep 17 00:00:00 2001 From: aslakihle Date: Mon, 12 Aug 2024 10:08:07 +0200 Subject: [PATCH 3/6] :art: prettier --- src/providers/TutorialProvider/TutorialProvider.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/providers/TutorialProvider/TutorialProvider.tsx b/src/providers/TutorialProvider/TutorialProvider.tsx index bdf41e8..dfae891 100644 --- a/src/providers/TutorialProvider/TutorialProvider.tsx +++ b/src/providers/TutorialProvider/TutorialProvider.tsx @@ -77,11 +77,12 @@ export const TutorialProvider: FC = ({ overrideEnvironmentName, customStepComponents, tutorials, - ignoredQueryKeys + ignoredQueryKeys }) => { const [activeTutorial, setActiveTutorial] = useState( undefined ); + const [tutorialError, setTutorialError] = useState(false); const [searchParams, setSearchParams] = useSearchParams(); const [shortNameFromParams, setShortNameFromParams] = useState< From ae2b0c488b0d106fb440fea1821521bfa648b5be Mon Sep 17 00:00:00 2001 From: aslakihle Date: Mon, 12 Aug 2024 13:39:46 +0200 Subject: [PATCH 4/6] :sparkles: Logic for tutorialProvider to wait for app queries to finish --- .../TutorialProvider.stories.tsx | 7 ++++- .../TutorialProvider/TutorialProvider.tsx | 27 +++++++++++++------ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/providers/TutorialProvider/TutorialProvider.stories.tsx b/src/providers/TutorialProvider/TutorialProvider.stories.tsx index a0059a9..53befcd 100644 --- a/src/providers/TutorialProvider/TutorialProvider.stories.tsx +++ b/src/providers/TutorialProvider/TutorialProvider.stories.tsx @@ -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'; @@ -171,6 +175,7 @@ export const Primary: StoryFn = () => { diff --git a/src/providers/TutorialProvider/TutorialProvider.tsx b/src/providers/TutorialProvider/TutorialProvider.tsx index dfae891..b50204a 100644 --- a/src/providers/TutorialProvider/TutorialProvider.tsx +++ b/src/providers/TutorialProvider/TutorialProvider.tsx @@ -13,7 +13,7 @@ import { } from 'react'; import { useSearchParams } from 'react-router-dom'; -import {useIsFetching} from "@tanstack/react-query"; +import { useIsFetching } from '@tanstack/react-query'; import { TUTORIAL_SEARCH_PARAM_KEY } from './TutorialProvider.const'; import { CustomTutorialComponent } from './TutorialProvider.types'; @@ -57,7 +57,7 @@ interface TutorialProviderProps { overrideEnvironmentName?: EnvironmentType; customStepComponents?: CustomTutorialComponent[]; tutorials?: Tutorial[]; - ignoredQueryKeys?: string[] + ignoredQueryKeys?: string[]; } /** @@ -77,7 +77,7 @@ export const TutorialProvider: FC = ({ overrideEnvironmentName, customStepComponents, tutorials, - ignoredQueryKeys + ignoredQueryKeys, }) => { const [activeTutorial, setActiveTutorial] = useState( undefined @@ -93,9 +93,15 @@ export const TutorialProvider: FC = ({ HTMLElement[] | undefined >(undefined); const [viewportWidth, setViewportWidth] = useState(window.innerWidth); - const appIsFetching = useIsFetching({predicate: (query) => { - return !ignoredQueryKeys?.some(ignoredKey => query.queryKey.includes(ignoredKey)) - }}) > 0 + const appIsFetching = + useIsFetching({ + predicate: (query) => { + return !ignoredQueryKeys?.some((ignoredKey) => + query.queryKey.includes(ignoredKey) + ); + }, + }) > 0; + const dialogRef = useRef(null); const appName = overrideAppName ?? getAppName(import.meta.env.VITE_NAME); @@ -103,7 +109,6 @@ export const TutorialProvider: FC = ({ overrideEnvironmentName ?? getEnvironmentName(import.meta.env.VITE_ENVIRONMENT_NAME); - const currentStepObject = useMemo(() => { if (!activeTutorial) return; return activeTutorial.steps.at(currentStep); @@ -177,7 +182,13 @@ export const TutorialProvider: FC = ({ console.error('Error trying to get elements to highlight', error); }); } - }, [activeTutorial, currentStep, tutorialError, shortNameFromParams, appIsFetching]); + }, [ + activeTutorial, + currentStep, + tutorialError, + shortNameFromParams, + appIsFetching, + ]); // CUSTOM COMPONENT CHECK // Check to see if the tutorial has the custom components for any custom steps it has. From 7daa1111e56539858eb6f9ba1de3a01d87e98e9f Mon Sep 17 00:00:00 2001 From: aslakihle Date: Mon, 12 Aug 2024 13:40:14 +0200 Subject: [PATCH 5/6] :white_check_mark: Test improvements and coverage fix --- .../TutorialProvider.test.tsx | 57 ++++++++++++++----- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/src/providers/TutorialProvider/TutorialProvider.test.tsx b/src/providers/TutorialProvider/TutorialProvider.test.tsx index 0d2d871..04b66a5 100644 --- a/src/providers/TutorialProvider/TutorialProvider.test.tsx +++ b/src/providers/TutorialProvider/TutorialProvider.test.tsx @@ -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'; @@ -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;`; @@ -106,7 +109,7 @@ vi.mock('src/api/services/TutorialService', () => { } else { resolve([fakeTutorial({ tutorialFromBackendHook: true })]); } - }, 500) + }, 200) ); } @@ -118,7 +121,7 @@ vi.mock('src/api/services/TutorialService', () => { } else { resolve(TEST_TUTORIAL_SAS_TOKEN); } - }, 500) + }, 200) ); } } @@ -133,6 +136,7 @@ interface GetMemoryRouterProps { withWrongCustomComponentKeyString?: boolean; withNoTutorialsOnPath?: boolean; withPathForTutorialFromHook?: boolean; + withIgnoredQueryKeys?: boolean; forceInProd?: boolean; } @@ -145,6 +149,7 @@ const getMemoryRouter = (props: GetMemoryRouterProps) => { withWrongCustomComponentKeyString, withNoTutorialsOnPath, withPathForTutorialFromHook, + withIgnoredQueryKeys, forceInProd, } = props; const queryClient = new QueryClient(); @@ -172,12 +177,17 @@ const getMemoryRouter = (props: GetMemoryRouterProps) => { : [ { key: withWrongCustomComponentKeyString - ? 'thisIsTheWrongKey' + ? TEST_WRONG_CUSTOM_KEY : TEST_TUTORIAL_CUSTOM_STEP_KEY, element:
{TEST_TUTORIAL_CUSTOM_STEP_KEY}
, }, ] } + ignoredQueryKeys={ + withIgnoredQueryKeys + ? [GET_TUTORIALS_FOR_APP, GET_TUTORIALS_SAS_TOKEN] + : [] + } > {tutorialSteps.map((step, index) => { if (withMissingElementToHighlight && index > 3) return null; @@ -245,10 +255,10 @@ describe('TutorialProvider', () => { const router = getMemoryRouter({ tutorial }); render(); + await waitForBackendCall(); const highlighterElement = screen.queryByTestId( TUTORIAL_HIGHLIGHTER_DATATEST_ID ); - expect(highlighterElement).toBeInTheDocument(); const skipButton = screen.getByText(/skip/i); @@ -264,10 +274,11 @@ describe('TutorialProvider', () => { const tutorial = fakeTutorial(); const user = userEvent.setup(); render(); + + await waitForBackendCall(); const highlighterElement = screen.queryByTestId( TUTORIAL_HIGHLIGHTER_DATATEST_ID ); - expect(highlighterElement).toBeInTheDocument(); const steps = tutorial.steps; @@ -309,7 +320,11 @@ describe('TutorialProvider', () => { withNoCustomSteps: true, }); const user = userEvent.setup(); - render(); + render( + + ); const stepOneTitle = screen.queryByText( getStepTitleOrKey(tutorial.steps[0]) @@ -347,17 +362,18 @@ describe('TutorialProvider', () => { expect(highlighterElement).not.toBeInTheDocument(); }); + test('will show tutorial from useGetTutorialsForApp hook', async () => { render( ); + await waitForBackendCall(); const highlighterElement = screen.queryByTestId( TUTORIAL_HIGHLIGHTER_DATATEST_ID ); - expect(highlighterElement).toBeInTheDocument(); }); @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 From d9d49cb682bb066d32af03ba36292fd3c4fe7fbd Mon Sep 17 00:00:00 2001 From: aslakihle Date: Mon, 12 Aug 2024 13:40:48 +0200 Subject: [PATCH 6/6] :wrench: Fix prettier config --- .prettierrc | 17 +++++++++++++++++ .prettierrc.js | 19 ------------------- src/api/index.ts | 6 +++--- 3 files changed, 20 insertions(+), 22 deletions(-) create mode 100644 .prettierrc delete mode 100644 .prettierrc.js diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..edc2d68 --- /dev/null +++ b/.prettierrc @@ -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', + }, + }, +] \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js deleted file mode 100644 index 1ff74d1..0000000 --- a/.prettierrc.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = { - trailingComma: 'es5', - tabWidth: 2, - useTabs: false, - semi: true, - singleQuote: true, - jsxSingleQuote: false, - bracketSpacing: true, - bracketSameLine: false, - arrowParens: 'always', - overrides: [ - { - files: '**/*.hbs', - options: { - parser: 'html', - }, - }, - ], -}; diff --git a/src/api/index.ts b/src/api/index.ts index 7b15623..cd4cba7 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -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';