diff --git a/spec/coffeescripts/jsx/grading/GradingStandardSpec.js b/spec/coffeescripts/jsx/grading/GradingStandardSpec.js deleted file mode 100644 index edce27295d3..00000000000 --- a/spec/coffeescripts/jsx/grading/GradingStandardSpec.js +++ /dev/null @@ -1,555 +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 . - */ - -import React from 'react' -import ReactDOM from 'react-dom' -import {Simulate} from 'react-addons-test-utils' -import $ from 'jquery' -import GradingStandard from 'jsx/grading/gradingStandard' - -QUnit.module('GradingStandard not being edited', { - setup() { - this.props = { - key: 1, - standard: { - id: 1, - title: 'Test Grading Standard', - data: [['A', 0.92], ['A-', 0.9], ['F', 0]] - }, - permissions: {manage: true}, - editing: false, - justAdded: false, - othersEditing: false, - round(number) { - return Math.round(number * 100) / 100 - }, - onSetEditingStatus() {}, - onDeleteGradingStandard() {}, - onSaveGradingStandard() {} - } - this.mountPoint = $('
').appendTo('#fixtures')[0] - const GradingStandardElement = - this.gradingStandard = ReactDOM.render(GradingStandardElement, this.mountPoint) - }, - teardown() { - ReactDOM.unmountComponentAtNode(this.mountPoint) - $('#fixtures').empty() - } -}) - -test('returns false for assessedAssignment', function() { - deepEqual(this.gradingStandard.assessedAssignment(), false) -}) - -test('renders the correct title', function() { - // 'Grading standard title' is wrapped in a screenreader-only span that this - // test suite does not ignore. 'Grading standadr title' is not actually displayed - deepEqual( - this.gradingStandard.refs.title.textContent, - 'Grading standard titleTest Grading Standard' - ) -}) - -test('renders the correct id name', function() { - deepEqual(this.gradingStandard.renderIdNames(), 'grading_standard_1') -}) - -test('renders the edit button', function() { - ok(this.gradingStandard.refs.editButton) -}) - -test('calls onSetEditingStatus when edit button is clicked', function() { - const setEditingStatus = this.spy(this.props, 'onSetEditingStatus') - const GradingStandardElement = - this.gradingStandard = ReactDOM.render(GradingStandardElement, this.mountPoint) - Simulate.click(this.gradingStandard.refs.editButton) - ok(setEditingStatus.calledOnce) - return setEditingStatus.restore() -}) - -test('renders the delete button', function() { - ok(this.gradingStandard.refs.deleteButton) -}) - -test('calls onDeleteGradingStandard when delete button is clicked', function() { - const deleteGradingStandard = this.spy(this.props, 'onDeleteGradingStandard') - const GradingStandardElement = - this.gradingStandard = ReactDOM.render(GradingStandardElement, this.mountPoint) - Simulate.click(this.gradingStandard.refs.deleteButton) - ok(deleteGradingStandard.calledOnce) - return deleteGradingStandard.restore() -}) - -test('does not show a message about not being able to manage', function() { - deepEqual(this.gradingStandard.refs.cannotManageMessage, undefined) -}) - -test('does not show the save button', function() { - deepEqual(this.gradingStandard.refs.saveButton, undefined) -}) - -test('does not show the cancel button', function() { - deepEqual(this.gradingStandard.refs.cancelButton, undefined) -}) - -QUnit.module("GradingStandard without 'manage' permissions", { - setup() { - const props = { - key: 1, - standard: { - id: 1, - title: 'Test Grading Standard', - data: [['A', 0.92], ['A-', 0.9], ['F', 0]], - context_type: 'Account' - }, - permissions: {manage: false}, - editing: false, - justAdded: false, - othersEditing: false, - round(number) { - return Math.round(number * 100) / 100 - }, - onSetEditingStatus() {}, - onDeleteGradingStandard() {}, - onSaveGradingStandard() {} - } - const GradingStandardElement = - this.gradingStandard = ReactDOM.render( - GradingStandardElement, - $('
').appendTo('#fixtures')[0] - ) - }, - teardown() { - ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this.gradingStandard).parentNode) - $('#fixtures').empty() - } -}) - -test('displays a cannot manage message', function() { - ok(this.gradingStandard.refs.cannotManageMessage) -}) - -test('disables edit and delete buttons', function() { - ok(this.gradingStandard.refs.disabledButtons) -}) - -QUnit.module('GradingStandard being edited', { - setup() { - this.props = { - key: 1, - standard: { - id: 1, - title: 'Test Grading Standard', - data: [['A', 0.92], ['A-', 0.9], ['F', 0]] - }, - permissions: {manage: true}, - editing: true, - justAdded: false, - othersEditing: false, - round(number) { - return Math.round(number * 100) / 100 - }, - onSetEditingStatus() {}, - onDeleteGradingStandard() {}, - onSaveGradingStandard: sinon.spy() - } - this.mountPoint = $('
').appendTo('#fixtures')[0] - const GradingStandardElement = - this.gradingStandard = ReactDOM.render(GradingStandardElement, this.mountPoint) - }, - teardown() { - ReactDOM.unmountComponentAtNode(this.mountPoint) - } -}) - -test('does not render the edit button', function() { - deepEqual(this.gradingStandard.refs.editButton, undefined) -}) - -test('does not render the delete button', function() { - deepEqual(this.gradingStandard.refs.deleteButton, undefined) -}) - -test('renders the save button', function() { - ok(this.gradingStandard.refs.saveButton) -}) - -test('rowNamesAreValid() returns true with non-empty, unique row names', function() { - deepEqual(this.gradingStandard.rowNamesAreValid(), true) -}) - -test('rowDataIsValid() returns true with non-empty, unique, non-overlapping row values', function() { - deepEqual(this.gradingStandard.rowDataIsValid(), true) -}) - -test('calls onSaveGradingStandard save button is clicked', function() { - Simulate.click(this.gradingStandard.refs.saveButton) - ok(this.gradingStandard.props.onSaveGradingStandard.calledOnce) -}) - -test('sets the state to saving when the save button is clicked', function() { - deepEqual(this.gradingStandard.state.saving, false) - Simulate.click(this.gradingStandard.refs.saveButton) - deepEqual(this.gradingStandard.state.saving, true) -}) - -test('shows the cancel button', function() { - ok(this.gradingStandard.refs.cancelButton) -}) - -test('calls onSetEditingStatus when the cancel button is clicked', function() { - const setEditingStatus = this.spy(this.props, 'onSetEditingStatus') - const GradingStandardElement = - this.gradingStandard = ReactDOM.render(GradingStandardElement, this.mountPoint) - Simulate.click(this.gradingStandard.refs.cancelButton) - ok(setEditingStatus.calledOnce) - return setEditingStatus.restore() -}) - -test('deletes the correct row on the editingStandard when deleteDataRow is called', function() { - this.gradingStandard.deleteDataRow(1) - deepEqual(this.gradingStandard.state.editingStandard.data, [['A', 0.92], ['F', 0]]) -}) - -test('does not delete the row if it is the last data row remaining', function() { - this.gradingStandard.deleteDataRow(1) - deepEqual(this.gradingStandard.state.editingStandard.data, [['A', 0.92], ['F', 0]]) - this.gradingStandard.deleteDataRow(0) - deepEqual(this.gradingStandard.state.editingStandard.data, [['F', 0]]) - this.gradingStandard.deleteDataRow(0) - deepEqual(this.gradingStandard.state.editingStandard.data, [['F', 0]]) -}) - -test('inserts the correct row on the editingStandard when insertGradingStandardRow is called', function() { - this.gradingStandard.insertGradingStandardRow(1) - deepEqual(this.gradingStandard.state.editingStandard.data, [ - ['A', 0.92], - ['A-', 0.9], - ['', ''], - ['F', 0] - ]) -}) - -test('changes the title on the editingStandard when title input changes', function() { - Simulate.change(this.gradingStandard.refs.title, {target: {value: 'Brand new title'}}) - deepEqual(this.gradingStandard.state.editingStandard.title, 'Brand new title') -}) - -test('changes the min score on the editingStandard when changeRowMinScore is called', function() { - this.gradingStandard.changeRowMinScore(2, '23') - deepEqual(this.gradingStandard.state.editingStandard.data[2], ['F', '23']) -}) - -test('changes the row name on the editingStandard when changeRowName is called', function() { - this.gradingStandard.changeRowName(2, 'Q') - deepEqual(this.gradingStandard.state.editingStandard.data[2], ['Q', 0]) -}) - -QUnit.module('GradingStandard being edited with blank names', { - setup() { - const props = { - key: 1, - standard: { - id: 1, - title: 'Test Grading Standard', - data: [['A', 0.92], ['', 0.9], ['F', 0]] - }, - permissions: {manage: true}, - editing: true, - justAdded: false, - othersEditing: false, - round(number) { - return Math.round(number * 100) / 100 - }, - onSetEditingStatus() {}, - onDeleteGradingStandard() {}, - onSaveGradingStandard() {} - } - const GradingStandardElement = - this.gradingStandard = ReactDOM.render( - GradingStandardElement, - $('
').appendTo('#fixtures')[0] - ) - }, - teardown() { - ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this.gradingStandard).parentNode) - $('#fixtures').empty() - } -}) - -test('rowNamesAreValid() returns false with empty row names', function() { - deepEqual(this.gradingStandard.rowNamesAreValid(), false) -}) - -test('alerts the user that the input is invalid when they try to save', function() { - Simulate.click(this.gradingStandard.refs.saveButton) - ok(this.gradingStandard.refs.invalidStandardAlert) -}) - -test('shows a messsage describing why the input is invalid', function() { - Simulate.click(this.gradingStandard.refs.saveButton) - deepEqual( - this.gradingStandard.refs.invalidStandardAlert.textContent, - "Cannot have duplicate or empty row names. Fix the names and try clicking 'Save' again." - ) -}) - -QUnit.module('GradingStandard being edited with duplicate names', { - setup() { - const props = { - key: 1, - standard: { - id: 1, - title: 'Test Grading Standard', - data: [['A', 0.92], ['A', 0.9], ['F', 0]] - }, - permissions: {manage: true}, - editing: true, - justAdded: false, - othersEditing: false, - round(number) { - return Math.round(number * 100) / 100 - }, - onSetEditingStatus() {}, - onDeleteGradingStandard() {}, - onSaveGradingStandard() {} - } - const GradingStandardElement = - this.gradingStandard = ReactDOM.render( - GradingStandardElement, - $('
').appendTo('#fixtures')[0] - ) - }, - teardown() { - ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this.gradingStandard).parentNode) - $('#fixtures').empty() - } -}) - -test('rowNamesAreValid() returns false with duplicate row names', function() { - deepEqual(this.gradingStandard.rowNamesAreValid(), false) -}) - -test('alerts the user that the input is invalid when they try to save', function() { - Simulate.click(this.gradingStandard.refs.saveButton) - ok(this.gradingStandard.refs.invalidStandardAlert) -}) - -test('shows a messsage describing why the input is invalid', function() { - Simulate.click(this.gradingStandard.refs.saveButton) - deepEqual( - this.gradingStandard.refs.invalidStandardAlert.textContent, - "Cannot have duplicate or empty row names. Fix the names and try clicking 'Save' again." - ) -}) - -QUnit.module('GradingStandard being edited with empty values', { - setup() { - const props = { - key: 1, - standard: { - id: 1, - title: 'Test Grading Standard', - data: [['A', 0.92], ['B', 0.9], ['F', '']] - }, - permissions: {manage: true}, - editing: true, - justAdded: false, - othersEditing: false, - round(number) { - return Math.round(number * 100) / 100 - }, - onSetEditingStatus() {}, - onDeleteGradingStandard() {}, - onSaveGradingStandard() {} - } - const GradingStandardElement = - this.gradingStandard = ReactDOM.render( - GradingStandardElement, - $('
').appendTo('#fixtures')[0] - ) - }, - teardown() { - ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this.gradingStandard).parentNode) - $('#fixtures').empty() - } -}) - -test('rowDataIsValid() returns false with empty values', function() { - deepEqual(this.gradingStandard.rowDataIsValid(), false) -}) - -test('alerts the user that the input is invalid when they try to save', function() { - Simulate.click(this.gradingStandard.refs.saveButton) - ok(this.gradingStandard.refs.invalidStandardAlert) -}) - -test('shows a messsage describing why the input is invalid', function() { - Simulate.click(this.gradingStandard.refs.saveButton) - deepEqual( - this.gradingStandard.refs.invalidStandardAlert.textContent, - "Cannot have overlapping or empty ranges. Fix the ranges and try clicking 'Save' again." - ) -}) - -QUnit.module('GradingStandard being edited with duplicate values', { - setup() { - const props = { - key: 1, - standard: { - id: 1, - title: 'Test Grading Standard', - data: [['A', 0.92], ['B', 0.92], ['F', 0]] - }, - permissions: {manage: true}, - editing: true, - justAdded: false, - othersEditing: false, - round(number) { - return Math.round(number * 100) / 100 - }, - onSetEditingStatus() {}, - onDeleteGradingStandard() {}, - onSaveGradingStandard() {} - } - const GradingStandardElement = - this.gradingStandard = ReactDOM.render( - GradingStandardElement, - $('
').appendTo('#fixtures')[0] - ) - }, - teardown() { - ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this.gradingStandard).parentNode) - $('#fixtures').empty() - } -}) - -test('rowDataIsValid() returns false with duplicate values', function() { - deepEqual(this.gradingStandard.rowDataIsValid(), false) -}) - -test('alerts the user that the input is invalid when they try to save', function() { - Simulate.click(this.gradingStandard.refs.saveButton) - ok(this.gradingStandard.refs.invalidStandardAlert) -}) - -test('shows a messsage describing why the input is invalid', function() { - Simulate.click(this.gradingStandard.refs.saveButton) - deepEqual( - this.gradingStandard.refs.invalidStandardAlert.textContent, - "Cannot have overlapping or empty ranges. Fix the ranges and try clicking 'Save' again." - ) -}) - -QUnit.module('GradingStandard being edited with values that round to the same number', { - setup() { - const props = { - key: 1, - standard: { - id: 1, - title: 'Test Grading Standard', - data: [['A', 0.92], ['B', 0.91996], ['F', 0]] - }, - permissions: {manage: true}, - editing: true, - justAdded: false, - othersEditing: false, - round(number) { - return Math.round(number * 100) / 100 - }, - onSetEditingStatus() {}, - onDeleteGradingStandard() {}, - onSaveGradingStandard() {} - } - const GradingStandardElement = - this.gradingStandard = ReactDOM.render( - GradingStandardElement, - $('
').appendTo('#fixtures')[0] - ) - }, - teardown() { - ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this.gradingStandard).parentNode) - $('#fixtures').empty() - } -}) - -test('rowDataIsValid() returns false with if values round to the same number (91.996 rounds to 92)', function() { - deepEqual(this.gradingStandard.rowDataIsValid(), false) -}) - -test('alerts the user that the input is invalid when they try to save', function() { - Simulate.click(this.gradingStandard.refs.saveButton) - ok(this.gradingStandard.refs.invalidStandardAlert) -}) - -test('shows a messsage describing why the input is invalid', function() { - Simulate.click(this.gradingStandard.refs.saveButton) - deepEqual( - this.gradingStandard.refs.invalidStandardAlert.textContent, - "Cannot have overlapping or empty ranges. Fix the ranges and try clicking 'Save' again." - ) -}) - -QUnit.module('GradingStandard being edited with overlapping values', { - setup() { - const props = { - key: 1, - standard: { - id: 1, - title: 'Test Grading Standard', - data: [['A', 0.92], ['B', 0.93], ['F', 0]] - }, - permissions: {manage: true}, - editing: true, - justAdded: false, - othersEditing: false, - round(number) { - return Math.round(number * 100) / 100 - }, - onSetEditingStatus() {}, - onDeleteGradingStandard() {}, - onSaveGradingStandard() {} - } - const GradingStandardElement = - this.gradingStandard = ReactDOM.render( - GradingStandardElement, - $('
').appendTo('#fixtures')[0] - ) - }, - teardown() { - ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this.gradingStandard).parentNode) - $('#fixtures').empty() - } -}) - -test('rowDataIsValid() returns false if scheme values overlap', function() { - deepEqual(this.gradingStandard.rowDataIsValid(), false) -}) - -test('alerts the user that the input is invalid when they try to save', function() { - Simulate.click(this.gradingStandard.refs.saveButton) - ok(this.gradingStandard.refs.invalidStandardAlert) -}) - -test('shows a messsage describing why the input is invalid', function() { - Simulate.click(this.gradingStandard.refs.saveButton) - deepEqual( - this.gradingStandard.refs.invalidStandardAlert.textContent, - "Cannot have overlapping or empty ranges. Fix the ranges and try clicking 'Save' again." - ) -}) diff --git a/spec/javascripts/jsx/grading/GradingStandardSpec.js b/spec/javascripts/jsx/grading/GradingStandardSpec.js new file mode 100644 index 00000000000..ca14354501f --- /dev/null +++ b/spec/javascripts/jsx/grading/GradingStandardSpec.js @@ -0,0 +1,582 @@ +/* + * 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 . + */ + +import React from 'react' +import {mount} from 'enzyme' + +import fakeENV from 'helpers/fakeENV' +import GradingStandard from 'jsx/grading/gradingStandard' + +QUnit.module('GradingStandard', suiteHooks => { + let props + let wrapper + + suiteHooks.beforeEach(() => { + props = { + editing: false, + justAdded: false, + othersEditing: false, + permissions: {manage: true}, + round(number) { + return Math.round(number * 100) / 100 + }, + onDeleteGradingStandard: sinon.spy(), + onSaveGradingStandard: sinon.spy(), + onSetEditingStatus: sinon.spy(), + standard: { + context_code: 'course_1201', + context_id: '1201', + context_name: 'Calculus 101', + context_type: 'Course', + data: [['A', 0.9], ['B', 0.8], ['C', 0.7], ['D', 0.6], ['F', 0]], + id: '5001', + title: 'Example Grading Scheme' + }, + uniqueId: '5001' + } + + fakeENV.setup({context_asset_string: 'course_1201'}) + }) + + suiteHooks.afterEach(() => { + wrapper.unmount() + fakeENV.teardown() + }) + + function mountComponent() { + wrapper = mount() + } + + function addRowAfter(rowIndex) { + const dataRow = wrapper.find('.grading_standard_row').at(rowIndex) + dataRow.find('.insert_row_button').simulate('click') + } + + function removeRow(rowIndex) { + const dataRow = wrapper.find('.grading_standard_row').at(rowIndex) + dataRow.find('.delete_row_button').simulate('click') + } + + function setRowName(rowIndex, name) { + const dataRow = wrapper.find('.grading_standard_row').at(rowIndex) + const input = dataRow.find('input.standard_name') + input.simulate('change', {target: {value: name}}) + } + + function setRowMinScore(rowIndex, score) { + const dataRow = wrapper.find('.grading_standard_row').at(rowIndex) + const input = dataRow.find('input.standard_value') + input.simulate('change', {target: {value: score}}) + input.simulate('blur') + } + + QUnit.module('when not being edited', () => { + test('includes the grading scheme id the id of the container', () => { + mountComponent() + strictEqual(wrapper.find('#grading_standard_5001').length, 1) + }) + + test('displays the grading scheme title', () => { + mountComponent() + const title = wrapper.find('.title').text() + ok(title.includes('Example Grading Scheme')) + }) + + test('displays an edit button', () => { + mountComponent() + const button = wrapper.find('.edit_grading_standard_button') + strictEqual(button.length, 1) + }) + + test('does not display the button as read-only', () => { + mountComponent() + const button = wrapper.find('.edit_grading_standard_button') + strictEqual(button.hasClass('read_only'), false) + }) + + test('displays a delete button', () => { + mountComponent() + const button = wrapper.find('.delete_grading_standard_button') + strictEqual(button.length, 1) + }) + + test('does not display a save button', () => { + mountComponent() + const button = wrapper.find('.save_button') + strictEqual(button.length, 0) + }) + + test('does not display a cancel button', () => { + mountComponent() + const button = wrapper.find('.cancel_button') + strictEqual(button.length, 0) + }) + }) + + QUnit.module('when clicking the "Edit" button', hooks => { + hooks.beforeEach(() => { + props.onSetEditingStatus = sinon.spy() + mountComponent() + wrapper.find('.edit_grading_standard_button').simulate('click') + }) + + test('calls the onSetEditingStatus prop', () => { + strictEqual(props.onSetEditingStatus.callCount, 1) + }) + + test('includes the unique id of the grading scheme when calling the onSetEditingStatus prop', () => { + const [uniqueId] = props.onSetEditingStatus.lastCall.args + strictEqual(uniqueId, '5001') + }) + + test('sets the editing status to true when calling the onSetEditingStatus prop', () => { + const [, status] = props.onSetEditingStatus.lastCall.args + strictEqual(status, true) + }) + }) + + QUnit.module('when clicking the "Delete" button', hooks => { + hooks.beforeEach(() => { + props.onDeleteGradingStandard = sinon.spy() + mountComponent() + wrapper.find('.delete_grading_standard_button').simulate('click') + }) + + test('calls the onDeleteGradingStandard prop', () => { + strictEqual(props.onDeleteGradingStandard.callCount, 1) + }) + + test('includes the unique id of the grading scheme when calling the onDeleteGradingStandard prop', () => { + const [, uniqueId] = props.onDeleteGradingStandard.lastCall.args + strictEqual(uniqueId, '5001') + }) + }) + + QUnit.module('when editing', contextHooks => { + contextHooks.beforeEach(() => { + props.editing = true + mountComponent() + }) + + test('does not display an edit button', () => { + const button = wrapper.find('.edit_grading_standard_button') + strictEqual(button.length, 0) + }) + + test('does not display a delete button', () => { + const button = wrapper.find('.delete_grading_standard_button') + strictEqual(button.length, 0) + }) + + test('displays a save button', () => { + const button = wrapper.find('.save_button') + strictEqual(button.length, 1) + }) + + test('uses "Save" as the save button text', () => { + const button = wrapper.find('.save_button') + equal(button.text(), 'Save') + }) + + test('displays a cancel button', () => { + const button = wrapper.find('.cancel_button') + strictEqual(button.length, 1) + }) + }) + + QUnit.module('when editing and removing a row', contextHooks => { + contextHooks.beforeEach(() => { + props.editing = true + }) + + test('removes the related row', () => { + mountComponent() + removeRow(1) + const rows = wrapper.find('.grading_standard_row') + const rowNames = rows.map(row => row.find('input.standard_name').prop('value')) + deepEqual(rowNames, ['A', 'C', 'D', 'F']) + }) + + test('does not include a delete button when only one row is present', () => { + props.standard.data.splice(1) + mountComponent() + const buttons = wrapper.find('.delete_row_button') + strictEqual(buttons.length, 0) + }) + }) + + QUnit.module('when editing and adding a row', contextHooks => { + contextHooks.beforeEach(() => { + props.editing = true + }) + + test('inserts an unnamed row after the related row', () => { + mountComponent() + addRowAfter(1) + const rows = wrapper.find('.grading_standard_row') + const rowNames = rows.map(row => row.find('input.standard_name').prop('value')) + deepEqual(rowNames, ['A', 'B', '', 'C', 'D', 'F']) + }) + + test('assigns no minimum score value to the inserted row', () => { + mountComponent() + addRowAfter(1) + const rows = wrapper.find('.grading_standard_row') + const rowScores = rows.map(row => row.find('input.standard_value').prop('value')) + deepEqual(rowScores, ['0.9', '0.8', '0', '0.7', '0.6', '0']) + }) + }) + + QUnit.module('when saving edits', contextHooks => { + contextHooks.beforeEach(() => { + props.editing = true + mountComponent() + }) + + function save() { + wrapper.find('.save_button').simulate('click') + } + + function getSavedGradingScheme() { + return props.onSaveGradingStandard.lastCall.args[0] + } + + QUnit.module('when nothing was changed', () => { + test('calls onSaveGradingStandard', () => { + save() + strictEqual(props.onSaveGradingStandard.callCount, 1) + }) + + test('calls onSaveGradingStandard with the grading scheme', () => { + save() + strictEqual(props.onSaveGradingStandard.callCount, 1) + deepEqual(getSavedGradingScheme(), props.standard) + }) + + test('disables the save button', () => { + save() + const button = wrapper.find('.save_button') + equal(button.prop('disabled'), 'true') + }) + + test('updates the save button to show "Saving..."', () => { + save() + const button = wrapper.find('.save_button') + equal(button.text(), 'Saving...') + }) + }) + + QUnit.module('when the title was changed', hooks => { + hooks.beforeEach(() => { + const input = wrapper.find('input.scheme_name') + input.simulate('change', {target: {value: 'Emoji Grading Scheme'}}) + }) + + test('calls onSaveGradingStandard', () => { + save() + strictEqual(props.onSaveGradingStandard.callCount, 1) + }) + + test('saves with the updated title', () => { + save() + equal(getSavedGradingScheme().title, 'Emoji Grading Scheme') + }) + }) + + QUnit.module('when a row name was changed', () => { + test('calls onSaveGradingStandard when the new name is valid', () => { + setRowName(0, 'A+') + save() + strictEqual(props.onSaveGradingStandard.callCount, 1) + }) + + test('saves with the updated row name', () => { + setRowName(0, 'A+') + save() + const {data} = getSavedGradingScheme() + equal(data[0][0], 'A+') + }) + + test('does not call onSaveGradingStandard when the new name is a duplicate', () => { + setRowName(0, 'B') + save() + strictEqual(props.onSaveGradingStandard.callCount, 0) + }) + + test('displays a validation message about duplicate row names', () => { + setRowName(0, 'B') + save() + const message = wrapper.find('#invalid_standard_message_5001').text() + ok(message.includes('Cannot have duplicate or empty row names.')) + }) + + test('does not call onSaveGradingStandard when the new name is blank', () => { + setRowName(0, '') + save() + strictEqual(props.onSaveGradingStandard.callCount, 0) + }) + + test('displays a validation message about empty row names', () => { + setRowName(0, '') + save() + const message = wrapper.find('#invalid_standard_message_5001').text() + ok(message.includes('Cannot have duplicate or empty row names.')) + }) + }) + + QUnit.module('when a row minimum score was changed', () => { + test('calls onSaveGradingStandard when the new score is valid', () => { + setRowMinScore(0, 0.95) + save() + strictEqual(props.onSaveGradingStandard.callCount, 1) + }) + + test('saves with the updated row score', () => { + setRowMinScore(0, 0.95) + save() + const {data} = getSavedGradingScheme() + strictEqual(data[0][1], '0.95') + }) + + test('does not call onSaveGradingStandard when the row score overlaps', () => { + setRowMinScore(1, 0.91) + save() + strictEqual(props.onSaveGradingStandard.callCount, 0) + }) + + test('displays a validation message about overlapping ranges', () => { + setRowMinScore(1, 0.91) + save() + const message = wrapper.find('#invalid_standard_message_5001').text() + ok(message.includes('Cannot have overlapping or empty ranges.')) + }) + + test('does not accept two scores which overlap after rounding to two decimal places', () => { + setRowMinScore(0, 0.92) + setRowMinScore(1, 0.91996) + save() + strictEqual(props.onSaveGradingStandard.callCount, 0) + }) + }) + + QUnit.module('when a row was added', hooks => { + hooks.beforeEach(() => { + addRowAfter(0) + }) + + test('calls onSaveGradingStandard when the new row is valid', () => { + setRowName(1, 'B+') + setRowMinScore(1, 0.89) + save() + strictEqual(props.onSaveGradingStandard.callCount, 1) + }) + + test('saves with the added row having the given name', () => { + setRowName(1, 'B+') + setRowMinScore(1, 0.89) + save() + const {data} = getSavedGradingScheme() + equal(data[1][0], 'B+') + }) + + test('saves with the added row having the given value', () => { + setRowName(1, 'B+') + setRowMinScore(1, 0.89) + save() + const {data} = getSavedGradingScheme() + equal(data[1][1], 0.89) + }) + + test('does not call onSaveGradingStandard when the new row name is a duplicate', () => { + setRowName(1, 'B') + setRowMinScore(1, 0.89) + save() + strictEqual(props.onSaveGradingStandard.callCount, 0) + }) + + test('displays a validation message about duplicate row names', () => { + setRowName(1, 'B') + setRowMinScore(1, 0.89) + save() + const message = wrapper.find('#invalid_standard_message_5001').text() + ok(message.includes('Cannot have duplicate or empty row names.')) + }) + + test('does not call onSaveGradingStandard when the new row name is blank', () => { + setRowName(1, '') + setRowMinScore(1, 0.89) + save() + strictEqual(props.onSaveGradingStandard.callCount, 0) + }) + + test('displays a validation message about blank row names', () => { + setRowName(1, '') + setRowMinScore(1, 0.89) + save() + const message = wrapper.find('#invalid_standard_message_5001').text() + ok(message.includes('Cannot have duplicate or empty row names.')) + }) + + test('does not call onSaveGradingStandard when the row score overlaps', () => { + setRowName(1, 'B+') + setRowMinScore(1, 0.91) + save() + strictEqual(props.onSaveGradingStandard.callCount, 0) + }) + + test('displays a validation message about overlapping ranges', () => { + setRowName(1, 'B+') + setRowMinScore(1, 0.91) + save() + const message = wrapper.find('#invalid_standard_message_5001').text() + ok(message.includes('Cannot have overlapping or empty ranges.')) + }) + + test('does not call onSaveGradingStandard when the row score is blank', () => { + setRowName(1, 'B+') + setRowMinScore(1, '') + save() + strictEqual(props.onSaveGradingStandard.callCount, 0) + }) + + test('displays a validation message about empty ranges', () => { + setRowName(1, 'B+') + setRowMinScore(1, '') + save() + const message = wrapper.find('#invalid_standard_message_5001').text() + ok(message.includes('Cannot have overlapping or empty ranges.')) + }) + }) + + QUnit.module('when a row was removed', () => { + test('saves without the removed row', () => { + removeRow(1) + save() + const {data} = getSavedGradingScheme() + deepEqual(data.map(datum => datum[0]), ['A', 'C', 'D', 'F']) + }) + }) + }) + + QUnit.module('when canceling edits', contextHooks => { + contextHooks.beforeEach(() => { + props.editing = true + mountComponent() + wrapper.find('.cancel_button').simulate('click') + }) + + test('calls onSetEditingStatus when the cancel button is clicked', () => { + strictEqual(props.onSetEditingStatus.callCount, 1) + }) + + test('includes the unique id of the grading scheme when calling the onSetEditingStatus prop', () => { + const [uniqueId] = props.onSetEditingStatus.lastCall.args + strictEqual(uniqueId, '5001') + }) + + test('sets the editing status to false when calling the onSetEditingStatus prop', () => { + const [, editing] = props.onSetEditingStatus.lastCall.args + strictEqual(editing, false) + }) + }) + + QUnit.module('when an assignment using the grading scheme has been assessed', hooks => { + hooks.beforeEach(() => { + props.standard['assessed_assignment?'] = true + }) + + test('sets the id of the container to "grading_standard_blank"', () => { + mountComponent() + strictEqual(wrapper.find('#grading_standard_blank').length, 1) + }) + + test('displays the edit button as read-only', () => { + mountComponent() + const button = wrapper.find('.edit_grading_standard_button') + strictEqual(button.hasClass('read_only'), true) + }) + }) + + QUnit.module('when another grading scheme is being edited', contextHooks => { + contextHooks.beforeEach(() => { + props.othersEditing = true + mountComponent() + }) + + test('disables the edit button', () => { + const button = wrapper.find('.disabled-buttons .icon-edit') + strictEqual(button.length, 1) + }) + + test('disables the delete button', () => { + const button = wrapper.find('.disabled-buttons .icon-trash') + strictEqual(button.length, 1) + }) + }) + + QUnit.module('when the grading scheme belongs to a different context', contextHooks => { + contextHooks.beforeEach(() => { + ENV.context_asset_string = 'course_1202' + }) + + test('displays the context type and name', () => { + mountComponent() + equal(wrapper.find('div.cannot-manage-notification').text(), '(course: Calculus 101)') + }) + + test('displays only the context type when the grading standard has no context name', () => { + delete props.standard.context_name + mountComponent() + equal(wrapper.find('div.cannot-manage-notification').text(), '(course level)') + }) + + test('disables the options menu when another grading scheme is being edited', () => { + props.othersEditing = true + mountComponent() + strictEqual(wrapper.find('div.cannot-manage-notification').length, 1) + }) + }) + + QUnit.module('when the user cannot manage the grading scheme', contextHooks => { + contextHooks.beforeEach(() => { + props.permissions.manage = false + }) + + test('displays a "cannot manage" message', () => { + mountComponent() + strictEqual(wrapper.find('div.cannot-manage-notification').length, 1) + }) + + test('displays the grading scheme title', () => { + mountComponent() + const title = wrapper.find('.title').text() + ok(title.includes('Example Grading Scheme')) + }) + + test('displays the context type and name', () => { + mountComponent() + equal(wrapper.find('div.cannot-manage-notification').text(), '(course: Calculus 101)') + }) + + test('displays only the context type when the grading standard has no context name', () => { + delete props.standard.context_name + mountComponent() + equal(wrapper.find('div.cannot-manage-notification').text(), '(course level)') + }) + }) +})