diff --git a/package.json b/package.json index ec31cf6d6..94917c3d0 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "@storybook/testing-library": "^0.0.9", "@testing-library/jest-dom": "^5.5.0", "@testing-library/react": "^10.0.3", + "@testing-library/react-hooks": "^7.0.2", "@types/cheerio": "^0.22.2", "@types/compression-webpack-plugin": "^9.1.1", "@types/d3-shape": "^1.2.6", diff --git a/src/components/Entities/EntityExecutions.tsx b/src/components/Entities/EntityExecutions.tsx index 4c2deb9a8..b2b3780e8 100644 --- a/src/components/Entities/EntityExecutions.tsx +++ b/src/components/Entities/EntityExecutions.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { Typography } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; import { contentMarginGridUnits } from 'common/layout'; -import { fetchStates } from 'components/hooks/types'; import { WaitForData } from 'components/common/WaitForData'; import { ExecutionFilters } from 'components/Executions/ExecutionFilters'; import { useExecutionShowArchivedState } from 'components/Executions/filters/useExecutionArchiveState'; @@ -14,6 +13,7 @@ import { SortDirection } from 'models/AdminEntity/types'; import { ResourceIdentifier } from 'models/Common/types'; import { executionSortFields } from 'models/Execution/constants'; import { compact } from 'lodash'; +import { useOnlyMyExecutionsFilterState } from 'components/Executions/filters/useOnlyMyExecutionsFilterState'; import { executionFilterGenerator } from './generators'; const useStyles = makeStyles((theme: Theme) => ({ @@ -42,6 +42,7 @@ export const EntityExecutions: React.FC = ({ const styles = useStyles(); const filtersState = useWorkflowExecutionFiltersState(); const archivedFilter = useExecutionShowArchivedState(); + const onlyMyExecutionsFilterState = useOnlyMyExecutionsFilterState({}); const sort = { key: executionSortFields.createdAt, @@ -57,7 +58,9 @@ export const EntityExecutions: React.FC = ({ ...baseFilters, ...filtersState.appliedFilters, archivedFilter.getFilter(), + onlyMyExecutionsFilterState.getFilter(), ]); + const executions = useWorkflowExecutions( { domain, project }, { @@ -71,12 +74,6 @@ export const EntityExecutions: React.FC = ({ executions.value = executions.value.filter((item) => chartIds.includes(item.id.name)); } - /** Don't render component until finish fetching user profile */ - const lastIndex = filtersState.filters.length - 1; - if (filtersState.filters[lastIndex].status !== fetchStates.LOADED) { - return null; - } - return ( <> @@ -89,6 +86,7 @@ export const EntityExecutions: React.FC = ({ clearCharts={clearCharts} showArchived={archivedFilter.showArchived} onArchiveFilterChange={archivedFilter.setShowArchived} + onlyMyExecutionsFilterState={onlyMyExecutionsFilterState} /> diff --git a/src/components/Entities/EntityExecutionsBarChart.tsx b/src/components/Entities/EntityExecutionsBarChart.tsx index 2f4b65aeb..f0667fb1f 100644 --- a/src/components/Entities/EntityExecutionsBarChart.tsx +++ b/src/components/Entities/EntityExecutionsBarChart.tsx @@ -121,12 +121,6 @@ export const EntityExecutionsBarChart: React.FC = [onToggle], ); - /** Don't render component until finish fetching user profile */ - const lastIndex = filtersState.filters.length - 1; - if (filtersState.filters[lastIndex].status !== fetchStates.LOADED) { - return null; - } - return ( diff --git a/src/components/Executions/ExecutionFilters.tsx b/src/components/Executions/ExecutionFilters.tsx index 57b2bcde3..2b4ce8d5c 100644 --- a/src/components/Executions/ExecutionFilters.tsx +++ b/src/components/Executions/ExecutionFilters.tsx @@ -31,6 +31,21 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); +interface OnlyMyExecutionsFilterState { + onlyMyExecutionsValue: boolean; + isFilterDisabled: boolean; + onOnlyMyExecutionsFilterChange: (filterOnlyMyExecutions: boolean) => void; +} + +export interface ExecutionFiltersProps { + filters: (FilterState | BooleanFilterState)[]; + chartIds?: string[]; + clearCharts?: () => void; + showArchived?: boolean; + onArchiveFilterChange?: (showArchievedItems: boolean) => void; + onlyMyExecutionsFilterState?: OnlyMyExecutionsFilterState; +} + const RenderFilter: React.FC<{ filter: FilterState }> = ({ filter }) => { const searchFilterState = filter as SearchFilterState; switch (filter.type) { @@ -51,13 +66,14 @@ const RenderFilter: React.FC<{ filter: FilterState }> = ({ filter }) => { * This allows for the consuming code to have direct access to the * current filters without relying on complicated callback arrangements */ -export const ExecutionFilters: React.FC<{ - filters: (FilterState | BooleanFilterState)[]; - chartIds?: string[]; - clearCharts?: () => void; - showArchived?: boolean; - onArchiveFilterChange?: (showArchievedItems: boolean) => void; -}> = ({ filters, chartIds, clearCharts, showArchived, onArchiveFilterChange }) => { +export const ExecutionFilters: React.FC = ({ + filters, + chartIds, + clearCharts, + showArchived, + onArchiveFilterChange, + onlyMyExecutionsFilterState, +}) => { const styles = useStyles(); filters = filters.map((filter) => { @@ -111,14 +127,32 @@ export const ExecutionFilters: React.FC<{ buttonText="Clear Manually Selected Executions" onReset={clearCharts} key="charts" + data-testId="clear-charts" /> )} + {!!onlyMyExecutionsFilterState && ( + + + onlyMyExecutionsFilterState.onOnlyMyExecutionsFilterChange(checked) + } + /> + } + className={styles.checkbox} + label="Only my executions" + /> + + )} {!!onArchiveFilterChange && ( onArchiveFilterChange(checked)} /> } diff --git a/src/components/Executions/__stories__/ExecutionFilters.stories.tsx b/src/components/Executions/__stories__/ExecutionFilters.stories.tsx index 466720e52..62c32bd40 100644 --- a/src/components/Executions/__stories__/ExecutionFilters.stories.tsx +++ b/src/components/Executions/__stories__/ExecutionFilters.stories.tsx @@ -29,6 +29,11 @@ stories.add('Workflow executions - all', () => ( clearCharts={action('clearCharts')} showArchived={false} onArchiveFilterChange={action('onArchiveFilterChange')} + onlyMyExecutionsFilterState={{ + isFilterDisabled: false, + onlyMyExecutionsValue: false, + onOnlyMyExecutionsFilterChange: action('onOnlyMyExecutionsFilterChange'), + }} /> )); stories.add('Workflow executions - minimal', () => ( diff --git a/src/components/Executions/filters/test/useOnlyMyExecutionsFilterState.test.ts b/src/components/Executions/filters/test/useOnlyMyExecutionsFilterState.test.ts new file mode 100644 index 000000000..33a067a78 --- /dev/null +++ b/src/components/Executions/filters/test/useOnlyMyExecutionsFilterState.test.ts @@ -0,0 +1,35 @@ +import { loadedFetchable } from 'components/hooks/__mocks__/fetchableData'; +import { useUserProfile } from 'components/hooks/useUserProfile'; +import { useOnlyMyExecutionsFilterState } from 'components/Executions/filters/useOnlyMyExecutionsFilterState'; +import { UserProfile } from 'models/Common/types'; +import { FetchableData } from 'components/hooks/types'; +import { renderHook } from '@testing-library/react-hooks'; + +jest.mock('components/hooks/useUserProfile'); + +describe('useOnlyMyExecutionsFilterState', () => { + const mockUseRemoteLiteralMap = useUserProfile as jest.Mock>; + mockUseRemoteLiteralMap.mockReturnValue(loadedFetchable(null, jest.fn())); + + describe.each` + isFilterDisabled | initialValue | expected + ${undefined} | ${undefined} | ${{ isFilterDisabled: false, onlyMyExecutionsValue: false }} + ${false} | ${undefined} | ${{ isFilterDisabled: false, onlyMyExecutionsValue: false }} + ${true} | ${undefined} | ${{ isFilterDisabled: true, onlyMyExecutionsValue: false }} + ${undefined} | ${false} | ${{ isFilterDisabled: false, onlyMyExecutionsValue: false }} + ${undefined} | ${true} | ${{ isFilterDisabled: false, onlyMyExecutionsValue: true }} + ${false} | ${false} | ${{ isFilterDisabled: false, onlyMyExecutionsValue: false }} + ${false} | ${true} | ${{ isFilterDisabled: false, onlyMyExecutionsValue: true }} + ${true} | ${false} | ${{ isFilterDisabled: true, onlyMyExecutionsValue: false }} + ${true} | ${true} | ${{ isFilterDisabled: true, onlyMyExecutionsValue: true }} + `('for each case', ({ isFilterDisabled, initialValue, expected }) => { + it(`should return ${JSON.stringify( + expected, + )} when called with isFilterDisabled = ${isFilterDisabled} and initialValue = ${initialValue}`, () => { + const { result } = renderHook(() => + useOnlyMyExecutionsFilterState({ isFilterDisabled, initialValue }), + ); + expect(result.current).toEqual(expect.objectContaining(expected)); + }); + }); +}); diff --git a/src/components/Executions/filters/types.ts b/src/components/Executions/filters/types.ts index 019e7a917..ba729a78b 100644 --- a/src/components/Executions/filters/types.ts +++ b/src/components/Executions/filters/types.ts @@ -22,10 +22,8 @@ export type FilterStateType = 'single' | 'multi' | 'search' | 'boolean'; export interface FilterState { active: boolean; button: FilterButtonState; - hidden?: boolean; label: string; type: FilterStateType; - status?: string; getFilter: () => FilterOperation[]; onReset: () => void; onChange?: (value) => void; diff --git a/src/components/Executions/filters/useCurrentUserOnlyFilterState.ts b/src/components/Executions/filters/useCurrentUserOnlyFilterState.ts deleted file mode 100644 index 21577440d..000000000 --- a/src/components/Executions/filters/useCurrentUserOnlyFilterState.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { FilterOperationName } from 'models/AdminEntity/types'; -import { UserProfile } from 'models/Common/types'; -import { useState } from 'react'; -import { useUserProfile } from 'components/hooks/useUserProfile'; -import { BooleanFilterState } from './types'; -import { useFilterButtonState } from './useFilterButtonState'; - -function getUserId(profile: UserProfile): string { - return profile.sub ? profile.sub : ''; -} - -/** Maintains the state for a button to be used for filtering by user. - */ -export function useCurrentUserOnlyFilterState(): BooleanFilterState { - const profile = useUserProfile(); - const userId = profile.value ? getUserId(profile.value) : ''; - const [active, setActive] = useState(true); - - const button = useFilterButtonState(); - - const getFilter = () => { - return active && userId - ? [ - { - value: userId, - key: 'user', - operation: FilterOperationName.EQ, - }, - ] - : []; - }; - - const onReset = () => setActive(true); - - return { - active, - button, - hidden: !userId, - label: 'Only my executions', - getFilter, - setActive, - onReset, - type: 'boolean', - status: profile.state.value.toString(), - }; -} diff --git a/src/components/Executions/filters/useExecutionFiltersState.ts b/src/components/Executions/filters/useExecutionFiltersState.ts index dbe07e714..71a2ed895 100644 --- a/src/components/Executions/filters/useExecutionFiltersState.ts +++ b/src/components/Executions/filters/useExecutionFiltersState.ts @@ -11,7 +11,6 @@ import { FilterState } from './types'; import { useMultiFilterState } from './useMultiFilterState'; import { useSearchFilterState } from './useSearchFilterState'; import { useSingleFilterState } from './useSingleFilterState'; -import { useCurrentUserOnlyFilterState } from './useCurrentUserOnlyFilterState'; export interface ExecutionFiltersState { appliedFilters: FilterOperation[]; @@ -57,7 +56,6 @@ export function useWorkflowExecutionFiltersState() { label: filterLabels.duration, queryStateKey: 'duration', }), - useCurrentUserOnlyFilterState(), ]); } diff --git a/src/components/Executions/filters/useOnlyMyExecutionsFilterState.ts b/src/components/Executions/filters/useOnlyMyExecutionsFilterState.ts new file mode 100644 index 000000000..24976d453 --- /dev/null +++ b/src/components/Executions/filters/useOnlyMyExecutionsFilterState.ts @@ -0,0 +1,48 @@ +import { useState } from 'react'; +import { FilterOperation, FilterOperationName } from 'models/AdminEntity/types'; +import { useUserProfile } from 'components/hooks/useUserProfile'; + +interface OnlyMyExecutionsFilterState { + onlyMyExecutionsValue: boolean; + isFilterDisabled: boolean; + onOnlyMyExecutionsFilterChange: (newValue: boolean) => void; + getFilter: () => FilterOperation | null; +} + +interface OnlyMyExecutionsFilterStateProps { + isFilterDisabled?: boolean; + initialValue?: boolean; +} + +/** + * Allows to filter executions by Current User Id + */ +export function useOnlyMyExecutionsFilterState({ + isFilterDisabled, + initialValue, +}: OnlyMyExecutionsFilterStateProps): OnlyMyExecutionsFilterState { + const profile = useUserProfile(); + const userId = profile.value?.subject ? profile.value.subject : ''; + const [onlyMyExecutionsValue, setOnlyMyExecutionsValue] = useState( + initialValue ?? false, + ); + + const getFilter = (): FilterOperation | null => { + if (!onlyMyExecutionsValue) { + return null; + } + + return { + key: 'user', + value: userId, + operation: FilterOperationName.EQ, + }; + }; + + return { + onlyMyExecutionsValue, + isFilterDisabled: isFilterDisabled ?? false, + onOnlyMyExecutionsFilterChange: setOnlyMyExecutionsValue, + getFilter, + }; +} diff --git a/src/components/Executions/test/ExecutionFilters.test.tsx b/src/components/Executions/test/ExecutionFilters.test.tsx new file mode 100644 index 000000000..e8e67681e --- /dev/null +++ b/src/components/Executions/test/ExecutionFilters.test.tsx @@ -0,0 +1,160 @@ +import { render } from '@testing-library/react'; +import * as React from 'react'; +import { ExecutionFilters, ExecutionFiltersProps } from '../ExecutionFilters'; +import { FilterState } from '../filters/types'; + +const filterLabel1 = 'Single Filter'; +const filterLabel2 = 'Multiple Filter'; +const filterLabel3 = 'Search Filter'; +const filters: FilterState[] = [ + { + active: true, + label: filterLabel1, + type: 'single', + button: { + open: false, + setOpen: jest.fn(), + onClick: jest.fn(), + }, + getFilter: jest.fn(), + onReset: jest.fn(), + onChange: jest.fn(), + }, + { + active: true, + label: filterLabel2, + type: 'multi', + button: { + open: false, + setOpen: jest.fn(), + onClick: jest.fn(), + }, + getFilter: jest.fn(), + onReset: jest.fn(), + onChange: jest.fn(), + }, + { + active: true, + label: filterLabel3, + type: 'search', + button: { + open: false, + setOpen: jest.fn(), + onClick: jest.fn(), + }, + getFilter: jest.fn(), + onReset: jest.fn(), + onChange: jest.fn(), + }, +]; + +describe('ExecutionFilters', () => { + describe('generic hook filters', () => { + it('should display all provided filters', () => { + const props: ExecutionFiltersProps = { + filters, + }; + const { getAllByRole } = render(); + const renderedFilters = getAllByRole(/button/i); + expect(renderedFilters).toHaveLength(3); + expect(renderedFilters[0]).toHaveTextContent(filterLabel1); + expect(renderedFilters[1]).toHaveTextContent(filterLabel2); + expect(renderedFilters[2]).toHaveTextContent(filterLabel3); + }); + + it('should not display hidden filters provided filters', () => { + const hiddenFilter = { ...filters[2], hidden: true }; + const props: ExecutionFiltersProps = { + filters: [filters[0], filters[1], hiddenFilter], + }; + const { getAllByRole } = render(); + const renderedFilters = getAllByRole(/button/i); + expect(renderedFilters).toHaveLength(2); + expect(renderedFilters[0]).toHaveTextContent(filterLabel1); + expect(renderedFilters[1]).toHaveTextContent(filterLabel2); + }); + }); + + describe('clear executions button', () => { + it('should not be rendered, when chartIds is not provided', () => { + const props: ExecutionFiltersProps = { + filters, + }; + const { queryByTestId } = render(); + expect(queryByTestId('clear-charts')).toBeNull(); + }); + + it('should not be rendered, when chartIds is empty', () => { + const chartIds = []; + const props: ExecutionFiltersProps = { + filters, + chartIds, + clearCharts: jest.fn(), + }; + const { queryByTestId } = render(); + expect(queryByTestId('clear-charts')).toBeNull(); + }); + + it('should be rendered, when chartIds is not empty', () => { + const chartIds = ['id']; + const props: ExecutionFiltersProps = { + filters, + chartIds, + clearCharts: jest.fn(), + }; + const { queryByTestId } = render(); + expect(queryByTestId('clear-charts')).toBeDefined(); + }); + }); + + describe('custom hook filters', () => { + it('should display onlyMyExecution checkbox when corresponding filter state was provided', () => { + const props: ExecutionFiltersProps = { + filters: [], + onlyMyExecutionsFilterState: { + onlyMyExecutionsValue: false, + isFilterDisabled: false, + onOnlyMyExecutionsFilterChange: jest.fn(), + }, + }; + const { getAllByRole } = render(); + const checkboxes = getAllByRole(/checkbox/i) as HTMLInputElement[]; + expect(checkboxes).toHaveLength(1); + expect(checkboxes[0]).toBeTruthy(); + expect(checkboxes[0]).toBeEnabled(); + }); + + it('should display showArchived checkbox when corresponding props were provided', () => { + const props: ExecutionFiltersProps = { + filters: [], + showArchived: true, + onArchiveFilterChange: jest.fn(), + }; + const { getAllByRole } = render(); + const checkboxes = getAllByRole(/checkbox/i) as HTMLInputElement[]; + expect(checkboxes).toHaveLength(1); + expect(checkboxes[0]).toBeTruthy(); + expect(checkboxes[0]).toBeEnabled(); + }); + + it('should display 2 checkbox filters', () => { + const props: ExecutionFiltersProps = { + filters: [], + onlyMyExecutionsFilterState: { + onlyMyExecutionsValue: false, + isFilterDisabled: false, + onOnlyMyExecutionsFilterChange: jest.fn(), + }, + showArchived: true, + onArchiveFilterChange: jest.fn(), + }; + const { getAllByRole } = render(); + const checkboxes = getAllByRole(/checkbox/i) as HTMLInputElement[]; + expect(checkboxes).toHaveLength(2); + expect(checkboxes[0]).toBeTruthy(); + expect(checkboxes[0]).toBeEnabled(); + expect(checkboxes[1]).toBeTruthy(); + expect(checkboxes[1]).toBeEnabled(); + }); + }); +}); diff --git a/src/components/Project/ProjectExecutions.tsx b/src/components/Project/ProjectExecutions.tsx index cd6771cd8..bf5430eba 100644 --- a/src/components/Project/ProjectExecutions.tsx +++ b/src/components/Project/ProjectExecutions.tsx @@ -22,6 +22,7 @@ import { import classNames from 'classnames'; import { useWorkflowExecutions } from 'components/hooks/useWorkflowExecutions'; import { useExecutionShowArchivedState } from 'components/Executions/filters/useExecutionArchiveState'; +import { useOnlyMyExecutionsFilterState } from 'components/Executions/filters/useOnlyMyExecutionsFilterState'; import { WaitForData } from 'components/common/WaitForData'; import { history } from 'routes/history'; import { Routes } from 'routes/routes'; @@ -66,8 +67,13 @@ export const ProjectExecutions: React.FC = ({ const styles = useStyles(); const archivedFilter = useExecutionShowArchivedState(); const filtersState = useWorkflowExecutionFiltersState(); + const onlyMyExecutionsFilterState = useOnlyMyExecutionsFilterState({}); - const allFilters = compact([...filtersState.appliedFilters, archivedFilter.getFilter()]); + const allFilters = compact([ + ...filtersState.appliedFilters, + archivedFilter.getFilter(), + onlyMyExecutionsFilterState.getFilter(), + ]); const config = { sort: defaultSort, filter: allFilters, @@ -128,39 +134,35 @@ export const ProjectExecutions: React.FC = ({ moreItemsAvailable={!!query.hasNextPage} showWorkflowName={true} isFetching={query.isFetching} + data-testid={'workflow-table'} /> ); - /** Don't render component until finish fetching user profile */ - const lastIndex = filtersState.filters.length - 1; - if (filtersState.filters[lastIndex].status === fetchStates.LOADED) { - return ( -
- - Last 100 Executions in the Project - -
- - - -
- - All Executions in the Project - - - {content} + return ( +
+ + Last 100 Executions in the Project + +
+ + +
- ); - } else { - return null; - } + + All Executions in the Project + + + {content} +
+ ); }; diff --git a/src/components/Project/test/ProjectExecutions.test.tsx b/src/components/Project/test/ProjectExecutions.test.tsx index a97d67b14..892341111 100644 --- a/src/components/Project/test/ProjectExecutions.test.tsx +++ b/src/components/Project/test/ProjectExecutions.test.tsx @@ -1,4 +1,4 @@ -import { render, waitFor, fireEvent } from '@testing-library/react'; +import { render, waitFor, fireEvent, within, getByText } from '@testing-library/react'; import { basicPythonWorkflow } from 'mocks/data/fixtures/basicPythonWorkflow'; import { oneFailedTaskWorkflow } from 'mocks/data/fixtures/oneFailedTaskWorkflow'; import { insertFixture } from 'mocks/data/insertFixture'; @@ -21,6 +21,7 @@ import { ProjectExecutions } from '../ProjectExecutions'; import { failedToLoadExecutionsString } from '../constants'; jest.mock('components/Executions/Tables/WorkflowExecutionsTable'); +// jest.mock('components/common/LoadingSpinner'); jest.mock('notistack', () => ({ useSnackbar: () => ({ enqueueSnackbar: jest.fn() }), })); @@ -35,7 +36,7 @@ describe('ProjectExecutions', () => { let mockGetUserProfile: jest.Mock>; const sampleUserProfile: UserProfile = { - sub: 'sub', + subject: 'subject', } as UserProfile; const defaultQueryParams1 = { @@ -44,7 +45,7 @@ describe('ProjectExecutions', () => { }; const defaultQueryParams2 = { - filters: 'eq(user,sub)', + filters: 'eq(user,subject)', [sortQueryKeys.direction]: SortDirection[SortDirection.DESCENDING], [sortQueryKeys.key]: executionSortFields.createdAt, }; @@ -81,6 +82,20 @@ describe('ProjectExecutions', () => { { wrapper: MemoryRouter }, ); + it('should show loading spinner', async () => { + mockGetUserProfile.mockResolvedValue(sampleUserProfile); + const { queryByTestId } = renderView(); + await waitFor(() => {}); + expect(queryByTestId(/loading-spinner/i)).toBeDefined(); + }); + + it('should display WorkflowExecutionsTable and BarChart ', async () => { + mockGetUserProfile.mockResolvedValue(sampleUserProfile); + const { queryByTestId } = renderView(); + await waitFor(() => {}); + expect(queryByTestId('workflow-table')).toBeDefined(); + }); + it('should not display checkbox if user does not login', async () => { const { queryByTestId } = renderView(); await waitFor(() => {}); @@ -88,28 +103,32 @@ describe('ProjectExecutions', () => { expect(queryByTestId(/checkbox/i)).toBeNull(); }); - it('should display checkbox if user login', async () => { + it('should display checkboxes if user login', async () => { mockGetUserProfile.mockResolvedValue(sampleUserProfile); - const { queryByTestId } = renderView(); + const { getAllByRole } = renderView(); await waitFor(() => {}); expect(mockGetUserProfile).toHaveBeenCalled(); - const checkbox = queryByTestId(/checkbox/i) as HTMLInputElement; - expect(checkbox).toBeTruthy(); + // There are 2 checkboxes on a page: 1 - onlyMyExecutions, 2 - show archived, both unchecked by default + const checkboxes = getAllByRole(/checkbox/i) as HTMLInputElement[]; + expect(checkboxes).toHaveLength(2); + expect(checkboxes[0]).toBeTruthy(); + expect(checkboxes[1]).toBeTruthy(); }); /** user doesn't have its own workflow */ - it('should not display workflow if user does not have workflow', async () => { + it('should not display workflow if the user does not have one when filtered onlyMyExecutions', async () => { mockGetUserProfile.mockResolvedValue(sampleUserProfile); - const { getByText, queryByText, queryByTestId } = renderView(); + const { getByText, queryByText, getAllByRole } = renderView(); await waitFor(() => {}); expect(mockGetUserProfile).toHaveBeenCalled(); - const checkbox = queryByTestId(/checkbox/i)?.querySelector('input') as HTMLInputElement; - expect(checkbox).toBeTruthy(); - expect(checkbox?.checked).toEqual(true); - await waitFor(() => expect(queryByText(executions1[0].closure.workflowId.name)).toBeNull()); - fireEvent.click(checkbox); - // in case that user uncheck checkbox, table should display all executions + // There are 2 checkboxes on a page: 1 - onlyMyExecutions, 2 - show archived, both unchecked by default + const checkboxes = getAllByRole(/checkbox/i) as HTMLInputElement[]; + expect(checkboxes[0]).toBeTruthy(); + expect(checkboxes[0]?.checked).toEqual(false); await waitFor(() => expect(getByText(executions1[0].closure.workflowId.name))); + fireEvent.click(checkboxes[0]); + // when user selects checkbox, table should have no executions to display + await waitFor(() => expect(queryByText(executions1[0].closure.workflowId.name)).toBeNull()); }); describe('when initial load fails', () => { diff --git a/src/models/Common/types.ts b/src/models/Common/types.ts index ceace6bdc..39e246024 100644 --- a/src/models/Common/types.ts +++ b/src/models/Common/types.ts @@ -185,7 +185,7 @@ export type IdentifierScope = | Identifier; export interface UserProfile { - sub: string; + subject: string; name: string; preferredUsername: string; givenName: string; diff --git a/yarn.lock b/yarn.lock index 8e0341149..e3c9a77a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3800,6 +3800,17 @@ lodash "^4.17.15" redent "^3.0.0" +"@testing-library/react-hooks@^7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-7.0.2.tgz#3388d07f562d91e7f2431a4a21b5186062ecfee0" + integrity sha512-dYxpz8u9m4q1TuzfcUApqi8iFfR6R0FaMbr2hjZJy1uC8z+bO/K4v8Gs9eogGKYQop7QsrBTFkv/BCF7MzD2Cg== + dependencies: + "@babel/runtime" "^7.12.5" + "@types/react" ">=16.9.0" + "@types/react-dom" ">=16.9.0" + "@types/react-test-renderer" ">=16.9.0" + react-error-boundary "^3.1.0" + "@testing-library/react@^10.0.3": version "10.4.9" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-10.4.9.tgz#9faa29c6a1a217bf8bbb96a28bd29d7a847ca150" @@ -4438,6 +4449,13 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== +"@types/react-dom@>=16.9.0": + version "17.0.14" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.14.tgz#c8f917156b652ddf807711f5becbd2ab018dea9f" + integrity sha512-H03xwEP1oXmSfl3iobtmQ/2dHF5aBHr8aUMwyGZya6OW45G+xtdzmq6HkncefiBt5JU8DVyaWl/nWZbjZCnzAQ== + dependencies: + "@types/react" "*" + "@types/react-dom@^16.9.7": version "16.9.10" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.10.tgz#4485b0bec3d41f856181b717f45fd7831101156f" @@ -4497,6 +4515,13 @@ dependencies: "@types/react" "*" +"@types/react-test-renderer@>=16.9.0": + version "17.0.1" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3120f7d1c157fba9df0118dae20cb0297ee0e06b" + integrity sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw== + dependencies: + "@types/react" "*" + "@types/react-test-renderer@^16.9.0": version "16.9.4" resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.9.4.tgz#377ccf51ea25c656b08aa474fb8194661009b865" @@ -4527,6 +4552,15 @@ "@types/prop-types" "*" csstype "^3.0.2" +"@types/react@>=16.9.0": + version "17.0.41" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.41.tgz#6e179590d276394de1e357b3f89d05d7d3da8b85" + integrity sha512-chYZ9ogWUodyC7VUTRBfblysKLjnohhFY9bGLwvnUFFy48+vB9DikmB3lW0qTFmBcKSzmdglcvkHK71IioOlDA== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/react@^16", "@types/react@^16.9.34": version "16.14.2" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.2.tgz#85dcc0947d0645349923c04ccef6018a1ab7538c" @@ -4540,6 +4574,11 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== +"@types/scheduler@*": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + "@types/serve-static@*", "@types/serve-static@^1.7.31": version "1.13.8" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.8.tgz#851129d434433c7082148574ffec263d58309c46" @@ -16037,6 +16076,13 @@ react-element-to-jsx-string@^14.3.4: is-plain-object "5.0.0" react-is "17.0.2" +react-error-boundary@^3.1.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" + integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== + dependencies: + "@babel/runtime" "^7.12.5" + react-fast-compare@^2.0.2: version "2.0.4" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"