make student lmgb responsive

closes OUT-3544
flag=none

Test plan:
- Add outcomes, aligned rubrics and quizzes,
  and assess students.
- Enable LMGB and sLMGB
- View sLMGB from teacher view and
  student view at different resolutions
  - verify that adjustments match proposals in
    ticket
  - verify that adjustments are functional at
    various screen sizes (320x256 min, 992x700 desktop,
    etc.)

Change-Id: Id8c4404843ccb80786e6d7286341f1ba5a3d51a4
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/242797
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Product-Review: Michael Brewer-Davis <mbd@instructure.com>
Reviewed-by: Pat Renner <prenner@instructure.com>
Reviewed-by: Augusto Callejas <acallejas@instructure.com>
QA-Review: Brian Watson <bwatson@instructure.com>
This commit is contained in:
Michael Brewer-Davis 2020-07-07 13:08:01 -05:00 committed by Michael Brewer-Davis
parent 98263403b3
commit 212aab602b
20 changed files with 661 additions and 2389 deletions

View File

@ -23,12 +23,13 @@ import {Button} from '@instructure/ui-buttons'
import {Flex} from '@instructure/ui-layout'
import {PresentationContent, ScreenReaderContent} from '@instructure/ui-a11y'
import {Text} from '@instructure/ui-elements'
import WithBreakpoints, {breakpointsShape} from '../shared/WithBreakpoints'
import {showFlashError} from '../shared/FlashAlert'
import I18n from 'i18n!grade_summary'
import SelectMenu from './SelectMenu'
export default class SelectMenuGroup extends React.Component {
class SelectMenuGroup extends React.Component {
static propTypes = {
assignmentSortOptions: PropTypes.arrayOf(PropTypes.array).isRequired,
courses: PropTypes.arrayOf(
@ -58,11 +59,13 @@ export default class SelectMenuGroup extends React.Component {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
})
).isRequired
).isRequired,
breakpoints: breakpointsShape
}
static defaultProps = {
selectedGradingPeriodID: null
selectedGradingPeriodID: null,
breakpoints: {}
}
constructor(props) {
@ -158,8 +161,14 @@ export default class SelectMenuGroup extends React.Component {
}
render() {
const isVertical = !this.props.breakpoints.miniTablet
return (
<Flex alignItems="end" wrapItems margin="0 0 small 0">
<Flex
alignItems={isVertical ? 'start' : 'end'}
wrapItems
margin="0 0 small 0"
direction={isVertical ? 'column' : 'row'}
>
<Flex.Item>
{this.props.students.length > 1 && (
<SelectMenu
@ -216,7 +225,7 @@ export default class SelectMenuGroup extends React.Component {
/>
</Flex.Item>
<Flex.Item margin="0 0 0 small">
<Flex.Item margin={isVertical ? 'small 0 0 0' : '0 0 0 small'}>
<Button
disabled={this.state.processing || this.noSelectMenuChanged()}
id="apply_select_menus"
@ -237,3 +246,5 @@ export default class SelectMenuGroup extends React.Component {
)
}
}
export default WithBreakpoints(SelectMenuGroup)

View File

@ -29,65 +29,39 @@ import UnassessedAssignment from './UnassessedAssignment'
import OutcomePopover from './OutcomePopover'
import {ScreenReaderContent, PresentationContent} from '@instructure/ui-a11y'
import TruncateWithTooltip from '../../shared/components/TruncateWithTooltip'
import WithBreakpoints, {breakpointsShape} from '../../shared/WithBreakpoints'
import * as shapes from './shapes'
export default class Outcome extends React.Component {
class Outcome extends React.Component {
static propTypes = {
outcome: shapes.outcomeShape.isRequired,
expanded: PropTypes.bool.isRequired,
onExpansionChange: PropTypes.func.isRequired,
outcomeProficiency: shapes.outcomeProficiencyShape
outcomeProficiency: shapes.outcomeProficiencyShape,
breakpoints: breakpointsShape
}
static defaultProps = {
outcomeProficiency: null
outcomeProficiency: null,
breakpoints: {}
}
handleToggle = (_event, expanded) => {
this.props.onExpansionChange('outcome', this.props.outcome.expansionId, expanded)
}
renderHeader() {
const {outcome, outcomeProficiency} = this.props
const {assignments, display_name, mastered, title, score, points_possible, results} = outcome
const numAlignments = assignments.length
const pillAttributes = {margin: '0 0 0 x-small', text: I18n.t('Not mastered')}
renderScoreAndPill() {
const {outcome} = this.props
const {mastered, score, points_possible, results} = outcome
const pillAttributes = {text: I18n.t('Not mastered')}
if (mastered) {
Object.assign(pillAttributes, {text: I18n.t('Mastered'), variant: 'success'})
}
return (
<Flex direction="row" justifyItems="space-between" data-selenium="outcome">
<Flex.Item shrink>
<Flex direction="column">
<Flex.Item>
<Text size="medium">
<Flex>
<Flex.Item>
<OutcomePopover outcome={outcome} outcomeProficiency={outcomeProficiency} />
</Flex.Item>
<Flex.Item shrink padding="0 x-small">
<TruncateWithTooltip>{display_name || title}</TruncateWithTooltip>
</Flex.Item>
</Flex>
</Text>
</Flex.Item>
<Flex.Item>
<Text size="small">
{I18n.t(
{
zero: 'No alignments',
one: '%{count} alignment',
other: '%{count} alignments'
},
{count: I18n.n(numAlignments)}
)}
</Text>
</Flex.Item>
</Flex>
</Flex.Item>
<Flex.Item>
{_.isNumber(score) && !_.every(results, ['hide_points', true]) && (
<Flex direction="row" justifyItems="start" padding="0 0 0 x-small">
{_.isNumber(score) && !_.every(results, ['hide_points', true]) && (
<Flex.Item padding="0 x-small 0 0">
<span>
<PresentationContent>
<Text size="medium">
@ -95,16 +69,64 @@ export default class Outcome extends React.Component {
</Text>
</PresentationContent>
<ScreenReaderContent>
{I18n.t('%{score} out of %{points_possible} points', {score, points_possible})}
{I18n.t('%{score} out of %{points_possible} points', {
score,
points_possible
})}
</ScreenReaderContent>
</span>
)}
</Flex.Item>
)}
<Flex.Item>
<Pill {...pillAttributes} />
</Flex.Item>
</Flex>
)
}
renderHeader() {
const {outcome, outcomeProficiency, breakpoints} = this.props
const {assignments, display_name, title} = outcome
const numAlignments = assignments.length
const verticalLayout = !breakpoints.tablet
return (
<Flex
direction={verticalLayout ? 'column' : 'row'}
justifyItems={verticalLayout ? null : 'space-between'}
alignItems={verticalLayout ? 'stretch' : null}
data-selenium="outcome"
>
<Flex.Item grow as="div">
<Text size="medium">
<Flex>
<Flex.Item>
<OutcomePopover outcome={outcome} outcomeProficiency={outcomeProficiency} />
</Flex.Item>
<Flex.Item shrink>
<TruncateWithTooltip>{display_name || title}</TruncateWithTooltip>
</Flex.Item>
</Flex>
</Text>
{verticalLayout && this.renderScoreAndPill()}
<View as="div" padding="0 0 0 x-small">
<Text size="small">
{I18n.t(
{
zero: 'No alignments',
one: '%{count} alignment',
other: '%{count} alignments'
},
{count: I18n.n(numAlignments)}
)}
</Text>
</View>
</Flex.Item>
{!verticalLayout && <Flex.Item>{this.renderScoreAndPill()}</Flex.Item>}
</Flex>
)
}
renderDetails() {
const {outcome, outcomeProficiency} = this.props
const {assignments, results} = outcome
@ -168,3 +190,5 @@ export default class Outcome extends React.Component {
)
}
}
export default WithBreakpoints(Outcome)

View File

@ -27,10 +27,15 @@ import natcompare from 'compiled/util/natcompare'
import TruncateWithTooltip from '../../shared/components/TruncateWithTooltip'
import Outcome from './Outcome'
import * as shapes from './shapes'
import WithBreakpoints, {breakpointsShape} from '../../shared/WithBreakpoints'
const outcomeGroupHeader = (outcomeGroup, numMastered, numGroup) => (
<Flex justifyItems="space-between">
<Flex.Item padding="0 x-small 0 0" size="0" grow>
const outcomeGroupHeader = (outcomeGroup, numMastered, numGroup, isVertical) => (
<Flex
padding="0 0 0 xxx-small"
justifyItems={isVertical ? null : 'space-between'}
direction={isVertical ? 'column' : 'row'}
>
<Flex.Item padding="0 x-small 0 0" size={isVertical ? undefined : '0'} grow>
<Text size="large" weight="bold">
<TruncateWithTooltip>{outcomeGroup.title}</TruncateWithTooltip>
</Text>
@ -41,18 +46,20 @@ const outcomeGroupHeader = (outcomeGroup, numMastered, numGroup) => (
</Flex>
)
export default class OutcomeGroup extends React.Component {
class OutcomeGroup extends React.Component {
static propTypes = {
outcomeGroup: shapes.outcomeGroupShape.isRequired,
outcomes: PropTypes.arrayOf(shapes.outcomeShape).isRequired,
expanded: PropTypes.bool.isRequired,
expandedOutcomes: ImmutablePropTypes.set.isRequired,
onExpansionChange: PropTypes.func.isRequired,
outcomeProficiency: shapes.outcomeProficiencyShape
outcomeProficiency: shapes.outcomeProficiencyShape,
breakpoints: breakpointsShape
}
static defaultProps = {
outcomeProficiency: null
outcomeProficiency: null,
breakpoints: {}
}
handleToggle = (_event, expanded) => {
@ -66,15 +73,17 @@ export default class OutcomeGroup extends React.Component {
expanded,
expandedOutcomes,
onExpansionChange,
outcomeProficiency
outcomeProficiency,
breakpoints
} = this.props
const numMastered = outcomes.filter(o => o.mastered).length
const numGroup = outcomes.length
const isVertical = !breakpoints.tablet
return (
<View as="div" className="outcomeGroup">
<ToggleGroup
summary={outcomeGroupHeader(outcomeGroup, numMastered, numGroup)}
summary={outcomeGroupHeader(outcomeGroup, numMastered, numGroup, isVertical)}
toggleLabel={I18n.t('Toggle outcomes for %{title}', {title: outcomeGroup.title})}
expanded={expanded}
onToggle={this.handleToggle}
@ -96,3 +105,5 @@ export default class OutcomeGroup extends React.Component {
)
}
}
export default WithBreakpoints(OutcomeGroup)

View File

@ -21,23 +21,25 @@ import _ from 'lodash'
import I18n from 'i18n!IndividualStudentMasteryOutcomePopover'
import {View, Flex} from '@instructure/ui-layout'
import {Text} from '@instructure/ui-elements'
import {Link} from '@instructure/ui-link'
import {ScreenReaderContent} from '@instructure/ui-a11y'
import CalculationMethodContent from 'compiled/models/grade_summary/CalculationMethodContent'
import {Popover} from '@instructure/ui-overlays'
import {IconInfoLine} from '@instructure/ui-icons'
import DatetimeDisplay from '../../shared/DatetimeDisplay'
import {CloseButton} from '@instructure/ui-buttons'
import {CloseButton, IconButton} from '@instructure/ui-buttons'
import {Modal} from '@instructure/ui-modal'
import WithBreakpoints, {breakpointsShape} from '../../shared/WithBreakpoints'
import * as shapes from './shapes'
export default class OutcomePopover extends React.Component {
class OutcomePopover extends React.Component {
static propTypes = {
outcome: shapes.outcomeShape.isRequired,
outcomeProficiency: shapes.outcomeProficiencyShape
outcomeProficiency: shapes.outcomeProficiencyShape,
breakpoints: breakpointsShape
}
static defaultProps = {
outcomeProficiency: null
outcomeProficiency: null,
breakpoints: {}
}
constructor() {
@ -76,17 +78,31 @@ export default class OutcomePopover extends React.Component {
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 _.sortBy(outcome.results, r => -r.submitted_or_assessed_at)[0].submitted_or_assessed_at
}
return null
}
renderPopoverContent() {
renderSelectedRating() {
const selectedRating = this.getSelectedRating()
return (
<Text size="small" weight="bold">
<div>
{selectedRating && (
<div style={{color: `#${selectedRating.color}`}}>{selectedRating.description}</div>
)}
</div>
</Text>
)
}
renderPopoverContent() {
const latestTime = this.latestTime()
const popoverContent = new CalculationMethodContent(this.props.outcome).present()
const {method, exampleText, exampleScores, exampleResult} = popoverContent
const {outcome} = this.props
const {outcome, breakpoints} = this.props
const isVertical = !breakpoints.miniTablet
return (
<View as="div" padding="large" maxWidth="30rem">
<CloseButton
@ -103,7 +119,9 @@ export default class OutcomePopover extends React.Component {
{outcome.title}
</div>
<div>
{isVertical && <div>{this.renderSelectedRating()}</div>}
{I18n.t('Last Assessment: ')}
{isVertical && <br />}
{latestTime ? (
<DatetimeDisplay datetime={latestTime} format="%b %d, %l:%M %p" />
) : (
@ -111,17 +129,7 @@ export default class OutcomePopover extends React.Component {
)}
</div>
</Flex.Item>
<Flex.Item grow shrink align="stretch">
<Text size="small" weight="bold">
<div>
{selectedRating && (
<div style={{color: `#${selectedRating.color}`, textAlign: 'end'}}>
{selectedRating.description}
</div>
)}
</div>
</Text>
</Flex.Item>
{!isVertical && <Flex.Item align="stretch">{this.renderSelectedRating()}</Flex.Item>}
</Flex>
<hr role="presentation" />
<div>
@ -143,8 +151,7 @@ export default class OutcomePopover extends React.Component {
)
}
render() {
const popoverContent = this.renderPopoverContent()
renderPopover() {
return (
<span>
<Popover
@ -155,26 +162,59 @@ export default class OutcomePopover extends React.Component {
shouldContainFocus
>
<Popover.Trigger>
<Link
<IconButton
size="small"
margin="xx-small"
withBackground={false}
withBorder={false}
screenReaderLabel={I18n.t('Click to expand outcome details')}
renderIcon={IconInfoLine}
onClick={() => this.setState(prevState => ({linkClicked: !prevState.linkClicked}))}
onMouseEnter={() => this.setState({linkHover: true})}
onMouseLeave={() => this.setState({linkHover: false})}
>
<span style={{color: 'black'}}>
<IconInfoLine />
</span>
<span>
{!this.state.linkClicked && (
<ScreenReaderContent>
{I18n.t('Click to expand outcome details')}
</ScreenReaderContent>
)}
</span>
</Link>
/>
</Popover.Trigger>
<Popover.Content>{popoverContent}</Popover.Content>
<Popover.Content>{this.renderPopoverContent()}</Popover.Content>
</Popover>
</span>
)
}
renderModal() {
return (
<span>
<IconButton
size="small"
margin="xx-small"
withBackground={false}
withBorder={false}
screenReaderLabel={I18n.t('Click to expand outcome details')}
renderIcon={IconInfoLine}
onClick={() => this.setState(prevState => ({linkClicked: !prevState.linkClicked}))}
/>
<Modal
open={this.state.linkClicked}
onDismiss={() =>
this.setState(prevState => prevState.linkClicked && this.setState({linkClicked: false}))
}
size="fullscreen"
label={I18n.t('Outcome Details')}
>
<Modal.Body>{this.renderPopoverContent()}</Modal.Body>
</Modal>
</span>
)
}
render() {
const {breakpoints} = this.props
const modalLayout = !breakpoints.miniTablet
if (modalLayout) {
return this.renderModal()
} else {
return this.renderPopover()
}
}
}
export default WithBreakpoints(OutcomePopover)

View File

@ -24,25 +24,22 @@ import {ApplyTheme} from '@instructure/ui-themeable'
import {View} from '@instructure/ui-layout'
import {Button} from '@instructure/ui-buttons'
import {IconAssignmentLine, IconQuizLine} from '@instructure/ui-icons'
import {List} from '@instructure/ui-elements'
const UnassessedAssignment = ({assignment}) => {
const {id, url, submission_types, title} = assignment
return (
<List.Item key={id}>
<View padding="small" display="block">
<ApplyTheme theme={{[Button.theme]: {linkColor: '#68777D', fontWeight: '700'}}}>
<Button
href={url}
variant="link"
theme={{mediumPadding: '0', mediumHeight: 'normal'}}
icon={_.includes(submission_types, 'online_quiz') ? IconQuizLine : IconAssignmentLine}
>
{title} ({I18n.t('Not yet assessed')})
</Button>
</ApplyTheme>
</View>
</List.Item>
<View padding="small" display="block" key={id}>
<ApplyTheme theme={{[Button.theme]: {linkColor: '#68777D', fontWeight: '700'}}}>
<Button
href={url}
variant="link"
theme={{mediumPadding: '0', mediumHeight: 'normal'}}
icon={_.includes(submission_types, 'online_quiz') ? IconQuizLine : IconAssignmentLine}
>
{title} ({I18n.t('Not yet assessed')})
</Button>
</ApplyTheme>
</View>
)
}

View File

@ -17,10 +17,10 @@
*/
import React from 'react'
import {render, shallow} from 'enzyme'
import {render, fireEvent, within} from '@testing-library/react'
import Outcome from '../Outcome'
const result = (id = 1, date = new Date(), hidePoints = false) => ({
const result = ({id = 1, date = new Date(), hide_points = false}, assignmentOverrides = {}) => ({
id,
percent: 0.1,
assignment: {
@ -28,9 +28,10 @@ const result = (id = 1, date = new Date(), hidePoints = false) => ({
html_url: 'http://foo',
name: 'My alignment',
submission_types: '',
score: 0
score: 0,
...assignmentOverrides
},
hide_points: hidePoints,
hide_points,
submitted_or_assessed_at: date.toISOString()
})
@ -42,11 +43,11 @@ const defaultProps = (props = {}) => ({
id: 1,
assignments: [
{
assignment_id: 1,
assignment_id: '1',
learning_outcome_id: 1,
submission_types: 'online_quiz',
title: 'My assignment',
url: 'www.example.com'
name: 'My assignment',
html_url: 'www.example.com'
}
],
expansionId: 100,
@ -76,6 +77,7 @@ const defaultProps = (props = {}) => ({
assignment: {
id: 'live_assessments/assessment_1',
name: 'My assessment',
html_url: 'http://bar',
submission_types: 'magic_marker',
score: 0
},
@ -90,93 +92,72 @@ const defaultProps = (props = {}) => ({
...props
})
it('renders the Outcome component', () => {
const wrapper = shallow(<Outcome {...defaultProps()} />)
expect(wrapper).toMatchSnapshot()
})
it('renders correctly expanded', () => {
const wrapper = shallow(<Outcome {...defaultProps()} expanded />)
expect(wrapper).toMatchSnapshot()
const {getByText, getByRole} = render(<Outcome {...defaultProps()} expanded />)
expect(getByText('My outcome')).not.toBeNull()
expect(getByRole('list')).not.toBeNull()
})
it('renders correctly expanded with no results', () => {
const props = defaultProps()
props.outcome.results = []
const wrapper = shallow(<Outcome {...props} expanded />)
expect(wrapper).toMatchSnapshot()
const {getByText} = render(<Outcome {...props} expanded />)
expect(getByText(/Not yet assessed/)).not.toBeNull()
})
it('renders correctly expanded with no results or assignments', () => {
const props = defaultProps()
props.outcome.results = []
props.outcome.assignments = []
const wrapper = shallow(<Outcome {...props} expanded />)
expect(wrapper).toMatchSnapshot()
const {getByText} = render(<Outcome {...props} expanded />)
expect(getByText(/No alignments are available/)).not.toBeNull()
})
describe('header', () => {
it('includes the outcome name', () => {
const wrapper = shallow(<Outcome {...defaultProps()} />)
const header = wrapper.find('ToggleGroup')
const summary = render(header.prop('summary'))
expect(summary.text()).toMatch('My outcome')
})
it('includes the outcome friendly name', () => {
const props = defaultProps()
props.outcome.display_name = 'Friendly name'
const wrapper = shallow(<Outcome {...props} />)
const header = wrapper.find('ToggleGroup')
const summary = render(header.prop('summary'))
expect(summary.text()).toMatch('Friendly name')
const {getByText} = render(<Outcome {...defaultProps()} />)
expect(getByText('My outcome')).not.toBeNull()
})
it('includes mastery when mastered', () => {
const props = defaultProps()
props.outcome.mastered = true
const wrapper = shallow(<Outcome {...props} />)
const header = wrapper.find('ToggleGroup')
const summary = render(header.prop('summary'))
expect(summary.text()).toMatch('Mastered')
const {getByText} = render(<Outcome {...props} />)
expect(getByText('Mastered')).not.toBeNull()
})
it('includes non-mastery when not mastered', () => {
const wrapper = shallow(<Outcome {...defaultProps()} />)
const header = wrapper.find('ToggleGroup')
const summary = render(header.prop('summary'))
expect(summary.text()).toMatch('Not mastered')
const {getByText} = render(<Outcome {...defaultProps()} />)
expect(getByText('Not mastered')).not.toBeNull()
})
it('shows correct number of alignments', () => {
const wrapper = shallow(<Outcome {...defaultProps()} />)
const header = wrapper.find('ToggleGroup')
const summary = render(header.prop('summary'))
expect(summary.text()).toMatch('1 alignment')
const {getByText} = render(<Outcome {...defaultProps()} />)
expect(getByText('1 alignment')).not.toBeNull()
})
it('shows points if only some results have hide points enabled', () => {
const props = defaultProps()
props.outcome.results = [result(1, undefined, false), result(2, undefined, true)]
const wrapper = shallow(<Outcome {...props} />)
const header = wrapper.find('ToggleGroup')
const summary = render(header.prop('summary'))
expect(summary.text()).toMatch('1/5')
props.outcome.results = [
result({id: 1, hide_points: false}),
result({id: 2, hide_points: true})
]
const {getByText} = render(<Outcome {...props} />)
expect(getByText('1/5')).not.toBeNull()
})
it('does not show points if all results have hide points enabled', () => {
const props = defaultProps()
props.outcome.results = [result(1, undefined, true), result(2, undefined, true)]
const wrapper = shallow(<Outcome {...props} />)
const header = wrapper.find('ToggleGroup')
const summary = render(header.prop('summary'))
expect(summary.text()).not.toMatch('1/5')
props.outcome.results = [result({id: 1, hide_points: true}), result({id: 2, hide_points: true})]
const {queryByText} = render(<Outcome {...props} />)
expect(queryByText('1/5')).toBeNull()
})
})
it('includes the individual results', () => {
const wrapper = shallow(<Outcome {...defaultProps()} />)
expect(wrapper.find('AssignmentResult')).toHaveLength(2)
const {getAllByRole} = render(<Outcome {...defaultProps()} expanded />)
const results = getAllByRole('listitem')
expect(results).toHaveLength(2)
})
it('renders the results by most recent', () => {
@ -186,19 +167,19 @@ it('renders the results by most recent', () => {
const hourAgo = new Date(now - 3600000)
const yearishAgo = new Date(now - 3600000 * 24 * 360)
props.outcome.results = [
result(1, hourAgo),
result(2, now),
result(3, minuteAgo),
result(4, yearishAgo)
result({id: 1, date: hourAgo}, {name: 'hour ago'}),
result({id: 2, date: now}, {name: 'now'}),
result({id: 3, date: minuteAgo}, {name: 'minute ago'}),
result({id: 4, date: yearishAgo}, {name: 'year ago'})
]
const wrapper = shallow(<Outcome {...props} />)
const results = wrapper.find('AssignmentResult')
const {getAllByRole} = render(<Outcome {...props} expanded />)
const results = getAllByRole('listitem')
expect(results).toHaveLength(4)
expect(results.get(0).props.result.id).toEqual(2) // now
expect(results.get(1).props.result.id).toEqual(3) // minuteAgo
expect(results.get(2).props.result.id).toEqual(1) // hourAgo
expect(results.get(3).props.result.id).toEqual(4) // yearishAgo
expect(within(results[0]).getByText('now')).not.toBeNull()
expect(within(results[1]).getByText('minute ago')).not.toBeNull()
expect(within(results[2]).getByText('hour ago')).not.toBeNull()
expect(within(results[3]).getByText('year ago')).not.toBeNull()
})
describe('handleToggle()', () => {
@ -206,8 +187,8 @@ describe('handleToggle()', () => {
const props = defaultProps()
props.onExpansionChange = jest.fn()
const wrapper = shallow(<Outcome {...props} />)
wrapper.instance().handleToggle(null, true)
const {getByText} = render(<Outcome {...props} />)
fireEvent.click(getByText(/Toggle alignment details/)) // expand
expect(props.onExpansionChange).toHaveBeenCalledWith('outcome', 100, true)
})
})

View File

@ -17,14 +17,16 @@
*/
import React from 'react'
import {render, shallow} from 'enzyme'
import {render, fireEvent, within} from '@testing-library/react'
import {Set} from 'immutable'
import OutcomeGroup from '../OutcomeGroup'
const outcome = (id, title) => ({
id,
title,
assignments: [],
assignments: [
{html_url: 'http://foo', id: 'assignment_2', name: 'My alignment', submission_types: 'none'}
],
mastered: false,
mastery_points: 3,
points_possible: 5,
@ -35,7 +37,7 @@ const outcome = (id, title) => ({
id: 1,
percent: 0.1,
assignment: {
id: 2,
id: 'assignment_2',
name: 'My alignment',
html_url: 'http://foo',
submission_types: ''
@ -49,31 +51,7 @@ const defaultProps = (props = {}) => ({
id: 10,
title: 'My group'
},
outcomes: [
{
id: 1,
expansionId: 100,
mastered: false,
mastery_points: 3,
points_possible: 5,
calculation_method: 'highest',
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'
}
}
],
title: 'My outcome'
}
],
outcomes: [outcome(1, 'My outcome')],
expanded: false,
expandedOutcomes: Set(),
onExpansionChange: () => {},
@ -81,27 +59,13 @@ const defaultProps = (props = {}) => ({
})
it('renders the OutcomeGroup component', () => {
const wrapper = shallow(<OutcomeGroup {...defaultProps()} />)
expect(wrapper).toMatchSnapshot()
})
describe('header', () => {
it('includes the outcome group name', () => {
const wrapper = shallow(<OutcomeGroup {...defaultProps()} />)
const header = wrapper.find('ToggleGroup')
const summary = render(header.prop('summary'))
expect(summary.text()).toMatch('My group')
})
const {getByText} = render(<OutcomeGroup {...defaultProps()} />)
expect(getByText('My group')).not.toBeNull()
})
it('includes the individual outcomes', () => {
const wrapper = shallow(<OutcomeGroup {...defaultProps()} />)
expect(wrapper.find('Outcome')).toHaveLength(1)
})
it('renders correctly expanded', () => {
const wrapper = shallow(<OutcomeGroup {...defaultProps()} expanded />)
expect(wrapper).toMatchSnapshot()
const {getByText} = render(<OutcomeGroup {...defaultProps()} expanded />)
expect(getByText('My outcome')).not.toBeNull()
})
it('renders outcomes in alphabetical order by title', () => {
@ -113,21 +77,22 @@ it('renders outcomes in alphabetical order by title', () => {
outcome(4, 'Aerosmith')
]
})
const wrapper = shallow(<OutcomeGroup {...props} />)
const outcomes = wrapper.find('Outcome')
const {getAllByRole} = render(<OutcomeGroup {...props} expanded />)
const outcomes = getAllByRole('listitem')
expect(outcomes).toHaveLength(4)
expect(outcomes.get(0).props.outcome.title).toEqual('Aardvark')
expect(outcomes.get(1).props.outcome.title).toEqual('abba')
expect(outcomes.get(2).props.outcome.title).toEqual('Aerosmith')
expect(outcomes.get(3).props.outcome.title).toEqual('ZZ Top')
expect(within(outcomes[0]).getByText('Aardvark')).not.toBeNull()
expect(within(outcomes[1]).getByText('abba')).not.toBeNull()
expect(within(outcomes[2]).getByText('Aerosmith')).not.toBeNull()
expect(within(outcomes[3]).getByText('ZZ Top')).not.toBeNull()
})
describe('handleToggle()', () => {
it('calls the correct onExpansionChange callback', () => {
const props = defaultProps()
props.onExpansionChange = jest.fn()
const wrapper = shallow(<OutcomeGroup {...props} />)
wrapper.instance().handleToggle(null, true)
const {getByRole} = render(<OutcomeGroup {...props} />)
const button = getByRole('button')
fireEvent.click(button)
expect(props.onExpansionChange).toHaveBeenCalledWith('group', 10, true)
})
})

View File

@ -17,11 +17,11 @@
*/
import React from 'react'
import {shallow} from 'enzyme'
import {render, fireEvent, within} from '@testing-library/react'
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 time1 = new Date('1995-12-17T03:24:00Z')
const time2 = new Date('1999-03-11T12:20:00Z')
const defaultProps = (props = {}) => ({
outcome: {
@ -33,7 +33,10 @@ const defaultProps = (props = {}) => ({
points_possible: 5,
calculation_method: 'highest',
score: 3,
ratings: [{description: 'My first rating'}, {description: 'My second rating'}],
ratings: [
{description: 'My first rating', mastery: true},
{description: 'My second rating', mastery: false}
],
results: [
{
id: 1,
@ -66,69 +69,90 @@ const defaultProps = (props = {}) => ({
},
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}
{color: 'blue', description: 'I am blue', points: 10, mastery: false},
{color: 'green', description: 'I am Groot', points: 5, mastery: true},
{color: 'red', description: 'I am red', points: 0, mastery: false}
]
},
...props
})
it('renders the OutcomePopover component', () => {
const wrapper = shallow(<OutcomePopover {...defaultProps()} />)
expect(wrapper).toMatchSnapshot()
const {getByText} = render(<OutcomePopover {...defaultProps()} />)
expect(getByText(/Click to expand/)).not.toBeNull()
})
it('renders correctly with no results', () => {
const props = defaultProps()
props.outcome.results = []
const wrapper = shallow(<OutcomePopover {...props} />)
expect(wrapper).toMatchSnapshot()
describe('modal mode', () => {
it('shows details on click', () => {
const {baseElement, getByRole} = render(<OutcomePopover {...defaultProps()} />)
const button = getByRole('button')
fireEvent.click(button)
expect(within(baseElement).getByText('Calculation Method')).not.toBeNull()
})
})
it('renders correctly with no custom outcomeProficiency', () => {
const props = defaultProps()
props.outcomeProficiency = null
const wrapper = shallow(<OutcomePopover {...props} />)
expect(wrapper).toMatchSnapshot()
})
it('properly expands details for screenreader users', () => {
const props = defaultProps()
const wrapper = shallow(<OutcomePopover {...props} />)
expect(wrapper.state('linkClicked')).toEqual(false)
wrapper.find('Link').simulate('click')
expect(wrapper.state('linkClicked')).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)
describe('popover mode', () => {
it('shows details on click', () => {
const {baseElement, getByRole} = render(
<OutcomePopover {...defaultProps()} breakpoints={{miniTablet: true}} />
)
const button = getByRole('button')
fireEvent.click(button)
expect(within(baseElement).getByText('Calculation Method')).not.toBeNull()
})
it('properly returns nothing when there are no results', () => {
it('shows details on hover', () => {
const {baseElement, getByRole} = render(
<OutcomePopover {...defaultProps()} breakpoints={{miniTablet: true}} />
)
const button = getByRole('button')
fireEvent.mouseEnter(button)
expect(within(baseElement).getByText('Calculation Method')).not.toBeNull()
})
it('removes details on leave', () => {
const {baseElement, getByRole} = render(
<OutcomePopover {...defaultProps()} breakpoints={{miniTablet: true}} />
)
const button = getByRole('button')
fireEvent.mouseEnter(button)
fireEvent.mouseLeave(button)
expect(within(baseElement).queryByText('Calculation Method')).toBeNull()
})
})
describe('latest time', () => {
it('renders correctly with no results', () => {
const props = defaultProps()
props.outcome.results = []
const wrapper = shallow(<OutcomePopover {...props} />)
expect(wrapper.instance().latestTime()).toBeNull()
const {baseElement, getByRole} = render(<OutcomePopover {...props} />)
const button = getByRole('button')
fireEvent.click(button)
expect(within(baseElement).getByText('Last Assessment: No submissions')).not.toBeNull()
})
it('properly returns the most recent submission time', () => {
const {baseElement, getByRole} = render(<OutcomePopover {...defaultProps()} />)
const button = getByRole('button')
fireEvent.click(button)
expect(within(baseElement).getByText(/Mar 11/)).not.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')
describe('selected rating', () => {
it('renders custom outcomeProficiency', () => {
const {baseElement, getByRole} = render(<OutcomePopover {...defaultProps()} />)
const button = getByRole('button')
fireEvent.click(button)
expect(within(baseElement).getByText('I am Groot')).not.toBeNull()
})
it('properly returns the default proficiency level', () => {
it('renders correct last assessment time with no custom outcomeProficiency', () => {
const props = defaultProps()
props.outcomeProficiency = null
const wrapper = shallow(<OutcomePopover {...props} />)
const rating = wrapper.instance().getSelectedRating()
expect(rating.description).toEqual('Meets Mastery')
const {baseElement, getByRole} = render(<OutcomePopover {...props} />)
const button = getByRole('button')
fireEvent.click(button)
expect(within(baseElement).getByText('Meets Mastery')).not.toBeNull()
})
})

View File

@ -1,251 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly expanded 1`] = `
<View
as="div"
borderColor="default"
className="outcomeGroup"
debug={false}
display="auto"
focusColor="info"
focusPosition="offset"
focused={false}
overflowX="visible"
overflowY="visible"
position="static"
shouldAnimateFocus={true}
>
<ToggleGroup
as="span"
border={true}
defaultExpanded={false}
elementRef={[Function]}
expanded={true}
icon={[Function]}
iconExpanded={[Function]}
onToggle={[Function]}
size="medium"
summary={
<Flex
as="span"
direction="row"
elementRef={[Function]}
inline={false}
justifyItems="space-between"
visualDebug={false}
wrapItems={false}
>
<FlexItem
as="span"
elementRef={[Function]}
grow={true}
padding="0 x-small 0 0"
shrink={false}
size="0"
>
<Text
as="span"
letterSpacing="normal"
size="large"
weight="bold"
wrap="normal"
>
<TruncateWithTooltip>
My group
</TruncateWithTooltip>
</Text>
</FlexItem>
<FlexItem
as="span"
elementRef={[Function]}
grow={false}
shrink={false}
>
<Pill
text="0 of 1 Mastered"
variant="default"
/>
</FlexItem>
</Flex>
}
toggleLabel="Toggle outcomes for My group"
transition={true}
>
<List
as="ul"
delimiter="solid"
margin="none"
size="medium"
variant="unstyled"
>
<ListItem
key="1"
margin="0"
>
<Outcome
expanded={false}
onExpansionChange={[Function]}
outcome={
Object {
"calculation_method": "highest",
"expansionId": 100,
"id": 1,
"mastered": false,
"mastery_points": 3,
"points_possible": 5,
"ratings": Array [
Object {
"description": "My first rating",
},
Object {
"description": "My second rating",
},
],
"results": Array [
Object {
"assignment": Object {
"html_url": "http://foo",
"id": 1,
"name": "My assignment",
"submission_types": "online_quiz",
},
"id": 1,
"percent": 0.1,
"score": 1,
},
],
"title": "My outcome",
}
}
outcomeProficiency={null}
/>
</ListItem>
</List>
</ToggleGroup>
</View>
`;
exports[`renders the OutcomeGroup component 1`] = `
<View
as="div"
borderColor="default"
className="outcomeGroup"
debug={false}
display="auto"
focusColor="info"
focusPosition="offset"
focused={false}
overflowX="visible"
overflowY="visible"
position="static"
shouldAnimateFocus={true}
>
<ToggleGroup
as="span"
border={true}
defaultExpanded={false}
elementRef={[Function]}
expanded={false}
icon={[Function]}
iconExpanded={[Function]}
onToggle={[Function]}
size="medium"
summary={
<Flex
as="span"
direction="row"
elementRef={[Function]}
inline={false}
justifyItems="space-between"
visualDebug={false}
wrapItems={false}
>
<FlexItem
as="span"
elementRef={[Function]}
grow={true}
padding="0 x-small 0 0"
shrink={false}
size="0"
>
<Text
as="span"
letterSpacing="normal"
size="large"
weight="bold"
wrap="normal"
>
<TruncateWithTooltip>
My group
</TruncateWithTooltip>
</Text>
</FlexItem>
<FlexItem
as="span"
elementRef={[Function]}
grow={false}
shrink={false}
>
<Pill
text="0 of 1 Mastered"
variant="default"
/>
</FlexItem>
</Flex>
}
toggleLabel="Toggle outcomes for My group"
transition={true}
>
<List
as="ul"
delimiter="solid"
margin="none"
size="medium"
variant="unstyled"
>
<ListItem
key="1"
margin="0"
>
<Outcome
expanded={false}
onExpansionChange={[Function]}
outcome={
Object {
"calculation_method": "highest",
"expansionId": 100,
"id": 1,
"mastered": false,
"mastery_points": 3,
"points_possible": 5,
"ratings": Array [
Object {
"description": "My first rating",
},
Object {
"description": "My second rating",
},
],
"results": Array [
Object {
"assignment": Object {
"html_url": "http://foo",
"id": 1,
"name": "My assignment",
"submission_types": "online_quiz",
},
"id": 1,
"percent": 0.1,
"score": 1,
},
],
"title": "My outcome",
}
}
outcomeProficiency={null}
/>
</ListItem>
</List>
</ToggleGroup>
</View>
`;

View File

@ -1,655 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly with no custom outcomeProficiency 1`] = `
<span>
<Popover
alignArrow={false}
constrain="window"
contentRef={[Function]}
defaultFocusElement={null}
defaultShow={false}
insertAt="bottom"
label={null}
liveRegion={null}
mountNode={null}
offsetX={0}
offsetY={0}
on={
Array [
"hover",
"click",
]
}
onBlur={[Function]}
onClick={[Function]}
onDismiss={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onPositionChanged={[Function]}
onPositioned={[Function]}
onShow={[Function]}
onToggle={[Function]}
placement="bottom"
positionTarget={null}
shadow="resting"
shouldCloseOnDocumentClick={true}
shouldCloseOnEscape={true}
shouldContainFocus={true}
shouldFocusContentOnTriggerBlur={false}
shouldRenderOffscreen={false}
shouldReturnFocus={true}
show={false}
stacking="topmost"
trackPosition={true}
variant="default"
withArrow={true}
>
<PopoverTrigger>
<Link
color="link"
iconPlacement="start"
isWithinText={true}
onClick={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<span
style={
Object {
"color": "black",
}
}
>
<IconInfoLine />
</span>
<span>
<ScreenReaderContent
as="span"
>
Click to expand outcome details
</ScreenReaderContent>
</span>
</Link>
</PopoverTrigger>
<PopoverContent>
<View
as="div"
borderColor="default"
debug={false}
display="auto"
focusColor="info"
focusPosition="offset"
focused={false}
maxWidth="30rem"
overflowX="visible"
overflowY="visible"
padding="large"
position="static"
shouldAnimateFocus={true}
>
<CloseButton
as="button"
cursor="pointer"
margin="0"
offset="x-small"
onClick={[Function]}
placement="end"
size="small"
type="button"
>
Click to close outcome details popover
</CloseButton>
<Text
as="span"
letterSpacing="normal"
size="small"
wrap="normal"
>
<Flex
alignItems="stretch"
as="span"
direction="row"
elementRef={[Function]}
inline={false}
justifyItems="space-between"
visualDebug={false}
wrapItems={false}
>
<FlexItem
as="span"
elementRef={[Function]}
grow={true}
shrink={true}
>
<div
style={
Object {
"overflowWrap": "break-word",
"wordWrap": "break-word",
}
}
>
My outcome
</div>
<div>
Last Assessment:
<DatetimeDisplay
datetime={2018-02-01T07:01:00.000Z}
format="%b %d, %l:%M %p"
/>
</div>
</FlexItem>
<FlexItem
align="stretch"
as="span"
elementRef={[Function]}
grow={true}
shrink={true}
>
<Text
as="span"
letterSpacing="normal"
size="small"
weight="bold"
wrap="normal"
>
<div>
<div
style={
Object {
"color": "#00AC18",
"textAlign": "end",
}
}
>
Meets Mastery
</div>
</div>
</Text>
</FlexItem>
</Flex>
<hr
role="presentation"
/>
<div>
<Text
as="span"
letterSpacing="normal"
size="small"
weight="bold"
wrap="normal"
>
Calculation Method
</Text>
<div>
Highest Score
</div>
<div
style={
Object {
"padding": "0.5rem 0 0 0",
}
}
>
<Text
as="span"
letterSpacing="normal"
size="small"
weight="bold"
wrap="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>
</span>
`;
exports[`renders correctly with no results 1`] = `
<span>
<Popover
alignArrow={false}
constrain="window"
contentRef={[Function]}
defaultFocusElement={null}
defaultShow={false}
insertAt="bottom"
label={null}
liveRegion={null}
mountNode={null}
offsetX={0}
offsetY={0}
on={
Array [
"hover",
"click",
]
}
onBlur={[Function]}
onClick={[Function]}
onDismiss={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onPositionChanged={[Function]}
onPositioned={[Function]}
onShow={[Function]}
onToggle={[Function]}
placement="bottom"
positionTarget={null}
shadow="resting"
shouldCloseOnDocumentClick={true}
shouldCloseOnEscape={true}
shouldContainFocus={true}
shouldFocusContentOnTriggerBlur={false}
shouldRenderOffscreen={false}
shouldReturnFocus={true}
show={false}
stacking="topmost"
trackPosition={true}
variant="default"
withArrow={true}
>
<PopoverTrigger>
<Link
color="link"
iconPlacement="start"
isWithinText={true}
onClick={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<span
style={
Object {
"color": "black",
}
}
>
<IconInfoLine />
</span>
<span>
<ScreenReaderContent
as="span"
>
Click to expand outcome details
</ScreenReaderContent>
</span>
</Link>
</PopoverTrigger>
<PopoverContent>
<View
as="div"
borderColor="default"
debug={false}
display="auto"
focusColor="info"
focusPosition="offset"
focused={false}
maxWidth="30rem"
overflowX="visible"
overflowY="visible"
padding="large"
position="static"
shouldAnimateFocus={true}
>
<CloseButton
as="button"
cursor="pointer"
margin="0"
offset="x-small"
onClick={[Function]}
placement="end"
size="small"
type="button"
>
Click to close outcome details popover
</CloseButton>
<Text
as="span"
letterSpacing="normal"
size="small"
wrap="normal"
>
<Flex
alignItems="stretch"
as="span"
direction="row"
elementRef={[Function]}
inline={false}
justifyItems="space-between"
visualDebug={false}
wrapItems={false}
>
<FlexItem
as="span"
elementRef={[Function]}
grow={true}
shrink={true}
>
<div
style={
Object {
"overflowWrap": "break-word",
"wordWrap": "break-word",
}
}
>
My outcome
</div>
<div>
Last Assessment:
No submissions
</div>
</FlexItem>
<FlexItem
align="stretch"
as="span"
elementRef={[Function]}
grow={true}
shrink={true}
>
<Text
as="span"
letterSpacing="normal"
size="small"
weight="bold"
wrap="normal"
>
<div>
<div
style={
Object {
"color": "#green",
"textAlign": "end",
}
}
>
I am Groot
</div>
</div>
</Text>
</FlexItem>
</Flex>
<hr
role="presentation"
/>
<div>
<Text
as="span"
letterSpacing="normal"
size="small"
weight="bold"
wrap="normal"
>
Calculation Method
</Text>
<div>
Highest Score
</div>
<div
style={
Object {
"padding": "0.5rem 0 0 0",
}
}
>
<Text
as="span"
letterSpacing="normal"
size="small"
weight="bold"
wrap="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>
</span>
`;
exports[`renders the OutcomePopover component 1`] = `
<span>
<Popover
alignArrow={false}
constrain="window"
contentRef={[Function]}
defaultFocusElement={null}
defaultShow={false}
insertAt="bottom"
label={null}
liveRegion={null}
mountNode={null}
offsetX={0}
offsetY={0}
on={
Array [
"hover",
"click",
]
}
onBlur={[Function]}
onClick={[Function]}
onDismiss={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onPositionChanged={[Function]}
onPositioned={[Function]}
onShow={[Function]}
onToggle={[Function]}
placement="bottom"
positionTarget={null}
shadow="resting"
shouldCloseOnDocumentClick={true}
shouldCloseOnEscape={true}
shouldContainFocus={true}
shouldFocusContentOnTriggerBlur={false}
shouldRenderOffscreen={false}
shouldReturnFocus={true}
show={false}
stacking="topmost"
trackPosition={true}
variant="default"
withArrow={true}
>
<PopoverTrigger>
<Link
color="link"
iconPlacement="start"
isWithinText={true}
onClick={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<span
style={
Object {
"color": "black",
}
}
>
<IconInfoLine />
</span>
<span>
<ScreenReaderContent
as="span"
>
Click to expand outcome details
</ScreenReaderContent>
</span>
</Link>
</PopoverTrigger>
<PopoverContent>
<View
as="div"
borderColor="default"
debug={false}
display="auto"
focusColor="info"
focusPosition="offset"
focused={false}
maxWidth="30rem"
overflowX="visible"
overflowY="visible"
padding="large"
position="static"
shouldAnimateFocus={true}
>
<CloseButton
as="button"
cursor="pointer"
margin="0"
offset="x-small"
onClick={[Function]}
placement="end"
size="small"
type="button"
>
Click to close outcome details popover
</CloseButton>
<Text
as="span"
letterSpacing="normal"
size="small"
wrap="normal"
>
<Flex
alignItems="stretch"
as="span"
direction="row"
elementRef={[Function]}
inline={false}
justifyItems="space-between"
visualDebug={false}
wrapItems={false}
>
<FlexItem
as="span"
elementRef={[Function]}
grow={true}
shrink={true}
>
<div
style={
Object {
"overflowWrap": "break-word",
"wordWrap": "break-word",
}
}
>
My outcome
</div>
<div>
Last Assessment:
<DatetimeDisplay
datetime={2018-02-01T07:01:00.000Z}
format="%b %d, %l:%M %p"
/>
</div>
</FlexItem>
<FlexItem
align="stretch"
as="span"
elementRef={[Function]}
grow={true}
shrink={true}
>
<Text
as="span"
letterSpacing="normal"
size="small"
weight="bold"
wrap="normal"
>
<div>
<div
style={
Object {
"color": "#green",
"textAlign": "end",
}
}
>
I am Groot
</div>
</div>
</Text>
</FlexItem>
</Flex>
<hr
role="presentation"
/>
<div>
<Text
as="span"
letterSpacing="normal"
size="small"
weight="bold"
wrap="normal"
>
Calculation Method
</Text>
<div>
Highest Score
</div>
<div
style={
Object {
"padding": "0.5rem 0 0 0",
}
}
>
<Text
as="span"
letterSpacing="normal"
size="small"
weight="bold"
wrap="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>
</span>
`;

View File

@ -1,59 +1,56 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`properly renders the UnassessedAssignment component 1`] = `
<ListItem
<View
borderColor="default"
debug={false}
display="block"
focusColor="info"
focusPosition="offset"
focused={false}
key="1"
overflowX="visible"
overflowY="visible"
padding="small"
position="static"
shouldAnimateFocus={true}
>
<View
borderColor="default"
debug={false}
display="block"
focusColor="info"
focusPosition="offset"
focused={false}
overflowX="visible"
overflowY="visible"
padding="small"
position="static"
shouldAnimateFocus={true}
<ApplyTheme
immutable={false}
theme={
Object {
Symbol(Button__MTEzMjky): Object {
"fontWeight": "700",
"linkColor": "#68777D",
},
}
}
>
<ApplyTheme
immutable={false}
<Button
as="button"
cursor="pointer"
display="inline-block"
elementRef={[Function]}
href="www.example.com"
icon={[Function]}
margin="0"
size="medium"
textAlign="center"
theme={
Object {
Symbol(Button__MTEzMjky): Object {
"fontWeight": "700",
"linkColor": "#68777D",
},
"mediumHeight": "normal",
"mediumPadding": "0",
}
}
type="button"
variant="link"
withBackground={true}
>
<Button
as="button"
cursor="pointer"
display="inline-block"
elementRef={[Function]}
href="www.example.com"
icon={[Function]}
margin="0"
size="medium"
textAlign="center"
theme={
Object {
"mediumHeight": "normal",
"mediumPadding": "0",
}
}
type="button"
variant="link"
withBackground={true}
>
example
(
Not yet assessed
)
</Button>
</ApplyTheme>
</View>
</ListItem>
example
(
Not yet assessed
)
</Button>
</ApplyTheme>
</View>
`;

View File

@ -1,29 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders the component 1`] = `
<Flex
alignItems="center"
as="span"
direction="row"
elementRef={[Function]}
inline={false}
justifyItems="center"
padding="medium"
visualDebug={false}
wrapItems={false}
>
<FlexItem
as="span"
elementRef={[Function]}
grow={false}
shrink={false}
>
<Spinner
as="div"
renderTitle="Loading outcome results"
size="large"
variant="default"
/>
</FlexItem>
</Flex>
`;

View File

@ -18,6 +18,7 @@
import React from 'react'
import {mount, shallow} from 'enzyme'
import {render, within} from '@testing-library/react'
import {Set} from 'immutable'
import IndividualStudentMastery from '../index'
import fetchOutcomes from '../fetchOutcomes'
@ -35,33 +36,25 @@ const props = {
}
it('renders the component', () => {
const wrapper = shallow(<IndividualStudentMastery {...props} />)
expect(wrapper).toMatchSnapshot()
const {getByText} = render(<IndividualStudentMastery {...props} />)
expect(getByText('Loading outcome results')).not.toBeNull()
})
it('attempts to load when mounted', () => {
mount(<IndividualStudentMastery {...props} />)
render(<IndividualStudentMastery {...props} />)
expect(fetchOutcomes).toHaveBeenCalled()
})
it('renders loading before promise resolves', () => {
fetchOutcomes.mockImplementation(() => new Promise(() => {})) // unresolved promise
const wrapper = mount(<IndividualStudentMastery {...props} />)
expect(wrapper.find('Spinner')).toHaveLength(1)
})
it('renders error when error occurs during fetch', async () => {
fetchOutcomes.mockImplementation(() => Promise.reject(new Error('foo')))
const wrapper = mount(<IndividualStudentMastery {...props} />)
await wrapper.instance().componentDidMount()
expect(wrapper.text()).toMatch('An error occurred')
const {findByText} = render(<IndividualStudentMastery {...props} />)
expect(await findByText(/An error occurred/)).not.toBeNull()
})
it('renders empty if no groups are returned', async () => {
fetchOutcomes.mockImplementation(() => Promise.resolve({outcomeGroups: [], outcomes: []}))
const wrapper = mount(<IndividualStudentMastery {...props} />)
await wrapper.instance().componentDidMount()
expect(wrapper.text()).toMatch('There are no outcomes in the course')
const {findByText} = render(<IndividualStudentMastery {...props} />)
expect(await findByText(/There are no outcomes in the course/)).not.toBeNull()
})
it('renders outcome groups if they are returned', async () => {
@ -71,9 +64,8 @@ it('renders outcome groups if they are returned', async () => {
outcomes: []
})
)
const wrapper = shallow(<IndividualStudentMastery {...props} />)
await wrapper.instance().componentDidMount()
expect(wrapper.update().find('OutcomeGroup')).toHaveLength(1)
const {findByText} = render(<IndividualStudentMastery {...props} />)
expect(await findByText('Group')).not.toBeNull()
})
describe('expand and contract', () => {
@ -100,6 +92,28 @@ describe('expand and contract', () => {
)
})
it('renders outcome groups in alphabetical order by title', async () => {
fetchOutcomes.mockImplementation(() =>
Promise.resolve({
outcomeGroups: [
{id: 1, title: 'ZZ Top Albums'},
{id: 2, title: 'Aerosmith Albums'},
{id: 3, title: 'Aardvark Albums'},
{id: 4, title: 'abba Albums'}
],
outcomes: []
})
)
const {findAllByRole} = render(<IndividualStudentMastery {...props} />)
const groups = await findAllByRole('listitem')
expect(groups).toHaveLength(4)
expect(within(groups[0]).getByText('Aardvark Albums')).not.toBeNull()
expect(within(groups[1]).getByText('abba Albums')).not.toBeNull()
expect(within(groups[2]).getByText('Aerosmith Albums')).not.toBeNull()
expect(within(groups[3]).getByText('ZZ Top Albums')).not.toBeNull()
})
// legacy enzyme tests
it('toggles elements to expanded when event fired', async () => {
const wrapper = mount(<IndividualStudentMastery {...props} />)
await wrapper.instance().componentDidMount()
@ -147,25 +161,3 @@ describe('expand and contract', () => {
expect(props.onExpansionChange).toHaveBeenLastCalledWith(false, true)
})
})
it('renders outcome groups in alphabetical order by title', async () => {
fetchOutcomes.mockImplementation(() =>
Promise.resolve({
outcomeGroups: [
{id: 1, title: 'ZZ Top Albums'},
{id: 2, title: 'Aerosmith Albums'},
{id: 3, title: 'Aardvark Albums'},
{id: 4, title: 'abba Albums'}
],
outcomes: []
})
)
const wrapper = shallow(<IndividualStudentMastery {...props} />)
await wrapper.instance().componentDidMount()
const groups = wrapper.find('OutcomeGroup')
expect(groups).toHaveLength(4)
expect(groups.get(0).props.outcomeGroup.title).toEqual('Aardvark Albums')
expect(groups.get(1).props.outcomeGroup.title).toEqual('abba Albums')
expect(groups.get(2).props.outcomeGroup.title).toEqual('Aerosmith Albums')
expect(groups.get(3).props.outcomeGroup.title).toEqual('ZZ Top Albums')
})

View File

@ -38,9 +38,18 @@ export const ratingShape = PropTypes.shape({
description: PropTypes.string.isRequired
})
export const alignmentShape = PropTypes.shape({
assignment_id: PropTypes.string,
learning_outcome_id: PropTypes.number,
submission_types: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
title: PropTypes.string,
url: PropTypes.string
})
export const outcomeShape = PropTypes.shape({
id: PropTypes.number.isRequired,
mastered: PropTypes.bool.isRequired,
mastered: PropTypes.bool,
assignments: PropTypes.arrayOf(alignmentShape),
ratings: PropTypes.arrayOf(ratingShape).isRequired,
results: PropTypes.arrayOf(outcomeResultShape).isRequired,
title: PropTypes.string.isRequired,

View File

@ -0,0 +1,64 @@
/*
* Copyright (C) 2020 - 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 PropTypes from 'prop-types'
import {Responsive} from '@instructure/ui-responsive'
// from _breakpoints.scss
export const BREAKPOINTS = {
miniTablet: {minWidth: '500px'},
tablet: {minWidth: '768px'},
desktop: {minWidth: '992px'},
desktopNavOpen: {minWidth: '1140px'},
desktopOnly: {minWidth: '768px'},
mobileOnly: {maxWidth: '767px'}
}
const convertMatchesToProp = matches => {
const breakpoints = {}
matches.forEach(match => (breakpoints[match] = true))
return breakpoints
}
const WithBreakpoints = Component => props => {
if (window.matchMedia) {
return (
<Responsive
match="media"
query={BREAKPOINTS}
render={(_addedProps, matches) => (
<Component breakpoints={convertMatchesToProp(matches)} {...props} />
)}
/>
)
} else {
return <Component breakpoints={{}} {...props} />
}
}
export const breakpointsShape = PropTypes.shape({
miniTablet: PropTypes.bool,
tablet: PropTypes.bool,
desktop: PropTypes.bool,
desktopNavOpen: PropTypes.bool,
desktopOnly: PropTypes.bool,
mobileOnly: PropTypes.bool
})
export default WithBreakpoints

View File

@ -0,0 +1,134 @@
/*
* Copyright (C) 2020 - 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 {render} from '@testing-library/react'
import WithBreakpoints from '../WithBreakpoints'
const testWidthQuery = (query, width) => {
const minWidth = query.match(/min-width: ([0-9]+)px/)?.[1]
const maxWidth = query.match(/max-width: ([0-9]+)px/)?.[1]
if (minWidth && width > parseFloat(minWidth)) {
return true
}
if (maxWidth && width < parseFloat(maxWidth)) {
return true
}
return false
}
describe('WithBreakpoints', () => {
const renderer = jest.fn(() => <div>Hello, world!</div>)
const Wrapped = WithBreakpoints(renderer)
let matchMedia
const mockWindowWidth = width => {
if (window.matchMedia) {
throw new Error('cannot mock when window.mediaQuery is defined')
}
matchMedia = query => ({
matches: testWidthQuery(query, width),
addListener: Function.prototype,
removeListener: Function.prototype
})
window.matchMedia = matchMedia
}
afterEach(() => {
renderer.mockClear()
if (matchMedia) {
delete window.matchMedia
}
})
describe('without matchMedia defined', () => {
it('renders inner component', () => {
const {getByText} = render(<Wrapped />)
expect(getByText('Hello, world!')).not.toBeNull()
})
it('does not include any breakpoints', () => {
render(<Wrapped />)
const breakpoints = renderer.mock.calls[0][0].breakpoints
expect(Object.keys(breakpoints)).toEqual([])
})
})
describe('with matchMedia defined', () => {
it('renders inner component', () => {
mockWindowWidth(1)
const {getByText} = render(<Wrapped />)
expect(getByText('Hello, world!')).not.toBeNull()
})
it('calls inner component once', () => {
mockWindowWidth(1)
render(<Wrapped />)
expect(renderer).toHaveBeenCalledTimes(1)
})
describe('breakpoints prop', () => {
it('includes breakpoints prop', () => {
mockWindowWidth(1)
render(<Wrapped />)
expect(renderer.mock.calls[0][0]).toMatchObject({breakpoints: expect.any(Object)})
})
it('includes correct breakpoints for small screen', () => {
mockWindowWidth(1)
render(<Wrapped />)
const breakpoints = renderer.mock.calls[0][0].breakpoints
expect(Object.keys(breakpoints)).toEqual(['mobileOnly'])
})
it('includes miniTablet breakpoint for miniTablet screen', () => {
mockWindowWidth(550)
render(<Wrapped />)
const breakpoints = renderer.mock.calls[0][0].breakpoints
expect(Object.keys(breakpoints)).toEqual(['miniTablet', 'mobileOnly'])
})
it('includes tablet breakpoints for tablet screen', () => {
mockWindowWidth(800)
render(<Wrapped />)
const breakpoints = renderer.mock.calls[0][0].breakpoints
expect(Object.keys(breakpoints)).toEqual(['miniTablet', 'tablet', 'desktopOnly'])
})
it('includes desktop breakpoints for desktop screen', () => {
mockWindowWidth(1000)
render(<Wrapped />)
const breakpoints = renderer.mock.calls[0][0].breakpoints
expect(Object.keys(breakpoints)).toEqual(['miniTablet', 'tablet', 'desktop', 'desktopOnly'])
})
it('includes desktopNavOpen breakpoints for large desktop screen', () => {
mockWindowWidth(2000)
render(<Wrapped />)
const breakpoints = renderer.mock.calls[0][0].breakpoints
expect(Object.keys(breakpoints)).toEqual([
'miniTablet',
'tablet',
'desktop',
'desktopNavOpen',
'desktopOnly'
])
})
})
})
})

View File

@ -22,7 +22,6 @@
// print button for grades summary page
.print-grades {
float: direction(right);
@media print {
display: none;
}
@ -323,12 +322,19 @@ div.rubric-toggle {
}
.outcome-toggles {
float: direction(right);
position: absolute;
#{direction(right)}: 1em;
margin-top: -2.3em;
text-align: #{direction(right)};
margin-top: 0.75rem;
@include breakpoint(mini-tablet) {
float: direction(right);
padding-top: 0;
position: absolute;
#{direction(right)}: 1em;
margin-top: -2.3em;
}
.btn {
margin-#{direction(left)}: 0.5em;
margin-#{direction(left)}: 0.75em;
}
a[class*=icon-]:before, a[class^=icon-]:before {
margin: 0;

View File

@ -894,6 +894,9 @@ form.ic-Form-group { margin: 0; }
.ic-Action-header {
margin-bottom: $ic-sp*2;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: start;
&.ic-Action-header--before-item-groups {
margin-bottom: 0;
@ -902,9 +905,10 @@ form.ic-Form-group { margin: 0; }
}
@include breakpoint(tablet) {
display: flex;
align-items: center;
box-sizing: border-box;
flex-direction: row;
justify-content: space-between;
.ic-Action-header__Primary {
flex-grow: 1;

View File

@ -16,7 +16,7 @@
*/
import React from 'react'
import {mount, shallow} from 'enzyme'
import {mount} from 'enzyme'
import SelectMenuGroup from 'jsx/grade_summary/SelectMenuGroup'
@ -40,9 +40,15 @@ QUnit.module('SelectMenuGroup', suiteHooks => {
{id: '60', nickname: 'Firebending', url: '/courses/60/grades', gradingPeriodSetId: '4'}
]
const gradingPeriods = [{id: '9', title: 'Fall Semester'}, {id: '12', title: 'Spring Semester'}]
const gradingPeriods = [
{id: '9', title: 'Fall Semester'},
{id: '12', title: 'Spring Semester'}
]
const students = [{id: '7', name: 'Bob Smith'}, {id: '11', name: 'Jane Doe'}]
const students = [
{id: '7', name: 'Bob Smith'},
{id: '11', name: 'Jane Doe'}
]
props = {
assignmentSortOptions,
@ -66,12 +72,12 @@ QUnit.module('SelectMenuGroup', suiteHooks => {
})
test('renders a student select menu if the students prop has more than 1 student', () => {
wrapper = shallow(<SelectMenuGroup {...props} />)
strictEqual(wrapper.find('#student_select_menu').length, 1)
wrapper = mount(<SelectMenuGroup {...props} />)
strictEqual(wrapper.find('SelectMenu#student_select_menu').length, 1)
})
test('does not render a student select menu if the students prop has only 1 student', () => {
wrapper = shallow(<SelectMenuGroup {...props} students={[{id: '11', name: 'Jane Doe'}]} />)
wrapper = mount(<SelectMenuGroup {...props} students={[{id: '11', name: 'Jane Doe'}]} />)
strictEqual(wrapper.find('#student_select_menu').length, 0)
})
@ -89,8 +95,8 @@ QUnit.module('SelectMenuGroup', suiteHooks => {
})
test('renders a grading period select menu if passed any grading periods', () => {
wrapper = shallow(<SelectMenuGroup {...props} />)
strictEqual(wrapper.find('#grading_period_select_menu').length, 1)
wrapper = mount(<SelectMenuGroup {...props} />)
strictEqual(wrapper.find('SelectMenu#grading_period_select_menu').length, 1)
})
test('includes "All Grading Periods" as an option in the grading period select menu', () => {
@ -105,7 +111,7 @@ QUnit.module('SelectMenuGroup', suiteHooks => {
})
test('does not render a grading period select menu if passed no grading periods', () => {
wrapper = shallow(<SelectMenuGroup {...props} gradingPeriods={[]} />)
wrapper = mount(<SelectMenuGroup {...props} gradingPeriods={[]} />)
strictEqual(wrapper.find('#grading_period_select_menu').length, 0)
})
@ -116,12 +122,12 @@ QUnit.module('SelectMenuGroup', suiteHooks => {
})
test('renders a course select menu if the courses prop has more than 1 course', () => {
wrapper = shallow(<SelectMenuGroup {...props} />)
strictEqual(wrapper.find('#course_select_menu').length, 1)
wrapper = mount(<SelectMenuGroup {...props} />)
strictEqual(wrapper.find('SelectMenu#course_select_menu').length, 1)
})
test('does not render a course select menu if the courses prop has only 1 course', () => {
wrapper = shallow(
wrapper = mount(
<SelectMenuGroup
{...props}
courses={[{id: '2', nickname: 'Autos', url: '/courses/2/grades'}]}
@ -169,13 +175,13 @@ QUnit.module('SelectMenuGroup', suiteHooks => {
})
test('renders a submit button', () => {
wrapper = shallow(<SelectMenuGroup {...props} />)
strictEqual(wrapper.find('#apply_select_menus').length, 1)
wrapper = mount(<SelectMenuGroup {...props} />)
strictEqual(wrapper.find('button#apply_select_menus').length, 1)
})
test('disables the submit button if no select menu options have changed', () => {
wrapper = mount(<SelectMenuGroup {...props} />)
const submitButton = wrapper.find('Button[id="apply_select_menus"]')
const submitButton = wrapper.find('button#apply_select_menus')
strictEqual(submitButton.prop('disabled'), true)
})
@ -185,7 +191,7 @@ QUnit.module('SelectMenuGroup', suiteHooks => {
.find('#student_select_menu')
.last()
.simulate('change', {target: {value: '7'}})
const submitButton = wrapper.find('Button[id="apply_select_menus"]')
const submitButton = wrapper.find('button#apply_select_menus')
strictEqual(submitButton.prop('disabled'), false)
})
@ -195,20 +201,17 @@ QUnit.module('SelectMenuGroup', suiteHooks => {
.find('#student_select_menu')
.last()
.simulate('change', {target: {value: '7'}})
wrapper
.find('Button[id="apply_select_menus"]')
.find('button')
.simulate('click')
strictEqual(wrapper.find('Button[id="apply_select_menus"]').prop('disabled'), true)
wrapper.find('button#apply_select_menus').simulate('click')
strictEqual(wrapper.find('button#apply_select_menus').prop('disabled'), true)
})
test('calls saveAssignmentOrder when the button is clicked, if assignment order has changed', () => {
const stub = sinon.stub().resolves()
wrapper = shallow(<SelectMenuGroup {...props} saveAssignmentOrder={stub} />)
wrapper = mount(<SelectMenuGroup {...props} saveAssignmentOrder={stub} />)
wrapper
.find('#assignment_sort_order_select_menu')
.find('select#assignment_sort_order_select_menu')
.simulate('change', {target: {value: 'title'}})
const submitButton = wrapper.find('#apply_select_menus')
const submitButton = wrapper.find('button#apply_select_menus')
submitButton.simulate('click')
strictEqual(stub.callCount, 1)
})
@ -220,7 +223,7 @@ QUnit.module('SelectMenuGroup', suiteHooks => {
.find('#student_select_menu')
.last()
.simulate('change', {target: {value: '7'}})
const submitButton = wrapper.find('#apply_select_menus').last()
const submitButton = wrapper.find('button#apply_select_menus').last()
submitButton.simulate('click')
strictEqual(props.saveAssignmentOrder.callCount, 0)
})
@ -239,7 +242,7 @@ QUnit.module('SelectMenuGroup', suiteHooks => {
QUnit.module('when the student has changed', contextHooks => {
contextHooks.beforeEach(() => {
wrapper = mountComponent()
submitButton = wrapper.find('#apply_select_menus').last()
submitButton = wrapper.find('button#apply_select_menus').last()
wrapper
.find('#student_select_menu')
.last()
@ -261,7 +264,7 @@ QUnit.module('SelectMenuGroup', suiteHooks => {
contextHooks.beforeEach(() => {
props.selectedCourseID = '2'
wrapper = mountComponent()
submitButton = wrapper.find('#apply_select_menus').last()
submitButton = wrapper.find('button#apply_select_menus').last()
wrapper
.find('#course_select_menu')
.last()
@ -282,7 +285,7 @@ QUnit.module('SelectMenuGroup', suiteHooks => {
contextHooks.beforeEach(() => {
props.selectedCourseID = '2'
wrapper = mountComponent()
submitButton = wrapper.find('#apply_select_menus').last()
submitButton = wrapper.find('button#apply_select_menus').last()
wrapper
.find('#course_select_menu')
.last()
@ -305,7 +308,7 @@ QUnit.module('SelectMenuGroup', suiteHooks => {
contextHooks.beforeEach(() => {
props.selectedCourseID = '21'
wrapper = mountComponent()
submitButton = wrapper.find('#apply_select_menus').last()
submitButton = wrapper.find('button#apply_select_menus').last()
wrapper
.find('#course_select_menu')
.last()
@ -326,7 +329,7 @@ QUnit.module('SelectMenuGroup', suiteHooks => {
contextHooks.beforeEach(() => {
props.selectedCourseID = '21'
wrapper = mountComponent()
submitButton = wrapper.find('#apply_select_menus').last()
submitButton = wrapper.find('button#apply_select_menus').last()
wrapper
.find('#course_select_menu')
.last()
@ -347,7 +350,7 @@ QUnit.module('SelectMenuGroup', suiteHooks => {
contextHooks.beforeEach(() => {
props.selectedCourseID = '21'
wrapper = mountComponent()
submitButton = wrapper.find('#apply_select_menus').last()
submitButton = wrapper.find('button#apply_select_menus').last()
wrapper
.find('#course_select_menu')
.last()