add popover details in student LMGB
closes OUT-2266 test plan: - have some outcomes in a course - attach some of the outcomes to a rubric and grade them - go to the LMGB, and then click on a student's name to go to their individual view - expand an outcome group, there should be an info icon - hover over the icon with the mouse, details should be provided that show the most recent assessment, mastery level (if applicable), caclulation method, and some example text and scores - use tab navigation - when tabbing to the info icon, the popover should appear - enable VO. Using the VO keys, navigate to the icon. You should be prompted to click for more information. Press enter, then navigate forward - you should be navigating around a SR only version of the popover details - navigate back to the icon - it should promt you to click to collapse the additional details. Click the button, then navigate forward again using the VO keys. The next element read/highlighted should be the title - compare mastery levels shown in the student lmgb with how they appear in the teacher lmgb. All results should properly match. Change-Id: I25d5943e1593cd9003de719b682275a4bf103db1 Reviewed-on: https://gerrit.instructure.com/158089 Reviewed-by: Augusto Callejas <acallejas@instructure.com> Tested-by: Jenkins Reviewed-by: Michael Brewer-Davis <mbd@instructure.com> QA-Review: Augusto Callejas <acallejas@instructure.com> Product-Review: Nathan Rogowski <nathan@instructure.com>
This commit is contained in:
parent
23f1dbee9a
commit
2e0515ad47
|
@ -1,122 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2015 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
define [
|
||||
'underscore'
|
||||
'i18n!outcomes'
|
||||
'jsx/shared/helpers/numberFormat'
|
||||
'../../underscore-ext/sum'
|
||||
], (_, I18n, numberFormat) ->
|
||||
class DecayingAverage
|
||||
constructor: (@weight, @range) ->
|
||||
@rest = @range[..-2]
|
||||
@last = _.last(@range)
|
||||
|
||||
value: ->
|
||||
n = ((_.sum(@rest) / @rest.length) * @toPercentage(@remainder())) +
|
||||
(@last * @toPercentage(@weight))
|
||||
Math.round(n * 100) / 100
|
||||
|
||||
remainder: ->
|
||||
100 - @weight
|
||||
|
||||
toPercentage: (n) ->
|
||||
n / 100
|
||||
|
||||
class NMastery
|
||||
constructor: (@n, @mastery_points, @range) ->
|
||||
|
||||
aboveMastery: ->
|
||||
_.filter(@range, (n) =>
|
||||
n >= @mastery_points
|
||||
)
|
||||
|
||||
value: ->
|
||||
if @mastery_points? && @aboveMastery().length >= @n
|
||||
Math.round(_.sum(@aboveMastery()) / @aboveMastery().length * 100) / 100
|
||||
else
|
||||
I18n.t("N/A")
|
||||
|
||||
class CalculationMethodContent
|
||||
constructor: (model) ->
|
||||
# We can pass in a straight object or a backbone model
|
||||
_.each([
|
||||
'calculation_method', 'calculation_int', 'mastery_points'
|
||||
], (attr) =>
|
||||
@[attr] = if model.get? then model.get(attr) else model[attr]
|
||||
)
|
||||
|
||||
decayingAverage: ->
|
||||
new DecayingAverage(@calculation_int, @exampleScoreIntegers()).value()
|
||||
|
||||
exampleScoreIntegers: ->
|
||||
[ 1, 4, 2, 3, 5, 3, 6 ]
|
||||
|
||||
nMastery: ->
|
||||
new NMastery(@calculation_int, @mastery_points, @exampleScoreIntegers()).value()
|
||||
|
||||
present: ->
|
||||
@toJSON()[@calculation_method]
|
||||
|
||||
toJSON: ->
|
||||
decaying_average:
|
||||
method: I18n.t("%{recentInt}/%{remainderInt} Decaying Average", {
|
||||
recentInt: @calculation_int
|
||||
remainderInt: 100 - @calculation_int
|
||||
})
|
||||
friendlyCalculationMethod: I18n.t("Decaying Average")
|
||||
calculationIntLabel: I18n.t("Last Item: ")
|
||||
calculationIntDescription: I18n.t('Between 1% and 99%')
|
||||
exampleText: I18n.t(
|
||||
"Most recent result counts as %{calculation_int} of mastery weight, average of all other results count as %{remainder} of weight. If there is only one result, the single score will be returned.", {
|
||||
calculation_int: I18n.n(@calculation_int, { percentage: true }),
|
||||
remainder: I18n.n(100 - @calculation_int, { percentage: true })
|
||||
}
|
||||
),
|
||||
exampleScores: @exampleScoreIntegers().join(', '),
|
||||
exampleResult: numberFormat.outcomeScore(@decayingAverage())
|
||||
n_mastery:
|
||||
method: I18n.t({
|
||||
one: "Achieve mastery one time",
|
||||
other: "Achieve mastery %{count} times"
|
||||
}, {
|
||||
count: @calculation_int
|
||||
})
|
||||
friendlyCalculationMethod: I18n.t("n Number of Times")
|
||||
calculationIntLabel: I18n.t('Items: ')
|
||||
calculationIntDescription: I18n.t('Between 1 and 5')
|
||||
exampleText: I18n.t(
|
||||
{
|
||||
one: "Must achieve mastery at least one time. Scores above mastery will be averaged to calculate final score.",
|
||||
other: "Must achieve mastery at least %{count} times. Scores above mastery will be averaged to calculate final score."
|
||||
}, {
|
||||
count: @calculation_int
|
||||
}),
|
||||
exampleScores: @exampleScoreIntegers().join(', '),
|
||||
exampleResult: numberFormat.outcomeScore(@nMastery())
|
||||
latest:
|
||||
method: I18n.t("Latest Score")
|
||||
friendlyCalculationMethod: I18n.t("Most Recent Score")
|
||||
exampleText: I18n.t("Mastery score reflects the most recent graded assignment or quiz."),
|
||||
exampleScores: @exampleScoreIntegers()[..3].join(', '),
|
||||
exampleResult: numberFormat.outcomeScore(_.last(@exampleScoreIntegers()[..3]))
|
||||
highest:
|
||||
method: I18n.t("Highest Score")
|
||||
friendlyCalculationMethod: I18n.t("Highest Score")
|
||||
exampleText: I18n.t("Mastery score reflects the highest score of a graded assignment or quiz."),
|
||||
exampleScores: @exampleScoreIntegers()[..3].join(', '),
|
||||
exampleResult: numberFormat.outcomeScore(_.max(@exampleScoreIntegers()[..3]))
|
|
@ -0,0 +1,151 @@
|
|||
|
||||
// Copyright (C) 2015 - present Instructure, Inc.
|
||||
|
||||
// This file is part of Canvas.
|
||||
|
||||
// Canvas is free software: you can redistribute it and/or modify it under
|
||||
// the terms of the GNU Affero General Public License as published by the Free
|
||||
// Software Foundation, version 3 of the License.
|
||||
|
||||
// Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
// A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
// details.
|
||||
|
||||
// You should have received a copy of the GNU Affero General Public License along
|
||||
// with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import _ from 'underscore'
|
||||
import I18n from 'i18n!outcomes'
|
||||
import numberFormat from 'jsx/shared/helpers/numberFormat'
|
||||
|
||||
_.mixin({
|
||||
sum (array, accessor = null, start = 0) {
|
||||
return _.reduce(array, (memo, el) => (accessor != null ? accessor(el) : el) + memo, start)
|
||||
},
|
||||
})
|
||||
|
||||
class DecayingAverage {
|
||||
constructor(weight, range) {
|
||||
this.weight = weight;
|
||||
this.range = range;
|
||||
this.rest = this.range.slice(0, +-2 + 1 || undefined);
|
||||
this.last = _.last(this.range);
|
||||
}
|
||||
|
||||
value() {
|
||||
const n = ((_.sum(this.rest) / this.rest.length) * this.toPercentage(this.remainder())) +
|
||||
(this.last * this.toPercentage(this.weight));
|
||||
return Math.round(n * 100) / 100;
|
||||
}
|
||||
|
||||
remainder() {
|
||||
return 100 - this.weight;
|
||||
}
|
||||
|
||||
toPercentage(n) {
|
||||
return n / 100;
|
||||
}
|
||||
}
|
||||
|
||||
class NMastery {
|
||||
constructor(n, mastery_points, range) {
|
||||
this.n = n;
|
||||
this.mastery_points = mastery_points;
|
||||
this.range = range;
|
||||
}
|
||||
|
||||
aboveMastery() {
|
||||
return _.filter(this.range, n => (n >= this.mastery_points));
|
||||
}
|
||||
|
||||
value() {
|
||||
if ((this.mastery_points != null) && (this.aboveMastery().length >= this.n)) {
|
||||
return Math.round((_.sum(this.aboveMastery()) / this.aboveMastery().length) * 100) / 100;
|
||||
} else {
|
||||
return I18n.t("N/A");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class CalculationMethodContent {
|
||||
constructor(model) {
|
||||
// We can pass in a straight object or a backbone model
|
||||
_.each([
|
||||
'calculation_method', 'calculation_int', 'mastery_points'
|
||||
], attr => (this[attr] = (model.get != null) ? model.get(attr) : model[attr]));
|
||||
}
|
||||
|
||||
decayingAverage() {
|
||||
return new DecayingAverage(this.calculation_int, this.exampleScoreIntegers()).value();
|
||||
}
|
||||
|
||||
exampleScoreIntegers() {
|
||||
return [ 1, 4, 2, 3, 5, 3, 6 ];
|
||||
}
|
||||
|
||||
nMastery() {
|
||||
return new NMastery(this.calculation_int, this.mastery_points, this.exampleScoreIntegers()).value();
|
||||
}
|
||||
|
||||
present() {
|
||||
return this.toJSON()[this.calculation_method];
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
decaying_average: {
|
||||
method: I18n.t("%{recentInt}/%{remainderInt} Decaying Average", {
|
||||
recentInt: this.calculation_int,
|
||||
remainderInt: 100 - this.calculation_int
|
||||
}),
|
||||
friendlyCalculationMethod: I18n.t("Decaying Average"),
|
||||
calculationIntLabel: I18n.t("Last Item: "),
|
||||
calculationIntDescription: I18n.t('Between 1% and 99%'),
|
||||
exampleText: I18n.t(
|
||||
"Most recent result counts as %{calculation_int} of mastery weight, average of all other results count as %{remainder} of weight. If there is only one result, the single score will be returned.", {
|
||||
calculation_int: I18n.n(this.calculation_int, { percentage: true }),
|
||||
remainder: I18n.n(100 - this.calculation_int, { percentage: true })
|
||||
}
|
||||
),
|
||||
exampleScores: this.exampleScoreIntegers().join(', '),
|
||||
exampleResult: numberFormat.outcomeScore(this.decayingAverage())
|
||||
},
|
||||
n_mastery: {
|
||||
method: I18n.t({
|
||||
one: "Achieve mastery one time",
|
||||
other: "Achieve mastery %{count} times"
|
||||
}, {
|
||||
count: this.calculation_int
|
||||
}),
|
||||
friendlyCalculationMethod: I18n.t("n Number of Times"),
|
||||
calculationIntLabel: I18n.t('Items: '),
|
||||
calculationIntDescription: I18n.t('Between 1 and 5'),
|
||||
exampleText: I18n.t(
|
||||
{
|
||||
one: "Must achieve mastery at least one time. Scores above mastery will be averaged to calculate final score.",
|
||||
other: "Must achieve mastery at least %{count} times. Scores above mastery will be averaged to calculate final score."
|
||||
}, {
|
||||
count: this.calculation_int
|
||||
}),
|
||||
exampleScores: this.exampleScoreIntegers().join(', '),
|
||||
exampleResult: numberFormat.outcomeScore(this.nMastery())
|
||||
},
|
||||
latest: {
|
||||
method: I18n.t("Latest Score"),
|
||||
friendlyCalculationMethod: I18n.t("Most Recent Score"),
|
||||
exampleText: I18n.t("Mastery score reflects the most recent graded assignment or quiz."),
|
||||
exampleScores: this.exampleScoreIntegers().slice(0, 4).join(', '),
|
||||
exampleResult: numberFormat.outcomeScore(_.last(this.exampleScoreIntegers().slice(0, 4)))
|
||||
},
|
||||
highest: {
|
||||
method: I18n.t("Highest Score"),
|
||||
friendlyCalculationMethod: I18n.t("Highest Score"),
|
||||
exampleText: I18n.t("Mastery score reflects the highest score of a graded assignment or quiz."),
|
||||
exampleScores: this.exampleScoreIntegers().slice(0, 4).join(', '),
|
||||
exampleResult: numberFormat.outcomeScore(_.max(this.exampleScoreIntegers().slice(0, 4)))
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -25,9 +25,9 @@ import ToggleGroup from '@instructure/ui-toggle-details/lib/components/ToggleGro
|
|||
import List, { ListItem } from '@instructure/ui-elements/lib/components/List'
|
||||
import Pill from '@instructure/ui-elements/lib/components/Pill'
|
||||
import Text from '@instructure/ui-elements/lib/components/Text'
|
||||
import IconOutcomes from '@instructure/ui-icons/lib/Line/IconOutcomes'
|
||||
import natcompare from 'compiled/util/natcompare'
|
||||
import AssignmentResult from './AssignmentResult'
|
||||
import OutcomePopover from './OutcomePopover'
|
||||
import * as shapes from './shapes'
|
||||
|
||||
export default class Outcome extends React.Component {
|
||||
|
@ -47,7 +47,7 @@ export default class Outcome extends React.Component {
|
|||
}
|
||||
|
||||
renderHeader () {
|
||||
const { outcome } = this.props
|
||||
const { outcome, outcomeProficiency } = this.props
|
||||
const { mastered, results, title } = outcome
|
||||
const numAlignments = results.length
|
||||
|
||||
|
@ -58,7 +58,9 @@ export default class Outcome extends React.Component {
|
|||
<FlexItem>
|
||||
<Text size="medium">
|
||||
<Flex>
|
||||
<FlexItem><IconOutcomes /></FlexItem>
|
||||
<FlexItem>
|
||||
<OutcomePopover outcome={outcome} outcomeProficiency={outcomeProficiency}/>
|
||||
</FlexItem>
|
||||
<FlexItem padding="0 x-small">{ title }</FlexItem>
|
||||
</Flex>
|
||||
</Text>
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import _ from 'lodash'
|
||||
import I18n from 'i18n!outcomes'
|
||||
import View from '@instructure/ui-layout/lib/components/View'
|
||||
import Flex, { FlexItem } from '@instructure/ui-layout/lib/components/Flex'
|
||||
import Text from '@instructure/ui-elements/lib/components/Text'
|
||||
import Link from '@instructure/ui-elements/lib/components/Link'
|
||||
import ScreenReaderContent from '@instructure/ui-a11y/lib/components/ScreenReaderContent'
|
||||
import CalculationMethodContent from 'compiled/models/grade_summary/CalculationMethodContent'
|
||||
import Popover, {PopoverTrigger, PopoverContent} from '@instructure/ui-overlays/lib/components/Popover'
|
||||
import IconInfo from '@instructure/ui-icons/lib/Line/IconInfo'
|
||||
import DatetimeDisplay from '../../shared/DatetimeDisplay'
|
||||
import * as shapes from './shapes'
|
||||
|
||||
export default class OutcomePopover extends React.Component {
|
||||
static propTypes = {
|
||||
outcome: shapes.outcomeShape.isRequired,
|
||||
outcomeProficiency: shapes.outcomeProficiencyShape
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
outcomeProficiency: null
|
||||
}
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
this.state = { moreInformation: false }
|
||||
}
|
||||
|
||||
getSelectedRating () {
|
||||
const { outcomeProficiency } = this.props
|
||||
const { points_possible, mastery_points, score } = this.props.outcome
|
||||
const hasScore = score >= 0
|
||||
if (outcomeProficiency && hasScore) {
|
||||
const totalPoints = points_possible || mastery_points
|
||||
const percentage = totalPoints ? (score / totalPoints) : score
|
||||
const maxRating = outcomeProficiency.ratings[0].points
|
||||
const scaledScore = maxRating * percentage
|
||||
return _.find(outcomeProficiency.ratings, (r) => (scaledScore >= r.points)) || _.last(outcomeProficiency.ratings)
|
||||
} else if (hasScore) {
|
||||
return _.find(this.defaultProficiency(mastery_points).ratings, (r) => (score >= r.points))
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
defaultProficiency = _.memoize((mastery_points) => (
|
||||
{
|
||||
ratings: [
|
||||
{points: mastery_points * 1.5, color: '127A1B', description: I18n.t('Exceeds Mastery')},
|
||||
{points: mastery_points, color: '00AC18', description: I18n.t('Meets Mastery')},
|
||||
{points: mastery_points/2, color: 'FAB901', description: I18n.t('Near Mastery')},
|
||||
{points: 0, color: 'EE0612', description: I18n.t('Well Below Mastery')}
|
||||
]
|
||||
}
|
||||
))
|
||||
|
||||
latestTime () {
|
||||
const { outcome } = this.props
|
||||
if (outcome.results.length > 0) {
|
||||
return _.sortBy(outcome.results, (r) => (r.submitted_or_assessed_at))[0].submitted_or_assessed_at
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
expandDetails = () => { this.setState({ moreInformation: !this.state.moreInformation }) }
|
||||
|
||||
renderPopoverContent () {
|
||||
const selectedRating = this.getSelectedRating()
|
||||
const latestTime = this.latestTime()
|
||||
const popoverContent = new CalculationMethodContent(this.props.outcome).present()
|
||||
const {
|
||||
method,
|
||||
exampleText,
|
||||
exampleScores,
|
||||
exampleResult
|
||||
} = popoverContent
|
||||
return (
|
||||
<View as='div' padding='small' maxWidth='30rem'>
|
||||
<Text size='small'>
|
||||
<Flex
|
||||
alignItems='stretch'
|
||||
direction='row'
|
||||
justifyItems='space-between'
|
||||
>
|
||||
<FlexItem grow shrink>
|
||||
<div>{I18n.t('Last Assessment: ')}
|
||||
{ latestTime ?
|
||||
<DatetimeDisplay datetime={latestTime} format='%b %d, %l:%M %p' /> :
|
||||
I18n.t('No submissions')
|
||||
}
|
||||
</div>
|
||||
</FlexItem>
|
||||
<FlexItem grow shrink align='stretch'>
|
||||
<Text size='small' weight='bold'>
|
||||
<div>
|
||||
{selectedRating &&
|
||||
<div style={{color: `#${selectedRating.color}`, textAlign: 'end'}}>
|
||||
{selectedRating.description}
|
||||
</div>}
|
||||
</div>
|
||||
</Text>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
<hr role='presentation'/>
|
||||
<div>
|
||||
<Text size='small' weight='bold'>{I18n.t('Calculation Method')}</Text>
|
||||
<div>{method}</div>
|
||||
<div style={{padding: '0.5rem 0 0 0'}}><Text size='small' weight="bold">{I18n.t('Example')}</Text></div>
|
||||
<div>{exampleText}</div>
|
||||
<div>{I18n.t('1- Item Scores: %{exampleScores}', { exampleScores })}</div>
|
||||
<div>{I18n.t('2- Final Score: %{exampleResult}', { exampleResult })}</div>
|
||||
</div>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const popoverContent = this.renderPopoverContent()
|
||||
return (
|
||||
<span>
|
||||
<Popover
|
||||
placement="bottom"
|
||||
>
|
||||
<PopoverTrigger>
|
||||
<Link onClick={() => this.expandDetails()}>
|
||||
<span style={{color: 'black'}}><IconInfo /></span>
|
||||
<span>
|
||||
{!this.state.moreInformation ?
|
||||
<ScreenReaderContent>{I18n.t('Click to expand outcome details')}</ScreenReaderContent> :
|
||||
<ScreenReaderContent>{I18n.t('Click to collapse outcome details')}</ScreenReaderContent>
|
||||
}
|
||||
</span>
|
||||
</Link>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
{popoverContent}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FlexItem>
|
||||
{this.state.moreInformation &&
|
||||
<ScreenReaderContent>{popoverContent}</ScreenReaderContent>
|
||||
}
|
||||
</FlexItem>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ const defaultProps = (props = {}) => (
|
|||
outcome: {
|
||||
id: 1,
|
||||
mastered: false,
|
||||
calculation_method: 'highest',
|
||||
ratings: [
|
||||
{ description: 'My first rating' },
|
||||
{ description: 'My second rating' }
|
||||
|
|
|
@ -41,6 +41,7 @@ const defaultProps = (props = {}) => (
|
|||
mastered: false,
|
||||
mastery_points: 3,
|
||||
points_possible: 5,
|
||||
calculation_method: 'highest',
|
||||
ratings: [
|
||||
{ description: 'My first rating' },
|
||||
{ description: 'My second rating' }
|
||||
|
|
|
@ -27,6 +27,7 @@ const outcome = (id, title) => ({
|
|||
mastered: false,
|
||||
mastery_points: 3,
|
||||
points_possible: 5,
|
||||
calculation_method: 'highest',
|
||||
ratings: [
|
||||
{ description: 'My first rating' },
|
||||
{ description: 'My second rating' }
|
||||
|
@ -58,6 +59,7 @@ const defaultProps = (props = {}) => (
|
|||
mastered: false,
|
||||
mastery_points: 3,
|
||||
points_possible: 5,
|
||||
calculation_method: 'highest',
|
||||
ratings: [
|
||||
{ description: 'My first rating' },
|
||||
{ description: 'My second rating' }
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { shallow } from 'enzyme'
|
||||
import OutcomePopover from '../OutcomePopover'
|
||||
|
||||
const time1 = new Date(Date.UTC(2018, 1, 1, 7, 1, 0))
|
||||
const time2 = new Date(Date.UTC(2018, 1, 1, 8, 1, 0))
|
||||
|
||||
const defaultProps = (props = {}) => (
|
||||
Object.assign({
|
||||
outcome: {
|
||||
id: 1,
|
||||
expansionId: 100,
|
||||
mastered: false,
|
||||
mastery_points: 3,
|
||||
points_possible: 5,
|
||||
calculation_method: 'highest',
|
||||
score: 3,
|
||||
ratings: [
|
||||
{ description: 'My first rating' },
|
||||
{ description: 'My second rating' }
|
||||
],
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
score: 1,
|
||||
percent: 0.1,
|
||||
assignment: {
|
||||
id: 1,
|
||||
html_url: 'http://foo',
|
||||
name: 'My assignment',
|
||||
submission_types: 'online_quiz',
|
||||
score: 0
|
||||
},
|
||||
submitted_or_assessed_at: time1
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
score: 7,
|
||||
percent: 0.7,
|
||||
assignment: {
|
||||
id: 2,
|
||||
html_url: 'http://bar',
|
||||
name: 'Assignment 2',
|
||||
submission_types: 'online_quiz',
|
||||
score: 3
|
||||
},
|
||||
submitted_or_assessed_at: time2
|
||||
}
|
||||
],
|
||||
title: 'My outcome'
|
||||
},
|
||||
outcomeProficiency: {
|
||||
ratings: [
|
||||
{ color: 'blue', description: "I am blue", points: 10},
|
||||
{ color: 'green', description: "I am Groot", points: 5},
|
||||
{ color: 'red', description: "I am red", points: 0}
|
||||
]
|
||||
}
|
||||
}, props)
|
||||
)
|
||||
|
||||
it('renders the OutcomePopover component', () => {
|
||||
const wrapper = shallow(<OutcomePopover {...defaultProps()}/>)
|
||||
expect(wrapper.debug()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders correctly with no results', () => {
|
||||
const props = defaultProps()
|
||||
props.outcome.results = []
|
||||
const wrapper = shallow(<OutcomePopover {...props}/>)
|
||||
expect(wrapper.debug()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders correctly with no custom outcomeProficiency', () => {
|
||||
const props = defaultProps()
|
||||
props.outcomeProficiency = null
|
||||
const wrapper = shallow(<OutcomePopover {...props}/>)
|
||||
expect(wrapper.debug()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('properly expands details for screenreader users', () => {
|
||||
const props = defaultProps()
|
||||
const wrapper = shallow(<OutcomePopover {...props}/>)
|
||||
expect(wrapper.state('moreInformation')).toEqual(false)
|
||||
wrapper.find('Link').simulate('click')
|
||||
expect(wrapper.state('moreInformation')).toEqual(true)
|
||||
})
|
||||
|
||||
describe('latestTime', () => {
|
||||
it('properly returns the most recent submission time', () => {
|
||||
const props = defaultProps()
|
||||
const wrapper = shallow(<OutcomePopover {...props}/>)
|
||||
expect(wrapper.instance().latestTime()).toEqual(time1)
|
||||
})
|
||||
|
||||
it('properly returns nothing when there are no results', () => {
|
||||
const props = defaultProps()
|
||||
props.outcome.results = []
|
||||
const wrapper = shallow(<OutcomePopover {...props}/>)
|
||||
expect(wrapper.instance().latestTime()).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSelectedRating', () => {
|
||||
it('properly returns the custom proficiency level', () => {
|
||||
const props = defaultProps()
|
||||
const wrapper = shallow(<OutcomePopover {...props}/>)
|
||||
const rating = wrapper.instance().getSelectedRating()
|
||||
expect(rating.description).toEqual('I am Groot')
|
||||
})
|
||||
|
||||
it('properly returns the default proficiency level', () => {
|
||||
const props = defaultProps()
|
||||
props.outcomeProficiency = null
|
||||
const wrapper = shallow(<OutcomePopover {...props}/>)
|
||||
const rating = wrapper.instance().getSelectedRating()
|
||||
expect(rating.description).toEqual('Meets Mastery')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,199 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly with no custom outcomeProficiency 1`] = `
|
||||
"<span>
|
||||
<Popover placement=\\"bottom\\" onToggle={[Function]} onClick={[Function]} onFocus={[Function]} onBlur={[Function]} onMouseOver={[Function]} onMouseOut={[Function]} onShow={[Function]} onDismiss={[Function]} stacking=\\"topmost\\" offsetX={0} offsetY={0} variant=\\"default\\" on={{...}} contentRef={[Function]} defaultShow={false} withArrow={true} trackPosition={true} constrain=\\"window\\" onPositioned={[Function]} onPositionChanged={[Function]} shouldRenderOffscreen={false} shouldContainFocus={false} shouldReturnFocus={true} shouldCloseOnDocumentClick={true} shouldCloseOnEscape={true} defaultFocusElement={{...}} label={{...}} mountNode={{...}} insertAt=\\"bottom\\" liveRegion={{...}} positionTarget={{...}} alignArrow={false}>
|
||||
<PopoverTrigger>
|
||||
<Link onClick={[Function]} variant=\\"default\\" as=\\"button\\" linkRef={[Function]} ellipsis={false} iconPlacement=\\"start\\">
|
||||
<span style={{...}}>
|
||||
<IconInfo />
|
||||
</span>
|
||||
<span>
|
||||
<ScreenReaderContent as=\\"span\\">
|
||||
Click to expand outcome details
|
||||
</ScreenReaderContent>
|
||||
</span>
|
||||
</Link>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<View as=\\"div\\" padding=\\"small\\" maxWidth=\\"30rem\\" display=\\"auto\\">
|
||||
<Text size=\\"small\\" as=\\"span\\" letterSpacing=\\"normal\\">
|
||||
<Flex alignItems=\\"stretch\\" direction=\\"row\\" justifyItems=\\"space-between\\" as=\\"span\\" inline={false} visualDebug={false} wrapItems={false}>
|
||||
<FlexItem grow={true} shrink={true} as=\\"span\\">
|
||||
<div>
|
||||
Last Assessment:
|
||||
<DatetimeDisplay datetime={{...}} format=\\"%b %d, %l:%M %p\\" />
|
||||
</div>
|
||||
</FlexItem>
|
||||
<FlexItem grow={true} shrink={true} align=\\"stretch\\" as=\\"span\\">
|
||||
<Text size=\\"small\\" weight=\\"bold\\" as=\\"span\\" letterSpacing=\\"normal\\">
|
||||
<div>
|
||||
<div style={{...}}>
|
||||
Meets Mastery
|
||||
</div>
|
||||
</div>
|
||||
</Text>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
<hr role=\\"presentation\\" />
|
||||
<div>
|
||||
<Text size=\\"small\\" weight=\\"bold\\" as=\\"span\\" letterSpacing=\\"normal\\">
|
||||
Calculation Method
|
||||
</Text>
|
||||
<div>
|
||||
Highest Score
|
||||
</div>
|
||||
<div style={{...}}>
|
||||
<Text size=\\"small\\" weight=\\"bold\\" as=\\"span\\" letterSpacing=\\"normal\\">
|
||||
Example
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
Mastery score reflects the highest score of a graded assignment or quiz.
|
||||
</div>
|
||||
<div>
|
||||
1- Item Scores: 1, 4, 2, 3
|
||||
</div>
|
||||
<div>
|
||||
2- Final Score: 4
|
||||
</div>
|
||||
</div>
|
||||
</Text>
|
||||
</View>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FlexItem as=\\"span\\" grow={false} shrink={false} />
|
||||
</span>"
|
||||
`;
|
||||
|
||||
exports[`renders correctly with no results 1`] = `
|
||||
"<span>
|
||||
<Popover placement=\\"bottom\\" onToggle={[Function]} onClick={[Function]} onFocus={[Function]} onBlur={[Function]} onMouseOver={[Function]} onMouseOut={[Function]} onShow={[Function]} onDismiss={[Function]} stacking=\\"topmost\\" offsetX={0} offsetY={0} variant=\\"default\\" on={{...}} contentRef={[Function]} defaultShow={false} withArrow={true} trackPosition={true} constrain=\\"window\\" onPositioned={[Function]} onPositionChanged={[Function]} shouldRenderOffscreen={false} shouldContainFocus={false} shouldReturnFocus={true} shouldCloseOnDocumentClick={true} shouldCloseOnEscape={true} defaultFocusElement={{...}} label={{...}} mountNode={{...}} insertAt=\\"bottom\\" liveRegion={{...}} positionTarget={{...}} alignArrow={false}>
|
||||
<PopoverTrigger>
|
||||
<Link onClick={[Function]} variant=\\"default\\" as=\\"button\\" linkRef={[Function]} ellipsis={false} iconPlacement=\\"start\\">
|
||||
<span style={{...}}>
|
||||
<IconInfo />
|
||||
</span>
|
||||
<span>
|
||||
<ScreenReaderContent as=\\"span\\">
|
||||
Click to expand outcome details
|
||||
</ScreenReaderContent>
|
||||
</span>
|
||||
</Link>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<View as=\\"div\\" padding=\\"small\\" maxWidth=\\"30rem\\" display=\\"auto\\">
|
||||
<Text size=\\"small\\" as=\\"span\\" letterSpacing=\\"normal\\">
|
||||
<Flex alignItems=\\"stretch\\" direction=\\"row\\" justifyItems=\\"space-between\\" as=\\"span\\" inline={false} visualDebug={false} wrapItems={false}>
|
||||
<FlexItem grow={true} shrink={true} as=\\"span\\">
|
||||
<div>
|
||||
Last Assessment:
|
||||
No submissions
|
||||
</div>
|
||||
</FlexItem>
|
||||
<FlexItem grow={true} shrink={true} align=\\"stretch\\" as=\\"span\\">
|
||||
<Text size=\\"small\\" weight=\\"bold\\" as=\\"span\\" letterSpacing=\\"normal\\">
|
||||
<div>
|
||||
<div style={{...}}>
|
||||
I am Groot
|
||||
</div>
|
||||
</div>
|
||||
</Text>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
<hr role=\\"presentation\\" />
|
||||
<div>
|
||||
<Text size=\\"small\\" weight=\\"bold\\" as=\\"span\\" letterSpacing=\\"normal\\">
|
||||
Calculation Method
|
||||
</Text>
|
||||
<div>
|
||||
Highest Score
|
||||
</div>
|
||||
<div style={{...}}>
|
||||
<Text size=\\"small\\" weight=\\"bold\\" as=\\"span\\" letterSpacing=\\"normal\\">
|
||||
Example
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
Mastery score reflects the highest score of a graded assignment or quiz.
|
||||
</div>
|
||||
<div>
|
||||
1- Item Scores: 1, 4, 2, 3
|
||||
</div>
|
||||
<div>
|
||||
2- Final Score: 4
|
||||
</div>
|
||||
</div>
|
||||
</Text>
|
||||
</View>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FlexItem as=\\"span\\" grow={false} shrink={false} />
|
||||
</span>"
|
||||
`;
|
||||
|
||||
exports[`renders the OutcomePopover component 1`] = `
|
||||
"<span>
|
||||
<Popover placement=\\"bottom\\" onToggle={[Function]} onClick={[Function]} onFocus={[Function]} onBlur={[Function]} onMouseOver={[Function]} onMouseOut={[Function]} onShow={[Function]} onDismiss={[Function]} stacking=\\"topmost\\" offsetX={0} offsetY={0} variant=\\"default\\" on={{...}} contentRef={[Function]} defaultShow={false} withArrow={true} trackPosition={true} constrain=\\"window\\" onPositioned={[Function]} onPositionChanged={[Function]} shouldRenderOffscreen={false} shouldContainFocus={false} shouldReturnFocus={true} shouldCloseOnDocumentClick={true} shouldCloseOnEscape={true} defaultFocusElement={{...}} label={{...}} mountNode={{...}} insertAt=\\"bottom\\" liveRegion={{...}} positionTarget={{...}} alignArrow={false}>
|
||||
<PopoverTrigger>
|
||||
<Link onClick={[Function]} variant=\\"default\\" as=\\"button\\" linkRef={[Function]} ellipsis={false} iconPlacement=\\"start\\">
|
||||
<span style={{...}}>
|
||||
<IconInfo />
|
||||
</span>
|
||||
<span>
|
||||
<ScreenReaderContent as=\\"span\\">
|
||||
Click to expand outcome details
|
||||
</ScreenReaderContent>
|
||||
</span>
|
||||
</Link>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<View as=\\"div\\" padding=\\"small\\" maxWidth=\\"30rem\\" display=\\"auto\\">
|
||||
<Text size=\\"small\\" as=\\"span\\" letterSpacing=\\"normal\\">
|
||||
<Flex alignItems=\\"stretch\\" direction=\\"row\\" justifyItems=\\"space-between\\" as=\\"span\\" inline={false} visualDebug={false} wrapItems={false}>
|
||||
<FlexItem grow={true} shrink={true} as=\\"span\\">
|
||||
<div>
|
||||
Last Assessment:
|
||||
<DatetimeDisplay datetime={{...}} format=\\"%b %d, %l:%M %p\\" />
|
||||
</div>
|
||||
</FlexItem>
|
||||
<FlexItem grow={true} shrink={true} align=\\"stretch\\" as=\\"span\\">
|
||||
<Text size=\\"small\\" weight=\\"bold\\" as=\\"span\\" letterSpacing=\\"normal\\">
|
||||
<div>
|
||||
<div style={{...}}>
|
||||
I am Groot
|
||||
</div>
|
||||
</div>
|
||||
</Text>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
<hr role=\\"presentation\\" />
|
||||
<div>
|
||||
<Text size=\\"small\\" weight=\\"bold\\" as=\\"span\\" letterSpacing=\\"normal\\">
|
||||
Calculation Method
|
||||
</Text>
|
||||
<div>
|
||||
Highest Score
|
||||
</div>
|
||||
<div style={{...}}>
|
||||
<Text size=\\"small\\" weight=\\"bold\\" as=\\"span\\" letterSpacing=\\"normal\\">
|
||||
Example
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
Mastery score reflects the highest score of a graded assignment or quiz.
|
||||
</div>
|
||||
<div>
|
||||
1- Item Scores: 1, 4, 2, 3
|
||||
</div>
|
||||
<div>
|
||||
2- Final Score: 4
|
||||
</div>
|
||||
</div>
|
||||
</Text>
|
||||
</View>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FlexItem as=\\"span\\" grow={false} shrink={false} />
|
||||
</span>"
|
||||
`;
|
|
@ -93,6 +93,7 @@ describe('expand and contract', () => {
|
|||
mastered: false,
|
||||
mastery_points: 0,
|
||||
points_possible: 0,
|
||||
calculation_method: 'highest',
|
||||
results: [],
|
||||
ratings: []
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue