Skip to content

Commit

Permalink
Refetch alignments when Alignment tab is activated
Browse files Browse the repository at this point in the history
closes OUT-5077
flag=outcome_alignment_summary

Test plan:
- Enable Improved Outcomes Management FF
- Enable Outcome Alignment Summary FF
- Go to Course > Outcomes
- Create 2 outcomes, then create a rubric and align
one of the outcomes with the rubric
- Create and assignment and align it with the rubric
- Click on Alignment tab
- Verify that Alignment Summary shows 2 Outcomes, with
50% coverage and 1 avg. alignments per outcome
- Verify that the two outcomes are shown in search results
- Click on Manage tab, create 2 more outcomes but do not
align them and then click on the Alighment tab
- Verify that Alignment Summary shows 4 Outcomes, with
25% coverage and 0.5 avg. alignments per outcome
- Click on Manage tab, delete one of the non aligned
outcomes then click on Alignment tab
- Verify that Alignment Summary shows 3 Outcomes, with
33% coverage and 0.7 avg. alignments per outcomes

Change-Id: I0b0b2d69ef7d8aa92fde4af09f8e25efd23045c8
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/297188
Tested-by: Service Cloud Jenkins <[email protected]>
Reviewed-by: Jason Anderson <[email protected]>
Reviewed-by: Angela Gomba <[email protected]>
QA-Review: Angela Gomba <[email protected]>
Product-Review: Ben Friedman <[email protected]>
  • Loading branch information
instout committed Jul 29, 2022
1 parent e286ed5 commit ac39379
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ const I18n = useI18nScope('AlignmentSummary')

const AlignmentOutcomeItemList = ({rootGroup, loading, loadMore, scrollContainer}) => {
const outcomes = rootGroup?.outcomes
const outcomesCount = rootGroup?.outcomesCount
const hasMore = outcomes?.pageInfo?.hasNextPage
const hasOutcomes = outcomes?.edges?.length > 0
const hasMoreOutcomes = outcomes?.pageInfo?.hasNextPage

const renderSearchLoader = () => (
<div style={{textAlign: 'center'}} data-testid="loading">
<Spinner renderTitle={I18n.t('Loading')} size="large" />
Expand Down Expand Up @@ -63,11 +64,9 @@ const AlignmentOutcomeItemList = ({rootGroup, loading, loadMore, scrollContainer

return (
<View as="div" minWidth="300px" data-testid="alignment-items-list-container">
{outcomesCount === 0 ? (
renderNoSearchResults()
) : (
{hasOutcomes ? (
<InfiniteScroll
hasMore={hasMore}
hasMore={hasMoreOutcomes}
loadMore={loadMore}
loader={renderInfiniteScrollLoader()}
scrollContainer={scrollContainer}
Expand All @@ -83,6 +82,8 @@ const AlignmentOutcomeItemList = ({rootGroup, loading, loadMore, scrollContainer
))}
</View>
</InfiniteScroll>
) : (
renderNoSearchResults()
)}
</View>
)
Expand Down
71 changes: 40 additions & 31 deletions ui/shared/outcomes/mocks/Management.js
Original file line number Diff line number Diff line change
Expand Up @@ -2478,31 +2478,34 @@ export const courseAlignmentStatsMocks = ({
alignedOutcomes = 1,
totalAlignments = 4,
totalArtifacts = 5,
alignedArtifacts = 4
alignedArtifacts = 4,
refetchIncrement = 10
} = {}) => {
const result = {
const returnResult = (inc = 0) => ({
data: {
course: {
outcomeAlignmentStats: {
totalOutcomes,
alignedOutcomes,
totalAlignments,
totalArtifacts,
alignedArtifacts,
totalOutcomes: totalOutcomes + inc,
alignedOutcomes: alignedOutcomes + inc,
totalAlignments: totalAlignments + inc,
totalArtifacts: totalArtifacts + inc,
alignedArtifacts: alignedArtifacts + inc,
__typename: 'CourseOutcomeAlignmentStats'
},
__typename: 'Course'
}
}
}
})

return [
{
request: {
query: COURSE_ALIGNMENT_STATS,
variables: {id}
},
result
result: returnResult(),
// for testing data refetch
newData: () => returnResult(refetchIncrement)
}
]
}
Expand Down Expand Up @@ -2542,18 +2545,20 @@ export const courseAlignmentMocks = ({
generateAlignment({id: `${el + 1}`, title: `Alignment ${el + 1}`})
)

const generateOutcomeNode = (outcomeId, withAlignments = true) => ({
const generateOutcomeNode = (outcomeId, withAlignments = true, isRefetch = false) => ({
_id: outcomeId,
title: `Outcome ${outcomeId}${withAlignments ? ' with alignments' : ''}`,
title: `Outcome ${outcomeId}${withAlignments ? ' with alignments' : ''}${
isRefetch ? ' - Refetched' : ''
}`,
description: `Outcome ${outcomeId} description`,
__typename: 'LearningOutcome',
alignments: withAlignments ? generateAlignments() : null
})

const generateEdges = outcomeIds => {
const generateEdges = (outcomeIds, isRefetch = false) => {
const edges = (testSearch = false) =>
(outcomeIds || []).map(id => ({
node: generateOutcomeNode(id, !!(id % 2 !== 0 || testSearch)),
node: generateOutcomeNode(id, !!(id % 2 !== 0 || testSearch), isRefetch),
__typename: 'ContentTag'
}))
if (searchFilter === 'WITH_ALIGNMENTS')
Expand All @@ -2572,30 +2577,34 @@ export const courseAlignmentMocks = ({
}
if (searchQuery) variables.searchQuery = searchQuery

const returnResult = (isRefetch = false) => ({
data: {
group: {
_id: groupId,
outcomesCount: numOfOutcomes,
__typename: 'LearningOutcomeGroup',
outcomes: {
pageInfo: {
hasNextPage: true,
endCursor: 'Mg',
__typename: 'PageInfo'
},
edges: generateEdges([1, 2], isRefetch),
__typename: 'ContentTagConnection'
}
}
}
})

return [
{
request: {
query: SEARCH_OUTCOME_ALIGNMENTS,
variables
},
result: {
data: {
group: {
_id: groupId,
outcomesCount: numOfOutcomes,
__typename: 'LearningOutcomeGroup',
outcomes: {
pageInfo: {
hasNextPage: true,
endCursor: 'Mg',
__typename: 'PageInfo'
},
edges: generateEdges([1, 2]),
__typename: 'ContentTagConnection'
}
}
}
}
result: returnResult(),
// for testing data refetch
newData: () => returnResult(true)
},
{
request: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ jest.mock('@canvas/alerts/react/FlashAlert')

describe('useCourseAlignmentStats', () => {
let cache, showFlashAlertSpy
const refetchMocks = [...courseAlignmentStatsMocks(), ...courseAlignmentStatsMocks({id: '2'})]
const getStats = result => {
const {totalOutcomes, alignedOutcomes, totalAlignments, totalArtifacts, alignedArtifacts} =
result.current.data.course.outcomeAlignmentStats
return [totalOutcomes, alignedOutcomes, totalAlignments, totalArtifacts, alignedArtifacts]
}

beforeEach(() => {
jest.useFakeTimers()
Expand All @@ -40,9 +46,14 @@ describe('useCourseAlignmentStats', () => {
jest.clearAllMocks()
})

const wrapper = ({children, mocks = courseAlignmentStatsMocks()}) => (
const wrapper = ({
children,
mocks = courseAlignmentStatsMocks(),
contextId = '1',
contextType = 'Course'
}) => (
<MockedProvider cache={cache} mocks={mocks}>
<OutcomesContext.Provider value={{env: {contextType: 'Course', contextId: '1'}}}>
<OutcomesContext.Provider value={{env: {contextType, contextId}}}>
{children}
</OutcomesContext.Provider>
</MockedProvider>
Expand All @@ -56,11 +67,7 @@ describe('useCourseAlignmentStats', () => {
expect(result.current.data).toEqual({})
await act(async () => jest.runAllTimers())
expect(result.current.loading).toBe(false)
expect(result.current.data.course.outcomeAlignmentStats.totalOutcomes).toBe(2)
expect(result.current.data.course.outcomeAlignmentStats.alignedOutcomes).toBe(1)
expect(result.current.data.course.outcomeAlignmentStats.totalAlignments).toBe(4)
expect(result.current.data.course.outcomeAlignmentStats.totalArtifacts).toBe(5)
expect(result.current.data.course.outcomeAlignmentStats.alignedArtifacts).toBe(4)
expect(getStats(result)).toEqual([2, 1, 4, 5, 4])
})

it('displays flash error message when stats fail to load', async () => {
Expand All @@ -77,4 +84,27 @@ describe('useCourseAlignmentStats', () => {
})
expect(result.current.error).not.toBe(null)
})

it('should refetch data if query for the same course id is run a second time', async () => {
const hook = renderHook(() => useCourseAlignmentStats(), {
wrapper,
initialProps: {
mocks: refetchMocks
}
})
expect(hook.result.current.loading).toBe(true)
expect(hook.result.current.data).toEqual({})
await act(async () => jest.runAllTimers())
expect(hook.result.current.loading).toBe(false)
expect(getStats(hook.result)).toEqual([2, 1, 4, 5, 4])

// fetch a different course to force hook rerender
// then refetch the original course to test refetch
hook.rerender({contextId: '2'})
hook.rerender({contextId: '1'})
expect(hook.result.current.loading).toBe(true)
await act(async () => jest.runAllTimers())
expect(hook.result.current.loading).toBe(false)
expect(getStats(hook.result)).toEqual([12, 11, 14, 15, 14])
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@
import React from 'react'
import useCourseAlignments from '../useCourseAlignments'
import {createCache} from '@canvas/apollo'
import {renderHook, act, cleanup} from '@testing-library/react-hooks'
import {renderHook, act} from '@testing-library/react-hooks'
import {courseAlignmentMocks} from '../../../mocks/Management'
import {MockedProvider} from '@apollo/react-testing'
import * as FlashAlert from '@canvas/alerts/react/FlashAlert'
import OutcomesContext from '../../contexts/OutcomesContext'

jest.mock('@canvas/alerts/react/FlashAlert')

const outcomeTitles = result => result.current.rootGroup.outcomes.edges.map(edge => edge.node.title)
const outcomeTitles = result =>
(result?.current?.rootGroup?.outcomes?.edges || []).map(edge => edge.node.title)

describe('useCourseAlignments', () => {
let cache, mocks, showFlashAlertSpy
Expand All @@ -42,7 +43,6 @@ describe('useCourseAlignments', () => {

afterEach(() => {
jest.clearAllMocks()
cleanup()
})

const wrapper = ({children}) => (
Expand Down Expand Up @@ -191,4 +191,36 @@ describe('useCourseAlignments', () => {
])
expect(hook.result.current.rootGroup.outcomes.pageInfo.hasNextPage).toBe(false)
})

it('should refetch data if search with same keyword is run a second time', async () => {
mocks = [
...courseAlignmentMocks({searchQuery: 'abc'}),
...courseAlignmentMocks({searchQuery: 'def'})
]
const hook = renderHook(() => useCourseAlignments(), {wrapper})

// original search
hook.result.current.onSearchChangeHandler({target: {value: 'abc'}})
expect(hook.result.current.loading).toBe(true)
await act(async () => jest.runAllTimers())
expect(hook.result.current.loading).toBe(false)
expect(outcomeTitles(hook.result)).toEqual(['Outcome 1 with alignments', 'Outcome 2'])

// run a different search to force hook rerender
hook.result.current.onSearchChangeHandler({target: {value: 'def'}})
expect(hook.result.current.loading).toBe(true)
await act(async () => jest.runAllTimers())
expect(hook.result.current.loading).toBe(false)
expect(outcomeTitles(hook.result)).toEqual(['Outcome 1 with alignments', 'Outcome 2'])

// repeat original search to test refetch
hook.result.current.onSearchChangeHandler({target: {value: 'abc'}})
expect(hook.result.current.loading).toBe(true)
await act(async () => jest.runAllTimers())
expect(hook.result.current.loading).toBe(false)
expect(outcomeTitles(hook.result)).toEqual([
'Outcome 1 with alignments - Refetched',
'Outcome 2 - Refetched'
])
})
})
8 changes: 5 additions & 3 deletions ui/shared/outcomes/react/hooks/useCourseAlignmentStats.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ const I18n = useI18nScope('AlignmentSummary')

const useCourseAlignmentStats = () => {
const {contextId} = useCanvasContext()
const variables = {
id: contextId
}

const {loading, error, data} = useQuery(COURSE_ALIGNMENT_STATS, {
variables: {
id: contextId
}
variables,
fetchPolicy: 'network-only'
})

useEffect(() => {
Expand Down
8 changes: 5 additions & 3 deletions ui/shared/outcomes/react/hooks/useCourseAlignments.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,19 @@ const useCourseAlignments = () => {
return searchStore.current
}

const id = rootOutcomeGroup.id
const variables = {
id,
id: rootOutcomeGroup.id,
outcomesContextType: contextType,
outcomesContextId: contextId,
searchFilter
}
const debouncedString = debounceSearchString(searchString)
if (debouncedString) variables.searchQuery = debouncedString

const {loading, error, data, fetchMore} = useQuery(SEARCH_OUTCOME_ALIGNMENTS, {variables})
const {loading, error, data, fetchMore} = useQuery(SEARCH_OUTCOME_ALIGNMENTS, {
variables,
fetchPolicy: 'network-only'
})

useEffect(() => {
if (error) {
Expand Down

0 comments on commit ac39379

Please sign in to comment.