spec: Remove enzyme.mount from blueprint_courses components

fixes LF-1430
flag=none

test plan:
- tests pass
- tests are still testing what they were before

Change-Id: I1bec3043950a31b22e38a25085e6baf618c42322
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/345618
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Jacob DeWar <jacob.dewar@instructure.com>
QA-Review: Jacob DeWar <jacob.dewar@instructure.com>
Product-Review: Eric Saupe <eric.saupe@instructure.com>
This commit is contained in:
Eric Saupe 2024-04-18 10:44:30 -07:00
parent 47633af85d
commit 3bc74a4844
18 changed files with 866 additions and 713 deletions

View File

@ -1,223 +0,0 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react'
import $ from 'jquery'
import 'jquery-migrate'
import {Provider} from 'react-redux'
import * as enzyme from 'enzyme'
import moxios from 'moxios'
import {ConnectedCourseSidebar} from 'ui/features/blueprint_course_master/react/components/CourseSidebar'
import MigrationStates from '@canvas/blueprint-courses/react/migrationStates'
import getSampleData from '../getSampleData'
import mockStore from '../mockStore'
let clock
let sidebarContentRef = null
const initialState = {
masterCourse: getSampleData().masterCourse,
existingAssociations: getSampleData().courses,
unsyncedChanges: getSampleData().unsyncedChanges,
migrationStatus: MigrationStates.states.unknown,
canManageCourse: true,
hasLoadedAssociations: true,
hasLoadedUnsyncedChanges: true,
}
const defaultProps = () => ({
contentRef: cr => {
sidebarContentRef = cr
},
routeTo: () => {},
})
function connect(props = defaultProps(), storeState = initialState) {
return (
<Provider store={mockStore(storeState)}>
<ConnectedCourseSidebar {...props} />
</Provider>
)
}
QUnit.module('Course Sidebar component', {
setup() {
clock = sinon.useFakeTimers()
const appElement = document.createElement('div')
appElement.id = 'application'
document.getElementById('fixtures').appendChild(appElement)
sidebarContentRef = null
moxios.install()
moxios.stubRequest('/api/v1/courses/4/blueprint_templates/default/migrations', {
status: 200,
response: [{id: '1'}],
})
},
teardown() {
moxios.uninstall()
document.getElementById('fixtures').innerHTML = ''
clock.restore()
},
})
test('renders the closed CourseSidebar component', () => {
const tree = enzyme.mount(connect())
const node = tree.find('button')
equal(node.text().trim(), 'Open Blueprint Sidebar')
tree.unmount()
})
test('renders the open CourseSidebar component', () => {
const tree = enzyme.mount(connect())
tree.find('button').simulate('click')
clock.tick(500)
ok(sidebarContentRef, 'sidebar contents')
const sidebar = $(sidebarContentRef)
const rows = sidebar.find('.bcs__row')
// associations
ok(rows.eq(0).find('button#mcSidebarAsscBtn').size(), 'Associations button')
equal(
rows.eq(0).text().trim(),
`Associations${initialState.existingAssociations.length}`,
'Associations count'
)
// sync history
ok(rows.eq(1).find('button#mcSyncHistoryBtn').size(), 'sync history button')
// unsynced changes
ok(rows.eq(2).find('button#mcUnsyncedChangesBtn').size(), 'unsynced changes button')
equal(rows.eq(2).find('span').eq(0).text(), 'Unsynced Changes')
const count = rows.eq(2).find('.bcs__row-right-content').text()
equal(count, initialState.unsyncedChanges.length, 'unsynced changes count')
tree.unmount()
})
test('renders no Uncynced Changes link if there are none', () => {
const props = defaultProps()
const state = {...initialState}
state.unsyncedChanges = []
const tree = enzyme.mount(connect(props, state))
tree.find('button').simulate('click')
ok(sidebarContentRef)
const sidebar = $(sidebarContentRef)
// no unsynced changes
notOk(sidebar.find('button#mcUnsyncedChangesBtn').size())
tree.unmount()
})
test('renders no Uncynced Changes link if there are no associations', () => {
const props = defaultProps()
const state = {...initialState}
state.existingAssociations = []
const tree = enzyme.mount(connect(props, state))
tree.find('button').simulate('click')
ok(sidebarContentRef)
const sidebar = $(sidebarContentRef)
// no unsynced changes
notOk(sidebar.find('button#mcUnsyncedChangesBtn').size())
tree.unmount()
})
test('renders no Uncynced Changes link if sync is in progress', () => {
const props = defaultProps()
const state = {...initialState}
state.migrationStatus = MigrationStates.states.imports_queued
const tree = enzyme.mount(connect(props, state))
tree.find('button').simulate('click')
ok(sidebarContentRef)
const sidebar = $(sidebarContentRef)
// no unsynced changes
notOk(sidebar.find('button#mcUnsyncedChangesBtn').size())
tree.unmount()
})
test('renders no Associations link if the user not an admin', () => {
const props = defaultProps()
const state = {...initialState}
state.canManageCourse = false
const tree = enzyme.mount(connect(props, state))
tree.find('button').simulate('click')
ok(sidebarContentRef)
const sidebar = $(sidebarContentRef)
// no unsynced changes
notOk(sidebar.find('button#mcSidebarAsscBtn').size())
tree.unmount()
})
test('renders Sync button if has associations and sync is active and no unsyced changes', () => {
const props = defaultProps()
const state = {...initialState}
state.unsyncedChanges = []
state.migrationStatus = MigrationStates.states.imports_queued
const tree = enzyme.mount(connect(props, state))
tree.find('button').simulate('click')
clock.tick(500)
ok(sidebarContentRef)
const sidebar = $(sidebarContentRef)
ok(sidebar.find('.bcs__migration-sync').size())
tree.unmount()
})
test('renders Sync button if has associations and has unsynced changes', () => {
const props = defaultProps()
const state = {...initialState}
const tree = enzyme.mount(connect(props, state))
tree.find('button').simulate('click')
clock.tick(500)
ok(sidebarContentRef)
const sidebar = $(sidebarContentRef)
ok(sidebar.find('.bcs__migration-sync').size())
tree.unmount()
})
test('renders no Sync button if there are no associations', () => {
const props = defaultProps()
const state = {...initialState}
state.existingAssociations = []
const tree = enzyme.mount(connect(props, state))
tree.find('button').simulate('click')
ok(sidebarContentRef)
const sidebar = $(sidebarContentRef)
notOk(sidebar.find('.bcs__migration-sync').size())
tree.unmount()
})
test('renders no Sync button if there are associations, but no unsynced changes and no sync in progress', () => {
const props = defaultProps()
const state = {...initialState}
state.unsyncedChanges = []
const tree = enzyme.mount(connect(props, state))
tree.find('button').simulate('click')
ok(sidebarContentRef)
const sidebar = $(sidebarContentRef)
notOk(sidebar.find('.bcs__migration-sync').size())
tree.unmount()
})

View File

@ -1,73 +0,0 @@
/*
* Copyright (C) 2017 - 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 * as enzyme from 'enzyme'
import LockBanner from '@canvas/blueprint-courses/react/components/LockManager/LockBanner'
QUnit.module('LockBanner component')
const defaultProps = () => ({
isLocked: true,
itemLocks: {
content: true,
points: false,
due_dates: false,
availability_dates: false,
},
})
test('renders an Alert when LockBanner is locked', () => {
const props = defaultProps()
props.isLocked = true
const tree = enzyme.mount(<LockBanner {...props} />)
const node = tree.find('Alert')
ok(node.exists())
})
test('does not render Alert when LockBanner is locked', () => {
const props = defaultProps()
props.isLocked = false
const tree = enzyme.mount(<LockBanner {...props} />)
const node = tree.find('Alert')
notOk(node.exists())
})
test('displays locked description text appropriately when one attribute is locked', () => {
const props = defaultProps()
const tree = enzyme.mount(<LockBanner {...props} />)
const text = tree.find('Text').at(2).text()
equal(text, 'Content')
})
test('displays locked description text appropriately when two attributes are locked', () => {
const props = defaultProps()
props.itemLocks.points = true
const tree = enzyme.mount(<LockBanner {...props} />)
const text = tree.find('Text').at(2).text()
equal(text, 'Content & Points')
})
test('displays locked description text appropriately when more than two attributes are locked', () => {
const props = defaultProps()
props.itemLocks.points = true
props.itemLocks.due_dates = true
const tree = enzyme.mount(<LockBanner {...props} />)
const text = tree.find('Text').at(2).text()
equal(text, 'Content, Points & Due Dates')
})

View File

@ -1,66 +0,0 @@
/*
* Copyright (C) 2017 - 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 * as enzyme from 'enzyme'
import LockToggle from '@canvas/blueprint-courses/react/components/LockManager/LockToggle'
QUnit.module('LockToggle component')
const defaultProps = () => ({
isLocked: true,
isToggleable: true,
})
test('renders the LockToggle component', () => {
const tree = enzyme.mount(<LockToggle {...defaultProps()} />)
const node = tree.find('.bpc-lock-toggle')
ok(node.exists())
})
test('renders a button when LockToggle is toggleable', () => {
const props = defaultProps()
props.isToggleable = true
const tree = enzyme.mount(<LockToggle {...props} />)
const node = tree.find('Button')
ok(node.exists())
})
test('does not render a button when LockToggle is not toggleable', () => {
const props = defaultProps()
props.isToggleable = false
const tree = enzyme.shallow(<LockToggle {...props} />)
const node = tree.find('Button')
notOk(node.exists())
})
test('renders a locked icon when LockToggle is locked', () => {
const props = defaultProps()
props.isLocked = true
const tree = enzyme.mount(<LockToggle {...props} />)
const node = tree.find('IconBlueprintLockSolid')
ok(node.exists())
})
test('renders an unlocked icon when LockToggle is unlocked', () => {
const props = defaultProps()
props.isLocked = false
const tree = enzyme.mount(<LockToggle {...props} />)
const node = tree.find('IconBlueprintSolid')
ok(node.exists())
})

View File

@ -1,85 +0,0 @@
/*
* Copyright (C) 2017 - 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 * as enzyme from 'enzyme'
import MigrationOptions from 'ui/features/blueprint_course_master/react/components/MigrationOptions'
import MigrationStates from '@canvas/blueprint-courses/react/migrationStates'
const noop = () => {}
QUnit.module('MigrationOptions component')
const defaultProps = {
migrationStatus: MigrationStates.states.unknown,
willSendNotification: false,
willIncludeCustomNotificationMessage: false,
willIncludeCourseSettings: false,
notificationMessage: '',
enableSendNotification: noop,
includeCustomNotificationMessage: noop,
setNotificationMessage: noop,
includeCourseSettings: noop,
}
test('renders the MigrationOptions component', () => {
const tree = enzyme.shallow(<MigrationOptions {...defaultProps} />)
const node = tree.find({as: 'fieldset'})
ok(node.exists())
})
test('renders the course-settings and notification-enable checkboxes', () => {
const tree = enzyme.mount(<MigrationOptions {...defaultProps} />)
const checkboxes = tree.find('input[type="checkbox"]')
equal(checkboxes.length, 2)
equal(checkboxes.at(0).prop('checked'), false)
equal(checkboxes.at(1).prop('checked'), false)
})
test('renders the add a message checkbox', () => {
const props = {...defaultProps}
props.willSendNotification = true
const tree = enzyme.mount(<MigrationOptions {...props} />)
ok(tree.find('Checkbox[label="Include Course Settings"]').first().exists())
equal(tree.find('Checkbox[label="Include Course Settings"]').first().prop('checked'), false)
ok(tree.find('Checkbox[label="Send Notification"]').first().exists())
equal(tree.find('Checkbox[label="Send Notification"]').first().prop('checked'), true)
const checkbox3 = tree
.find('Checkbox')
.filterWhere(n => n.text().includes('0/140'))
.first()
ok(checkbox3.exists())
equal(checkbox3.prop('checked'), false)
const messagebox = tree.find('TextArea')
ok(!messagebox.exists())
})
test('renders the message text area', () => {
const props = {...defaultProps}
props.willSendNotification = true
props.willIncludeCustomNotificationMessage = true
const tree = enzyme.mount(<MigrationOptions {...props} />)
const messagebox = tree.find('TextArea')
ok(messagebox.exists())
})

View File

@ -1,79 +0,0 @@
/*
* Copyright (C) 2017 - 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 * as enzyme from 'enzyme'
import MigrationSync from 'ui/features/blueprint_course_master/react/components/MigrationSync'
QUnit.module('MigrationSync component')
const defaultProps = () => ({
migrationStatus: 'void',
hasCheckedMigration: true,
isLoadingBeginMigration: false,
checkMigration: () => {},
beginMigration: () => {},
stopMigrationStatusPoll: () => {},
})
test('renders the MigrationSync component', () => {
const tree = enzyme.shallow(<MigrationSync {...defaultProps()} />)
const node = tree.find('.bcs__migration-sync')
ok(node.exists())
})
test('renders the progress indicator if in a loading migration state', () => {
const props = defaultProps()
props.migrationStatus = 'queued'
const tree = enzyme.shallow(<MigrationSync {...props} />)
const node = tree.find('.bcs__migration-sync__loading')
ok(node.exists())
})
test('renders the progress indicator if in the process of beginning a migration', () => {
const props = defaultProps()
props.isLoadingBeginMigration = true
const tree = enzyme.shallow(<MigrationSync {...props} />)
const node = tree.find('.bcs__migration-sync__loading')
ok(node.exists())
})
test('calls beginMigration when sync button is clicked', () => {
const props = defaultProps()
props.beginMigration = sinon.spy()
const tree = enzyme.mount(<MigrationSync {...props} />)
const button = tree.find('.bcs__migration-sync button')
button.at(0).simulate('click')
equal(props.beginMigration.callCount, 1)
})
test('calls checkMigration on mount if it has not been checked already', () => {
const props = defaultProps()
props.hasCheckedMigration = false
props.checkMigration = sinon.spy()
const tree = enzyme.shallow(<MigrationSync {...props} />)
equal(props.checkMigration.callCount, 1)
})
test('does not call checkMigration on mount if it has been checked already', () => {
const props = defaultProps()
props.hasCheckedMigration = true
props.checkMigration = sinon.spy()
const tree = enzyme.shallow(<MigrationSync {...props} />)
equal(props.checkMigration.callCount, 0)
})

View File

@ -1,67 +0,0 @@
/*
* Copyright (C) 2017 - 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 * as enzyme from 'enzyme'
import SyncHistoryItem from '@canvas/blueprint-courses/react/components/SyncHistoryItem'
import getSampleData from '../getSampleData'
QUnit.module('SyncHistoryItem component')
const defaultProps = () => ({
heading: null,
migration: getSampleData().history[0],
})
test('renders the SyncHistoryItem component', () => {
const tree = enzyme.shallow(<SyncHistoryItem {...defaultProps()} />)
const node = tree.find('.bcs__history-item')
ok(node.exists())
})
test('renders heading component when migration has changes', () => {
const props = defaultProps()
props.heading = <p className="test-heading">test</p>
const tree = enzyme.shallow(<SyncHistoryItem {...props} />)
const node = tree.find('.bcs__history-item .test-heading')
ok(node.exists())
})
test('does not render the heading component when migration has no changes', () => {
const props = defaultProps()
props.heading = <p className="test-heading">test</p>
props.migration.changes = []
const tree = enzyme.shallow(<SyncHistoryItem {...props} />)
const node = tree.find('.bcs__history-item .test-heading')
notOk(node.exists())
})
test('renders changes using the appropriate prop component', () => {
const props = defaultProps()
props.ChangeComponent = () => <div className="test-change" />
const tree = enzyme.mount(<SyncHistoryItem {...props} />)
const node = tree.find('.bcs__history-item .test-change')
equal(node.length, props.migration.changes.length)
})
test('includes the name of the person who started the sync', () => {
const tree = enzyme.mount(<SyncHistoryItem {...defaultProps()} />)
const node = tree.find('.bcs__history-item__title')
const text = node.text()
notEqual(text.indexOf('changes pushed by Bob Jones'), -1)
})

View File

@ -1,54 +0,0 @@
/*
* Copyright (C) 2017 - 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 * as enzyme from 'enzyme'
import SyncHistory from 'ui/features/blueprint_course_master/react/components/SyncHistory'
import getSampleData from '../getSampleData'
QUnit.module('SyncHistory component')
const defaultProps = () => ({
loadHistory: () => {},
isLoadingHistory: false,
hasLoadedHistory: false,
loadAssociations: () => {},
isLoadingAssociations: false,
hasLoadedAssociations: false,
migrations: getSampleData().history,
})
test('renders the SyncHistory component', () => {
const tree = enzyme.shallow(<SyncHistory {...defaultProps()} />)
const node = tree.find('.bcs__history')
ok(node.exists())
})
test('displays spinner when loading courses', () => {
const props = defaultProps()
props.isLoadingHistory = true
const tree = enzyme.shallow(<SyncHistory {...props} />)
const node = tree.find('.bcs__history Spinner')
ok(node.exists())
})
test('renders SyncHistoryItem components for each migration', () => {
const tree = enzyme.mount(<SyncHistory {...defaultProps()} />)
const node = tree.find('SyncHistoryItem')
equal(node.length, 1)
})

View File

@ -1,25 +0,0 @@
/*
* Copyright (C) 2016 - 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 {createStore, applyMiddleware} from 'redux'
import {thunk} from 'redux-thunk'
import rootReducer from '@canvas/blueprint-courses/react/reducer'
export default function mockStore(initialState) {
return applyMiddleware(thunk)(createStore)(rootReducer, initialState)
}

View File

@ -0,0 +1,252 @@
/*
* Copyright (C) 2024 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react'
import $ from 'jquery'
import 'jquery-migrate'
import {Provider} from 'react-redux'
import {render} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import moxios from 'moxios'
import sinon from 'sinon'
import {ConnectedCourseSidebar} from '../CourseSidebar'
import MigrationStates from '@canvas/blueprint-courses/react/migrationStates'
import getSampleData from './getSampleData'
import {createStore, applyMiddleware} from 'redux'
import {thunk} from 'redux-thunk'
import rootReducer from '@canvas/blueprint-courses/react/reducer'
let clock
let sidebarContentRef = null
const initialState = {
masterCourse: getSampleData().masterCourse,
existingAssociations: getSampleData().courses,
unsyncedChanges: getSampleData().unsyncedChanges,
migrationStatus: MigrationStates.states.unknown,
canManageCourse: true,
hasLoadedAssociations: true,
hasLoadedUnsyncedChanges: true,
canAutoPublishCourses: false,
}
const defaultProps = () => ({
contentRef: cr => {
sidebarContentRef = cr
},
routeTo: () => {}
})
function mockStore(initialState) {
return applyMiddleware(thunk)(createStore)(rootReducer, initialState)
}
function connect(props = defaultProps(), storeState = initialState) {
return (
<Provider store={mockStore(storeState)}>
<ConnectedCourseSidebar {...props} />
</Provider>
)
}
describe('Course Sidebar component', () => {
beforeEach(() => {
clock = sinon.useFakeTimers()
moxios.install()
moxios.stubRequest('/api/v1/courses/4/blueprint_templates/default/migrations', {
status: 200,
response: [{id: '1'}],
})
})
afterEach(() => {
moxios.uninstall()
clock.restore()
})
test('renders the closed CourseSidebar component', () => {
const tree = render(connect())
const node = tree.container.querySelector('button')
expect(node.textContent.trim()).toEqual('Open Blueprint Sidebar')
tree.unmount()
})
test('renders the open CourseSidebar component', async () => {
const tree = render(connect())
const button = tree.container.querySelector('button')
const user = userEvent.setup({delay: null})
await user.click(button)
clock.tick(500)
expect(sidebarContentRef).toBeTruthy()
const sidebar = $(sidebarContentRef)
const rows = sidebar.find('.bcs__row')
// associations
expect(rows.eq(0).find('button#mcSidebarAsscBtn').size()).toBeTruthy()
expect(rows.eq(0).text().trim()).toEqual(`Associations${initialState.existingAssociations.length}`)
// sync history
expect(rows.eq(1).find('button#mcSyncHistoryBtn').size()).toBeTruthy()
// unsynced changes
expect(rows.eq(2).find('button#mcUnsyncedChangesBtn').size()).toBeTruthy()
expect(rows.eq(2).find('span').eq(0).text()).toEqual('Unsynced Changes')
const count = Number(rows.eq(2).find('.bcs__row-right-content').text())
expect(count).toEqual(initialState.unsyncedChanges.length)
tree.unmount()
})
test('renders no Uncynced Changes link if there are none', async () => {
const props = defaultProps()
const state = {...initialState}
state.unsyncedChanges = []
const tree = render(connect(props, state))
const button = tree.container.querySelector('button')
const user = userEvent.setup({delay: null})
await user.click(button)
expect(sidebarContentRef).toBeTruthy()
const sidebar = $(sidebarContentRef)
// no unsynced changes
expect(sidebar.find('button#mcUnsyncedChangesBtn').size()).toBeFalsy()
tree.unmount()
})
test('renders no Uncynced Changes link if there are no associations', async () => {
const props = defaultProps()
const state = {...initialState}
state.existingAssociations = []
const tree = render(connect(props, state))
const button = tree.container.querySelector('button')
const user = userEvent.setup({delay: null})
await user.click(button)
expect(sidebarContentRef).toBeTruthy()
const sidebar = $(sidebarContentRef)
// no unsynced changes
expect(sidebar.find('button#mcUnsyncedChangesBtn').size()).toBeFalsy()
tree.unmount()
})
test('renders no Uncynced Changes link if sync is in progress', async () => {
const props = defaultProps()
const state = {...initialState}
state.migrationStatus = MigrationStates.states.imports_queued
const tree = render(connect(props, state))
const button = tree.container.querySelector('button')
const user = userEvent.setup({delay: null})
await user.click(button)
expect(sidebarContentRef).toBeTruthy()
const sidebar = $(sidebarContentRef)
// no unsynced changes
expect(sidebar.find('button#mcUnsyncedChangesBtn').size()).toBeFalsy()
tree.unmount()
})
test('renders no Associations link if the user not an admin', async () => {
const props = defaultProps()
const state = {...initialState}
state.canManageCourse = false
const tree = render(connect(props, state))
const button = tree.container.querySelector('button')
const user = userEvent.setup({delay: null})
await user.click(button)
expect(sidebarContentRef).toBeTruthy()
const sidebar = $(sidebarContentRef)
// no unsynced changes
expect(sidebar.find('button#mcSidebarAsscBtn').size()).toBeFalsy()
tree.unmount()
})
test('renders Sync button if has associations and sync is active and no unsyced changes', async () => {
const props = defaultProps()
const state = {...initialState}
state.unsyncedChanges = []
state.migrationStatus = MigrationStates.states.imports_queued
const tree = render(connect(props, state))
const button = tree.container.querySelector('button')
const user = userEvent.setup({delay: null})
await user.click(button)
clock.tick(500)
expect(sidebarContentRef).toBeTruthy()
const sidebar = $(sidebarContentRef)
expect(sidebar.find('.bcs__migration-sync').size()).toBeTruthy()
tree.unmount()
})
test('renders Sync button if has associations and has unsynced changes', async() => {
const props = defaultProps()
const state = {...initialState}
const tree = render(connect(props, state))
const button = tree.container.querySelector('button')
const user = userEvent.setup({delay: null})
await user.click(button)
clock.tick(500)
expect(sidebarContentRef).toBeTruthy()
const sidebar = $(sidebarContentRef)
expect(sidebar.find('.bcs__migration-sync').size()).toBeTruthy()
tree.unmount()
})
test('renders no Sync button if there are no associations', async () => {
const props = defaultProps()
const state = {...initialState}
state.existingAssociations = []
const tree = render(connect(props, state))
const button = tree.container.querySelector('button')
const user = userEvent.setup({delay: null})
await user.click(button)
expect(sidebarContentRef).toBeTruthy()
const sidebar = $(sidebarContentRef)
expect(sidebar.find('.bcs__migration-sync').size()).toBeFalsy()
tree.unmount()
})
test('renders no Sync button if there are associations, but no unsynced changes and no sync in progress', async () => {
const props = defaultProps()
const state = {...initialState}
state.unsyncedChanges = []
const tree = render(connect(props, state))
const button = tree.container.querySelector('button')
const user = userEvent.setup({delay: null})
await user.click(button)
expect(sidebarContentRef).toBeTruthy()
const sidebar = $(sidebarContentRef)
expect(sidebar.find('.bcs__migration-sync').size()).toBeFalsy()
tree.unmount()
})
})

View File

@ -0,0 +1,87 @@
/*
* Copyright (C) 2024 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react'
import {shallow} from 'enzyme'
import {render} from '@testing-library/react'
import {getByLabelText} from '@testing-library/dom'
import MigrationOptions from '../MigrationOptions'
import MigrationStates from '@canvas/blueprint-courses/react/migrationStates'
const noop = () => {}
describe('MigrationOptions component', () => {
const defaultProps = {
migrationStatus: MigrationStates.states.unknown,
willSendNotification: false,
willIncludeCustomNotificationMessage: false,
willIncludeCourseSettings: false,
notificationMessage: '',
enableSendNotification: noop,
includeCustomNotificationMessage: noop,
setNotificationMessage: noop,
includeCourseSettings: noop,
}
test('renders the MigrationOptions component', () => {
const tree = shallow(<MigrationOptions {...defaultProps} />)
const node = tree.find({as: 'fieldset'})
expect(node.exists()).toBeTruthy()
})
test('renders the course-settings and notification-enable checkboxes', () => {
const tree = render(<MigrationOptions {...defaultProps} />)
const checkboxes = tree.container.querySelectorAll('input[type="checkbox"]')
expect(checkboxes.length).toEqual(2)
expect(checkboxes[0].checked).toEqual(false)
expect(checkboxes[1].checked).toEqual(false)
})
test('renders the add a message checkbox', () => {
const props = {...defaultProps}
props.willSendNotification = true
const tree = render(<MigrationOptions {...props} />)
const courseSettingsCheckbox = getByLabelText(tree.container, 'Include Course Settings')
expect(courseSettingsCheckbox).toBeTruthy()
expect(courseSettingsCheckbox.checked).toEqual(false)
const notificationCheckbox = getByLabelText(tree.container, 'Send Notification')
expect(notificationCheckbox).toBeTruthy()
expect(notificationCheckbox.checked).toEqual(true)
const checkbox3 = getByLabelText(tree.container, 'Add a Message (0/140)')
expect(checkbox3).toBeTruthy()
expect(checkbox3.checked).toEqual(false)
const messagebox = tree.container.querySelector('textarea')
expect(messagebox).toBeFalsy()
})
test('renders the message text area', () => {
const props = {...defaultProps}
props.willSendNotification = true
props.willIncludeCustomNotificationMessage = true
const tree = render(<MigrationOptions {...props} />)
const messagebox = tree.container.querySelector('textarea')
expect(messagebox).toBeTruthy()
})
})

View File

@ -0,0 +1,84 @@
/*
* Copyright (C) 2024 - 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 {shallow} from 'enzyme'
import sinon from 'sinon'
import userEvent from '@testing-library/user-event'
import MigrationSync from '../MigrationSync'
describe('MigrationSync component', () => {
const defaultProps = () => ({
migrationStatus: 'void',
hasCheckedMigration: true,
isLoadingBeginMigration: false,
checkMigration: () => {},
beginMigration: () => {},
stopMigrationStatusPoll: () => {},
})
test('renders the MigrationSync component', () => {
const tree = shallow(<MigrationSync {...defaultProps()} />)
const node = tree.find('.bcs__migration-sync')
expect(node).toBeTruthy()
})
test('renders the progress indicator if in a loading migration state', () => {
const props = defaultProps()
props.migrationStatus = 'queued'
const tree = shallow(<MigrationSync {...props} />)
const node = tree.find('.bcs__migration-sync__loading')
expect(node).toBeTruthy()
})
test('renders the progress indicator if in the process of beginning a migration', () => {
const props = defaultProps()
props.isLoadingBeginMigration = true
const tree = shallow(<MigrationSync {...props} />)
const node = tree.find('.bcs__migration-sync__loading')
expect(node).toBeTruthy()
})
test('calls beginMigration when sync button is clicked', async () => {
const props = defaultProps()
props.beginMigration = sinon.spy()
const tree = render(<MigrationSync {...props} />)
const button = tree.container.querySelector('.bcs__migration-sync button')
const user = userEvent.setup({delay: null})
await user.click(button)
expect(props.beginMigration.callCount).toEqual(1)
})
test('calls checkMigration on mount if it has not been checked already', () => {
const props = defaultProps()
props.hasCheckedMigration = false
props.checkMigration = sinon.spy()
const tree = shallow(<MigrationSync {...props} />)
expect(props.checkMigration.callCount).toEqual(1)
})
test('does not call checkMigration on mount if it has been checked already', () => {
const props = defaultProps()
props.hasCheckedMigration = true
props.checkMigration = sinon.spy()
const tree = shallow(<MigrationSync {...props} />)
expect(props.checkMigration.callCount).toEqual(0)
})
})

View File

@ -0,0 +1,57 @@
/*
* Copyright (C) 2024 - 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 {shallow} from 'enzyme'
import SyncHistory from '../SyncHistory'
import getSampleData from './getSampleData'
describe('SyncHistory component', () => {
const defaultProps = () => ({
loadHistory: () => {},
isLoadingHistory: false,
hasLoadedHistory: false,
loadAssociations: () => {},
isLoadingAssociations: false,
hasLoadedAssociations: false,
migrations: getSampleData().history,
})
test('renders the SyncHistory component', () => {
const tree = shallow(<SyncHistory {...defaultProps()} />)
const node = tree.find('.bcs__history')
expect(node).toBeTruthy()
})
test('displays spinner when loading courses', () => {
const props = defaultProps()
props.isLoadingHistory = true
const tree = shallow(<SyncHistory {...props} />)
const node = tree.find('.bcs__history Spinner')
expect(node).toBeTruthy()
})
test('renders SyncHistoryItem components for each migration', () => {
const tree = render(<SyncHistory {...defaultProps()} />)
console.log(tree.container.innerHTML)
const node = tree.container.querySelectorAll('.bcs__history-item')
expect(node.length).toEqual(1)
})
})

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
* Copyright (C) 2024 - present Instructure, Inc.
*
* This file is part of Canvas.
*
@ -18,9 +18,10 @@
import React from 'react'
import {Provider} from 'react-redux'
import {mount} from 'enzyme'
import {render} from '@testing-library/react'
import {getByText, getAllByText} from '@testing-library/dom'
import createStore from '@canvas/blueprint-courses/react/store'
import {ConnectedUnsyncedChanges} from 'ui/features/blueprint_course_master/react/components/UnsyncedChanges'
import {ConnectedUnsyncedChanges} from '../UnsyncedChanges'
import MigrationStates from '@canvas/blueprint-courses/react/migrationStates'
const noop = () => {}
@ -90,45 +91,39 @@ function connect(props = {...defaultProps}) {
)
}
QUnit.module('UnsyncedChanges component')
describe('UnsyncedChanges component', () => {
test('renders the UnsyncedChanges component', () => {
const tree = mount(connect())
let node = tree.find('UnsyncedChanges')
ok(node.exists())
node = tree.find('.bcs__history')
ok(node.exists())
const tree = render(connect())
let node = tree.container.querySelector('.bcs__unsynced-item__table')
expect(node).toBeTruthy()
node = tree.container.querySelector('.bcs__history')
expect(node).toBeTruthy()
})
test('renders the migration options component', () => {
const tree = mount(connect())
const node = tree.find('MigrationOptions')
ok(node.exists())
const tree = render(connect())
const node = getByText(tree.container, 'History Settings')
expect(node).toBeTruthy()
})
test('renders the changes properly', () => {
const tree = mount(connect())
const changes = tree.find('tr[data-testid="bcs__unsynced-item"]')
equal(changes.length, 4)
const locks = changes.find('IconBlueprintLockSolid')
equal(locks.length, 1)
const unlocks = changes.find('IconBlueprintSolid')
equal(unlocks.length, 3)
const tree = render(connect())
const changes = tree.container.querySelectorAll('tr[data-testid="bcs__unsynced-item"]')
expect(changes.length).toEqual(4)
const locks = tree.container.querySelectorAll('svg[name="IconBlueprintLock"]')
expect(locks.length).toEqual(1)
const unlocks = tree.container.querySelectorAll('svg[name="IconBlueprint"]')
expect(unlocks.length).toEqual(3)
})
test('renders the media tracks properly', () => {
const tree = mount(connect())
const changes = tree.find('tr[data-testid="bcs__unsynced-item"]')
equal(changes.length, 4)
const assetName = changes.findWhere(
node =>
node.name() === 'Text' &&
node.text() === 'media.mp4 (English)' &&
node.parent().type() === 'span'
)
equal(assetName.length, 1)
const assetType = changes.findWhere(
node => node.name() === 'Text' && node.text() === 'Caption' && node.parent().type() === 'td'
)
equal(assetType.length, 1)
const tree = render(connect())
const changes = tree.container.querySelectorAll('tr[data-testid="bcs__unsynced-item"]')
expect(changes.length).toEqual(4)
const assetName = getAllByText(tree.container, 'media.mp4 (English)')
expect(assetName.length).toEqual(1)
const assetType = getAllByText(tree.container, 'Caption')
expect(assetType.length).toEqual(1)
})
})

View File

@ -67,7 +67,7 @@ export default class LockBanner extends Component {
<Text weight="bold" size="small">
{I18n.t('Locked:')}&nbsp;
</Text>
<Text size="small">{formatLockObject(this.props.itemLocks)}</Text>
<Text size="small" data-testid="lockedMessage">{formatLockObject(this.props.itemLocks)}</Text>
</Alert>
)
}

View File

@ -0,0 +1,74 @@
/*
* Copyright (C) 2024 - 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 LockBanner from '../LockBanner'
describe('LockBanner component', () => {
const defaultProps = () => ({
isLocked: true,
itemLocks: {
content: true,
points: false,
due_dates: false,
availability_dates: false,
},
})
test('renders an Alert when LockBanner is locked', () => {
const props = defaultProps()
props.isLocked = true
const tree = render(<LockBanner {...props} />)
const node = tree.container.querySelector('div')
expect(node).toBeTruthy()
})
test('does not render Alert when LockBanner is locked', () => {
const props = defaultProps()
props.isLocked = false
const tree = render(<LockBanner {...props} />)
const node = tree.container.querySelector('div')
expect(node).toBeFalsy()
})
test('displays locked description text appropriately when one attribute is locked', () => {
const props = defaultProps()
const tree = render(<LockBanner {...props} />)
const text = tree.container.querySelector("[data-testid='lockedMessage'").textContent
expect(text).toEqual('Content')
})
test('displays locked description text appropriately when two attributes are locked', () => {
const props = defaultProps()
props.itemLocks.points = true
const tree = render(<LockBanner {...props} />)
const text = tree.container.querySelector("[data-testid='lockedMessage'").textContent
expect(text).toEqual('Content & Points')
})
test('displays locked description text appropriately when more than two attributes are locked', () => {
const props = defaultProps()
props.itemLocks.points = true
props.itemLocks.due_dates = true
const tree = render(<LockBanner {...props} />)
const text = tree.container.querySelector("[data-testid='lockedMessage'").textContent
expect(text).toEqual('Content, Points & Due Dates')
})
})

View File

@ -0,0 +1,70 @@
/*
* Copyright (C) 2024 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react'
import {shallow} from 'enzyme'
import {render} from '@testing-library/react'
import LockToggle from '../LockToggle'
describe('LockToggle component', () => {
const defaultProps = () => ({
isLocked: true,
isToggleable: true,
})
test('renders the LockToggle component', () => {
const tree = render(<LockToggle {...defaultProps()} />)
const node = tree.container.querySelector('.bpc-lock-toggle')
expect(node).toBeTruthy()
})
test('renders a button when LockToggle is toggleable', () => {
const props = defaultProps()
props.isToggleable = true
const tree = render(<LockToggle {...props} />)
const node = tree.container.querySelector('button')
expect(node).toBeTruthy()
})
test('does not render a button when LockToggle is not toggleable', () => {
const props = defaultProps()
props.isToggleable = false
const tree = shallow(<LockToggle {...props} />)
const node = tree.find('button')
expect(node.exists()).toBeFalsy()
})
test('renders a locked icon when LockToggle is locked', () => {
const props = defaultProps()
props.isLocked = true
const tree = render(<LockToggle {...props} />)
console.log(tree.container.innerHTML)
const node = tree.container.querySelector('svg[name="IconBlueprintLock"]')
expect(node).toBeTruthy()
})
test('renders an unlocked icon when LockToggle is unlocked', () => {
const props = defaultProps()
props.isLocked = false
const tree = render(<LockToggle {...props} />)
console.log(tree.container.innerHTML)
const node = tree.container.querySelector('svg[name="IconBlueprint"]')
expect(node).toBeTruthy()
})
})

View File

@ -0,0 +1,69 @@
/*
* Copyright (C) 2024 - 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 {shallow} from 'enzyme'
import SyncHistoryItem from '../SyncHistoryItem'
import getSampleData from './getSampleData'
describe('SyncHistoryItem component', () => {
const defaultProps = () => ({
heading: null,
migration: getSampleData().history[0],
})
test('renders the SyncHistoryItem component', () => {
const tree = shallow(<SyncHistoryItem {...defaultProps()} />)
const node = tree.find('.bcs__history-item')
expect(node).toBeTruthy()
})
test('renders heading component when migration has changes', () => {
const props = defaultProps()
props.heading = <p className="test-heading">test</p>
const tree = shallow(<SyncHistoryItem {...props} />)
const node = tree.find('.bcs__history-item .test-heading')
expect(node).toBeTruthy()
})
test('does not render the heading component when migration has no changes', () => {
const props = defaultProps()
props.heading = <p className="test-heading">test</p>
props.migration.changes = []
const tree = shallow(<SyncHistoryItem {...props} />)
const node = tree.find('.bcs__history-item .test-heading')
expect(node.exists()).toBeFalsy()
})
test('renders changes using the appropriate prop component', () => {
const props = defaultProps()
props.ChangeComponent = () => <div className="test-change" />
const tree = render(<SyncHistoryItem {...props} />)
const node = tree.container.querySelectorAll('.bcs__history-item .test-change')
expect(node.length).toEqual(props.migration.changes.length)
})
test('includes the name of the person who started the sync', () => {
const tree = render(<SyncHistoryItem {...defaultProps()} />)
const node = tree.container.querySelector('.bcs__history-item__title')
const text = node.textContent
expect(text.indexOf('changes pushed by Bob Jones')).toEqual(57)
})
})

View File

@ -0,0 +1,137 @@
/*
* Copyright (C) 2017 - 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/>.
*/
export default function getSampleData() {
return {
terms: [
{id: '1', name: 'Term One'},
{id: '2', name: 'Term Two'},
],
subAccounts: [
{id: '1', name: 'Account One'},
{id: '2', name: 'Account Two'},
],
childCourse: {
id: '1',
enrollment_term_id: '1',
name: 'Course 1',
},
masterCourse: {
id: '2',
enrollment_term_id: '1',
name: 'Course 2',
},
courses: [
{
id: '1',
name: 'Course One',
course_code: 'course_1',
term: {
id: '1',
name: 'Term One',
},
teachers: [
{
display_name: 'Teacher One',
},
],
sis_course_id: '1001',
},
{
id: '2',
name: 'Course Two',
course_code: 'course_2',
term: {
id: '2',
name: 'Term Two',
},
teachers: [
{
display_name: 'Teacher Two',
},
],
sis_course_id: '1001',
},
],
history: [
{
id: '2',
workflow_state: 'completed',
created_at: '2013-08-28T23:59:00-06:00',
user: {
display_name: 'Bob Jones',
},
changes: [
{
asset_id: '2',
asset_type: 'quiz',
asset_name: 'Chapter 5 Quiz',
change_type: 'updated',
html_url: 'http://localhost:3000/courses/3/quizzes/2',
exceptions: [
{
course_id: '1',
conflicting_changes: ['points'],
name: 'Course 1',
term: {name: 'Default Term'},
},
{
course_id: '5',
conflicting_changes: ['content'],
name: 'Course 5',
term: {name: 'Default Term'},
},
{
course_id: '56',
conflicting_changes: ['deleted'],
name: 'Course 56',
term: {name: 'Default Term'},
},
],
},
],
},
],
unsyncedChanges: [
{
asset_id: '22',
asset_type: 'assignment',
asset_name: 'Another Discussion',
change_type: 'deleted',
html_url: '/courses/4/assignments/22',
locked: false,
},
{
asset_id: '22',
asset_type: 'attachment',
asset_name: 'Bulldog.png',
change_type: 'updated',
html_url: '/courses/4/files/96',
locked: true,
},
{
asset_id: 'page-1',
asset_type: 'wiki_page',
asset_name: 'Page 1',
change_type: 'created',
html_url: '/4/pages/page-1',
locked: false,
},
],
}
}