Add the choose quiz engine modal

closes QUIZ-7285

Test Plan
---------
- Test with canvas provisioned with quiz_lti/quiz_api
- With the new quizzes on quiz page FF enabled:
-- Navigate to the quizzes page and click the |+ Quiz| button
-- Ensure a modal is shown and is a11y compliant
-- Ensure canceling/submitting with either option works as expected
-- Ensure new quizzes created are shown on the quizzes page

- With the new quizzes on quiz page FF disabled:
-- Navigate to the quizzes page and click the |+ Quiz| button
-- Ensure you are taken to the quizzes old creation flow
-- Ensure new quizzes are not shown on the quizzes page
-- Navigate to the Assignments page and click the |+ Quiz/Test| button
-- Ensure you can create new quizzes and they are displayed only on the
assignments page

Change-Id: I022a0f857d74cf4d315a7c69e467c450889e4aad
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/223118
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Jared Crystal <jcrystal@instructure.com>
QA-Review: Jared Crystal <jcrystal@instructure.com>
Product-Review: Kevin Dougherty <jdougherty@instructure.com>
This commit is contained in:
Tyler Burraston 2020-01-13 11:56:46 -07:00
parent 01165c1db3
commit 3eb57d0489
6 changed files with 255 additions and 31 deletions

View File

@ -24,6 +24,7 @@ import '../../jquery.rails_flash_notifications'
import React from 'react'
import ReactDOM from 'react-dom'
import ContentTypeExternalToolTray from 'jsx/shared/ContentTypeExternalToolTray'
import QuizEngineModal from 'jsx/quizzes/QuizEngineModal'
import {ltiState} from '../../../../public/javascripts/lti/post_message/handleLtiPostMessage'
export default class IndexView extends Backbone.View {
@ -40,7 +41,8 @@ export default class IndexView extends Backbone.View {
this.prototype.events = {
'keyup #searchTerm': 'keyUpSearch',
'mouseup #searchTerm': 'keyUpSearch',
'click .header-bar-right .menu_tool_link': 'openExternalTool'
'click .header-bar-right .menu_tool_link': 'openExternalTool',
'click .choose-quiz-engine': 'chooseQuizEngine'
}
this.prototype.keyUpSearch = _.debounce(function() {
@ -104,6 +106,22 @@ export default class IndexView extends Backbone.View {
return json
}
chooseQuizEngine() {
this.renderQuizEngineModal(true, $('.choose-quiz-engine'))
}
renderQuizEngineModal(setOpen, returnFocusTo) {
const handleDismiss = () => {
this.renderQuizEngineModal(false)
returnFocusTo && returnFocusTo.focus()
}
ReactDOM.render(
<QuizEngineModal onDismiss={handleDismiss} setOpen={setOpen} />,
$('#quiz-modal-mount-point')[0]
)
}
openExternalTool(ev) {
if (ev != null) {
ev.preventDefault()

View File

@ -0,0 +1,128 @@
/*
* Copyright (C) 2019 - 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, {useState} from 'react'
import I18n from 'i18n!quiz_engine_modal'
import CanvasModal from 'jsx/shared/components/CanvasModal'
import {Link} from '@instructure/ui-link'
import {RadioInputGroup, RadioInput} from '@instructure/ui-radio-input'
import {Text} from '@instructure/ui-text'
import {Button} from '@instructure/ui-buttons'
import getCookie from 'jsx/shared/helpers/getCookie'
const CLASSIC = 'classic'
const NEW = 'new'
function QuizEngineModal({setOpen, onDismiss}) {
const [option, setOption] = useState()
const authenticity_token = () => getCookie('_csrf_token')
const link = (
<Link href="https://community.canvaslms.com/docs/DOC-12115-quizzes-lti-feature-comparison">
{I18n.t('Learn more about the differences.')}
</Link>
)
const newQuizLabel = <Text weight="bold">{I18n.t('New Quizzes')}</Text>
const classicLabel = <Text weight="bold">{I18n.t('Classic Quizzes')}</Text>
const newDesc = (
<div style={{paddingLeft: '1.75rem', maxWidth: '23.5rem'}}>
<Text weight="light">
{I18n.t(`This has more question types like hotspot,
categorization, matching, and ordering. It also has
more moderation and accommodation features.`)}
</Text>
</div>
)
const classicDesc = (
<div style={{paddingLeft: '1.75rem', maxWidth: '23.5rem'}}>
<Text weight="light">
{I18n.t(`For the time being, if you need security from
3rd-party tools, Speedgrader, or CSVs for student
response analysis, this is the better choice.`)}
</Text>
</div>
)
const footer = (
<div>
<Button onClick={onDismiss} margin="0 x-small 0 0" variant="light">
{I18n.t('Cancel')}
</Button>
<Button type="submit" onClick={handleSubmit} variant="primary" disabled={!option}>
{I18n.t('Submit')}
</Button>
</div>
)
const description = (
<div style={{paddingBottom: '1.5rem', maxWidth: '25rem'}}>
<Text>
{I18n.t(`Canvas now has two quiz engines. Please choose which
you'd like to use.`)}
&nbsp;{link}
</Text>
</div>
)
function post(path, params, method = 'post') {
const form = document.createElement('form')
form.method = method
form.action = path
for (const key in params) {
if (params.hasOwnProperty(key)) {
const hiddenField = document.createElement('input')
hiddenField.type = 'hidden'
hiddenField.name = key
hiddenField.value = params[key]
form.appendChild(hiddenField)
}
}
document.body.appendChild(form)
form.submit()
}
function handleSubmit() {
if (option === CLASSIC) {
post(ENV.URLS.new_quiz_url, {authenticity_token: authenticity_token()})
} else if (option === NEW) {
window.location.href = `${ENV.URLS.new_assignment_url}?quiz_lti`
}
}
function handleChange(e, value) {
setOption(value)
}
return (
<CanvasModal
open={setOpen}
onDismiss={onDismiss}
padding="medium"
label={I18n.t('Choose a Quiz Engine')}
footer={footer}
>
{description}
<RadioInputGroup name="quizEngine" onChange={handleChange} defaultValue={option}>
<RadioInput key={CLASSIC} value={CLASSIC} label={classicLabel} size="large" />
{classicDesc}
<RadioInput key={NEW} value={NEW} label={newQuizLabel} size="large" />
{newDesc}
</RadioInputGroup>
</CanvasModal>
)
}
export default QuizEngineModal

View File

@ -0,0 +1,88 @@
/*
* Copyright (C) 2019 - 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, fireEvent} from '@testing-library/react'
import QuizEngineModal from '../QuizEngineModal'
describe('QuizEngineModal', () => {
beforeAll(() => {
ENV = Object.assign(ENV, {
URLS: {
new_assignment_url: 'http://localhost/assignments',
new_quiz_url: 'http://localhost/quizzes'
}
})
})
it('renders a header, close button, and children', () => {
const handleDismiss = jest.fn()
const {getByText} = render(<QuizEngineModal setOpen onDismiss={handleDismiss} />)
expect(getByText('Choose a Quiz Engine').tagName).toBe('H2')
expect(getByText('Submit')).toBeInTheDocument()
expect(getByText('Cancel')).toBeInTheDocument()
const closeButton = getByText('Close').closest('button')
expect(closeButton).toBeInTheDocument()
fireEvent.click(closeButton)
expect(handleDismiss).toHaveBeenCalled()
})
it('submit is disabled without a selected choice', () => {
const handleDismiss = jest.fn()
const {getByText} = render(<QuizEngineModal setOpen onDismiss={handleDismiss} />)
expect(
getByText('Submit')
.closest('button')
.getAttribute('disabled')
).toBeDefined()
})
it('submit is enabled with a selected choice', () => {
const handleDismiss = jest.fn()
const {getByText} = render(<QuizEngineModal setOpen onDismiss={handleDismiss} />)
fireEvent.click(getByText('Classic Quizzes'))
expect(
getByText('Submit')
.closest('button')
.getAttribute('disabled')
).toBeNull()
})
it('submits to new quizzes', () => {
const handleDismiss = jest.fn()
const windLoc = global.window.location
delete global.window.location
global.window.location = {href: 'http://localhost'}
const {getByText} = render(<QuizEngineModal setOpen onDismiss={handleDismiss} />)
fireEvent.click(getByText('New Quizzes'))
fireEvent.click(getByText('Submit').closest('button'))
expect(window.location.href).toBe('http://localhost/assignments?quiz_lti')
global.window.location = windLoc
})
it('submits to classic quizzes', () => {
const handleDismiss = jest.fn()
window.HTMLFormElement.prototype.submit = jest.fn()
const {getByText} = render(<QuizEngineModal setOpen onDismiss={handleDismiss} />)
fireEvent.click(getByText('Classic Quizzes'))
fireEvent.click(getByText('Submit').closest('button'))
expect(window.HTMLFormElement.prototype.submit).toHaveBeenCalled()
const form = document.querySelector(`[method="post"][action="${ENV.URLS.new_quiz_url}"]`)
expect(form).toBeTruthy()
})
})

View File

@ -156,13 +156,6 @@ li.quiz {
overflow: visible;
}
.new_quiz_lti_wrapper {
display: inline-block;
margin-#{direction(right)}: 6px;
padding-#{direction(right)}: 10px;
border-#{direction(right)}: 1px solid $ic-color-medium-lighter;
}
.quiz-header {
border-bottom: 1px solid #a4a4a4;
overflow: hidden;

View File

@ -8,24 +8,11 @@
{{#ifAny permissions.create permissions.manage}}
<div class="header-bar-right">
{{#if flags.quiz_lti_enabled}}
<span class="new_quiz_lti_wrapper">
<a
href="{{ENV.URLS.new_assignment_url}}?quiz_lti"
class="new_quiz_lti btn icon-plus"
role="button"
title='{{#t}}Add Quiz{{/t}}'
aria-label='{{#t}}Add Quiz{{/t}}'
>{{#t}}Quiz{{/t}}</a>
</span>
<form class="new-quiz-form" action="{{urls.new_quiz_url}}" method="post">
<input type="hidden" name="authenticity_token" />
<button class="btn btn-primary new-quiz-link"
title='{{#t}}Add Old Quiz{{/t}}'
aria-label='{{#t}}Add Old Quiz{{/t}}'>
<i class="icon-plus" />&nbsp;{{#t}}Old Quiz{{/t}}
</button>
</form>
<button class="btn btn-primary choose-quiz-engine"
title='{{#t}}Add Quiz{{/t}}'
aria-label='{{#t}}Add Quiz{{/t}}'>
<i class="icon-plus" />&nbsp;{{#t}}Quiz{{/t}}
</button>
{{else}}
<form class="new-quiz-form" action="{{urls.new_quiz_url}}" method="post">
<input type="hidden" name="authenticity_token" />
@ -79,6 +66,7 @@
<div id="direct-share-mount-point" />
<div id="external-tool-mount-point" />
<div id="quiz-modal-mount-point" />
<div class="item-group-container">
{{#if hasNoQuizzes}}

View File

@ -23,6 +23,7 @@ import NoQuizzesView from 'compiled/views/quizzes/NoQuizzesView'
import $ from 'jquery'
import fakeENV from 'helpers/fakeENV'
import 'helpers/jquery.simulate'
import ReactDOM from 'react-dom'
let fixtures = null
const indexView = function(assignments, open, surveys) {
@ -136,17 +137,25 @@ test('#hasSurveys if has surveys', () => {
const view = indexView(null, null, surveys)
ok(view.options.hasSurveys)
})
test("shows '+ Quiz' button if quiz lti enabled", () => {
test("shows modified '+ Quiz' button if quiz lti enabled", () => {
ENV.flags.quiz_lti_enabled = true
const view = indexView(null, null, null)
const $button = view.$('.new_quiz_lti')
const $button = view.$('.choose-quiz-engine')
equal($button.length, 1)
ok(/\?quiz_lti$/.test($button.attr('href')))
})
test("does not show '+ Quiz' button when quiz lti disabled", () => {
test("does not show modified '+ Quiz' button when quiz lti disabled", () => {
ENV.flags.quiz_lti_enabled = false
const view = indexView(null, null, null)
equal(view.$('.new_quiz_lti').length, 0)
equal(view.$('.choose-quiz-engine').length, 0)
})
test('renders choose quiz engine modal', () => {
ENV.flags.quiz_lti_enabled = true
sinon.stub(ReactDOM, 'render')
const view = indexView(null, null, null)
view.$('.choose-quiz-engine').simulate('click')
const args = ReactDOM.render.firstCall.args
equal(args[0].props.setOpen, true)
ReactDOM.render.restore()
})
test('should render the view', () => {
const assignments = new QuizCollection([