From 5ce1c9dd57eb5ddb1ce7990864ee9e3a6c418f0b Mon Sep 17 00:00:00 2001 From: Brandon Stull Date: Thu, 16 Sep 2021 08:46:24 -0400 Subject: [PATCH 1/5] #1772 First draft at making verified Materia LTI passback scores available to the viewer component. --- .../obojobo-chunks-materia/server/index.js | 24 +++++++++++ .../viewer-component.js | 43 ++++++++++++++----- .../viewer-component.scss | 11 +++-- 3 files changed, 63 insertions(+), 15 deletions(-) diff --git a/packages/obonode/obojobo-chunks-materia/server/index.js b/packages/obonode/obojobo-chunks-materia/server/index.js index f475cec2b8..b721c119ad 100644 --- a/packages/obonode/obojobo-chunks-materia/server/index.js +++ b/packages/obonode/obojobo-chunks-materia/server/index.js @@ -2,6 +2,7 @@ const router = require('express').Router() //eslint-disable-line new-cap const logger = require('obojobo-express/server/logger') const uuid = require('uuid').v4 const bodyParser = require('body-parser') +const db = require('obojobo-express/server/db') const oboEvents = require('obojobo-express/server/obo_events') const Visit = require('obojobo-express/server/models/visit') const Draft = require('obojobo-express/server/models/draft') @@ -210,4 +211,27 @@ router renderLtiLaunch(launchParams, method, endpoint, res) }) +router + .route('/materia-lti-score-verify') + .get([requireCurrentUser, requireCurrentVisit]) + .get(async (req, res) => { + await db.one( + `SELECT payload + FROM events + WHERE action = 'materia:ltiScorePassback' + AND visit_id = $[visitId] + AND payload->>'lisResultSourcedId' = $[resourceId] + ORDER BY created_at DESC + LIMIT 1`, + { + visitId: req.currentVisit.id, + resourceId: `${req.currentVisit.id}__${req.query.nodeId}` + }).then(result => { + res.send({ + score: result.payload.score, + success: result.payload.success + }) + }) + }) + module.exports = router diff --git a/packages/obonode/obojobo-chunks-materia/viewer-component.js b/packages/obonode/obojobo-chunks-materia/viewer-component.js index 5a1236d99d..c5b511aa53 100644 --- a/packages/obonode/obojobo-chunks-materia/viewer-component.js +++ b/packages/obonode/obojobo-chunks-materia/viewer-component.js @@ -2,6 +2,7 @@ import IFrame from 'obojobo-chunks-iframe/viewer-component' import React from 'react' import isOrNot from 'obojobo-document-engine/src/scripts/common/util/isornot' import TextGroupEl from 'obojobo-document-engine/src/scripts/common/chunk/text-chunk/text-group-el' +import API from 'obojobo-document-engine/src/scripts/viewer/util/api' import './viewer-component.scss' import IFrameControlTypes from 'obojobo-chunks-iframe/iframe-control-types' @@ -27,6 +28,8 @@ export default class Materia extends React.Component { // state setup this.state = { model, + visitId: props.moduleData.navState.visitId, + nodeId: props.model.id, score: null, verifiedScore: false, open: false @@ -40,7 +43,8 @@ export default class Materia extends React.Component { onPostMessageFromMateria(event) { // iframe isn't present OR // postmessage didn't come from the iframe we're listening to - if (!this.iframeRef.current || event.source !== this.iframeRef.current.contentWindow) { + // if (!this.iframeRef.current || event.source !== this.iframeRef.current.contentWindow) { + if (!this.iframeRef.current.refs.iframe || event.source !== this.iframeRef.current.refs.iframe.contentWindow) { return } @@ -58,7 +62,15 @@ export default class Materia extends React.Component { switch (data.type) { case 'materiaScoreRecorded': - this.setState({ score: data.score }) + // this should probably be abstracted in a util function somewhere + API.get(`${window.location.origin}/materia-lti-score-verify?visitId=${this.state.visitId}&nodeId=${this.state.nodeId}`, 'json') + .then(API.processJsonResults) + .then(result => { + this.setState({ + score: result.score, + verifiedScore: true + }) + }) break } } catch (e) { @@ -83,15 +95,24 @@ export default class Materia extends React.Component { } renderTextCaption() { - return this.state.model.modelState.textGroup.first.text ? ( -
- -
- ) : null + let textCaptionRender = null + + if (this.state.model.modelState.textGroup.first.text) { + textCaptionRender = ( +
+ + + Your highest score: {this.state.score}% + +
+ ) + } + + return textCaptionRender } renderCaptionOrScore() { diff --git a/packages/obonode/obojobo-chunks-materia/viewer-component.scss b/packages/obonode/obojobo-chunks-materia/viewer-component.scss index 01fc7ba159..715c739252 100644 --- a/packages/obonode/obojobo-chunks-materia/viewer-component.scss +++ b/packages/obonode/obojobo-chunks-materia/viewer-component.scss @@ -9,12 +9,15 @@ .materia-score { display: block; text-align: center; - margin: 0 auto; - margin-bottom: 2em; + margin: -5em auto 0; font-size: 0.7em; - opacity: 0.5; - margin-top: -3.5em; + opacity: 1; z-index: 2; + transition: opacity 0.4s; + + &.is-not-verified { + opacity: 0; + } } .label { From 71ddb1bec0e1263061f5ac5d3572e340688326d7 Mon Sep 17 00:00:00 2001 From: Brandon Stull Date: Tue, 28 Sep 2021 09:05:33 -0400 Subject: [PATCH 2/5] #1772 Added functionality to the Materia chunk to verify scores and display them in the viewer. Added Materia as a type option for questions. Added support to assessment reviews for partial scores. --- .../src/scss/_includes.scss | 1 + .../constants.js | 14 +- .../obonode/obojobo-chunks-materia/index.js | 3 +- .../server/materiaassessment.js | 22 ++ .../server/route-helpers.js | 2 +- .../viewer-component.js | 85 ++++- .../viewer-component.test.js | 210 +++++++++++- .../__snapshots__/adapter.test.js.snap | 9 + .../editor-component.test.js.snap | 17 +- .../obojobo-chunks-question/adapter.js | 7 + .../obojobo-chunks-question/adapter.test.js | 59 ++++ .../obojobo-chunks-question/converter.js | 11 +- .../editor-component.js | 2 + .../feedback-labels.js | 44 ++- .../feedback-labels.test.js | 317 ++++++++++-------- .../question-outcome.js | 41 +++ .../viewer-component.js | 2 + .../viewer-component.scss | 4 + .../viewer-component.test.js | 1 + .../assessment-score-report-view.scss | 1 + .../__snapshots__/basic-review.test.js.snap | 3 + 21 files changed, 679 insertions(+), 176 deletions(-) create mode 100644 packages/obonode/obojobo-chunks-materia/server/materiaassessment.js diff --git a/packages/app/obojobo-document-engine/src/scss/_includes.scss b/packages/app/obojobo-document-engine/src/scss/_includes.scss index 4ff4afa9cf..3d2fca859e 100644 --- a/packages/app/obojobo-document-engine/src/scss/_includes.scss +++ b/packages/app/obojobo-document-engine/src/scss/_includes.scss @@ -15,6 +15,7 @@ $color-bg2: #f4f4f4; $color-bg3: #131313; $color-action-bg: #f9f4ff; $color-correct: #38ae24; +$color-partially-correct: #ffc802; $color-incorrect: #c21f00; $color-survey: #48b8b9; $color-alt-correct: #ffc802; diff --git a/packages/obonode/obojobo-chunks-abstract-assessment/constants.js b/packages/obonode/obojobo-chunks-abstract-assessment/constants.js index 845ce5a5a2..2eef4742f5 100644 --- a/packages/obonode/obojobo-chunks-abstract-assessment/constants.js +++ b/packages/obonode/obojobo-chunks-abstract-assessment/constants.js @@ -1,5 +1,6 @@ const CHOICE_NODE = 'ObojoboDraft.Chunks.AbstractAssessment.Choice' const FEEDBACK_NODE = 'ObojoboDraft.Chunks.AbstractAssessment.Feedback' +const MATERIA_NODE = 'ObojoboDraft.Chunks.Materia' // Whenever an inheriting component is created // Add its Assessment type to the valid assessments, its answer type to valid answers @@ -16,22 +17,27 @@ import { } from 'obojobo-chunks-multiple-choice-assessment/constants' import mcAssessment from 'obojobo-chunks-multiple-choice-assessment/empty-node.json' +import materiaAssessment from 'obojobo-chunks-materia/empty-node.json' + const assessmentToAnswer = { [NUMERIC_ASSESSMENT_NODE]: numericAssessment.children[0].children[0], - [MC_ASSESSMENT_NODE]: mcAssessment.children[0].children[0] + [MC_ASSESSMENT_NODE]: mcAssessment.children[0].children[0], + [MATERIA_NODE]: materiaAssessment.children[0] } const answerTypeToJson = { [NUMERIC_ANSWER_NODE]: numericAssessment.children[0].children[0], - [MC_ANSWER_NODE]: mcAssessment.children[0].children[0] + [MC_ANSWER_NODE]: mcAssessment.children[0].children[0], + [MATERIA_NODE]: materiaAssessment.children[0] } const answerToAssessment = { [NUMERIC_ANSWER_NODE]: numericAssessment, - [MC_ANSWER_NODE]: mcAssessment + [MC_ANSWER_NODE]: mcAssessment, + [MATERIA_NODE]: materiaAssessment } -const validAssessments = [NUMERIC_ASSESSMENT_NODE, MC_ASSESSMENT_NODE] +const validAssessments = [NUMERIC_ASSESSMENT_NODE, MC_ASSESSMENT_NODE, MATERIA_NODE] const validAnswers = [NUMERIC_ANSWER_NODE, MC_ANSWER_NODE] export { diff --git a/packages/obonode/obojobo-chunks-materia/index.js b/packages/obonode/obojobo-chunks-materia/index.js index 4a88ae9555..57b76165d1 100644 --- a/packages/obonode/obojobo-chunks-materia/index.js +++ b/packages/obonode/obojobo-chunks-materia/index.js @@ -7,6 +7,7 @@ module.exports = { clientScripts: { viewer: 'viewer.js', editor: 'editor.js' - } + }, + serverScripts: ['server/materiaassessment'] } } diff --git a/packages/obonode/obojobo-chunks-materia/server/materiaassessment.js b/packages/obonode/obojobo-chunks-materia/server/materiaassessment.js new file mode 100644 index 0000000000..46fa5c265a --- /dev/null +++ b/packages/obonode/obojobo-chunks-materia/server/materiaassessment.js @@ -0,0 +1,22 @@ +const DraftNode = require('obojobo-express/server/models/draft_node') + +class NumericAssessment extends DraftNode { + static get nodeName() { + return 'ObojoboDraft.Chunks.Materia' + } + + constructor(draftTree, node, initFn) { + super(draftTree, node, initFn) + this.registerEvents({ + 'ObojoboDraft.Chunks.Question:calculateScore': this.onCalculateScore + }) + } + + onCalculateScore(app, question, responseRecord, setScore) { + if (!question.contains(this.node)) return + + setScore(responseRecord.response.score) + } +} + +module.exports = NumericAssessment diff --git a/packages/obonode/obojobo-chunks-materia/server/route-helpers.js b/packages/obonode/obojobo-chunks-materia/server/route-helpers.js index a2a418e3b6..63002446c2 100644 --- a/packages/obonode/obojobo-chunks-materia/server/route-helpers.js +++ b/packages/obonode/obojobo-chunks-materia/server/route-helpers.js @@ -58,7 +58,7 @@ const widgetLaunchParams = (document, visit, user, materiaOboNodeId, baseUrl) => } // materia currently uses context_id to group scores and attempts - // obojobo doesn't support materia as scoreable questions yet, so the key in use here is intended to: + // the key in use here is intended to: // * support materia in content pages // * re lti launch will reset scores/attempts // * browser reload of the window will resume an attempt/score window diff --git a/packages/obonode/obojobo-chunks-materia/viewer-component.js b/packages/obonode/obojobo-chunks-materia/viewer-component.js index c5b511aa53..07548e7d9e 100644 --- a/packages/obonode/obojobo-chunks-materia/viewer-component.js +++ b/packages/obonode/obojobo-chunks-materia/viewer-component.js @@ -3,6 +3,10 @@ import React from 'react' import isOrNot from 'obojobo-document-engine/src/scripts/common/util/isornot' import TextGroupEl from 'obojobo-document-engine/src/scripts/common/chunk/text-chunk/text-group-el' import API from 'obojobo-document-engine/src/scripts/viewer/util/api' +import QuestionUtil from 'obojobo-document-engine/src/scripts/viewer/util/question-util' + +import Viewer from 'obojobo-document-engine/src/scripts/viewer' +const { NavUtil } = Viewer.util import './viewer-component.scss' import IFrameControlTypes from 'obojobo-chunks-iframe/iframe-control-types' @@ -11,16 +15,23 @@ import IFrameSizingTypes from 'obojobo-chunks-iframe/iframe-sizing-types' export default class Materia extends React.Component { constructor(props) { super(props) + this.iframeRef = React.createRef() + let iframeSrc = this.srcToLTILaunchUrl(props.moduleData.navState.visitId, props.model.id) + if (props.mode === 'review') { + iframeSrc = props.response.scoreUrl + } + // manipulate iframe settings const model = props.model.clone() model.modelState = { ...model.modelState, - src: this.srcToLTILaunchUrl(props.moduleData.navState.visitId, props.model.id), + src: iframeSrc, border: true, fit: 'scale', initialZoom: 1, + // autoload: props.mode === 'review', // could be annoying either way, maybe better overall if not automatic controls: [IFrameControlTypes.RELOAD], sizing: IFrameSizingTypes.FIXED } @@ -40,11 +51,17 @@ export default class Materia extends React.Component { this.onShow = this.onShow.bind(this) } + static isResponseEmpty(response) { + return !response.verifiedScore + } + onPostMessageFromMateria(event) { - // iframe isn't present OR - // postmessage didn't come from the iframe we're listening to - // if (!this.iframeRef.current || event.source !== this.iframeRef.current.contentWindow) { - if (!this.iframeRef.current.refs.iframe || event.source !== this.iframeRef.current.refs.iframe.contentWindow) { + // iframe isn't present + if (!this.iframeRef || !this.iframeRef.current || !this.iframeRef.current.refs.iframe) { + return + } + // OR postmessage didn't come from the iframe we're listening to + if (event.source !== this.iframeRef.current.refs.iframe.contentWindow) { return } @@ -63,13 +80,36 @@ export default class Materia extends React.Component { switch (data.type) { case 'materiaScoreRecorded': // this should probably be abstracted in a util function somewhere - API.get(`${window.location.origin}/materia-lti-score-verify?visitId=${this.state.visitId}&nodeId=${this.state.nodeId}`, 'json') + API.get( + `/materia-lti-score-verify?visitId=${this.state.visitId}&nodeId=${this.state.nodeId}`, + 'json' + ) .then(API.processJsonResults) .then(result => { - this.setState({ + const newState = { score: result.score, verifiedScore: true + } + this.setState({ + ...this.state, + ...newState }) + + const modelId = this.props.questionModel.get('id') + const moduleContext = NavUtil.getContext(this.props.moduleData.navState) + + QuestionUtil.setResponse( + modelId, + { + ...newState, + scoreUrl: data.score_url + }, + null, + moduleContext, + moduleContext.split(':')[1], + moduleContext.split(':')[2], + false + ) }) break } @@ -97,6 +137,13 @@ export default class Materia extends React.Component { renderTextCaption() { let textCaptionRender = null + let scoreRender = null + if (this.state.score && this.state.verifiedScore) { + scoreRender = ( + Your highest score: {this.state.score}% + ) + } + if (this.state.model.modelState.textGroup.first.text) { textCaptionRender = (
@@ -105,9 +152,7 @@ export default class Materia extends React.Component { textItem={this.state.model.modelState.textGroup.first} groupIndex="0" /> - - Your highest score: {this.state.score}% - + {scoreRender}
) } @@ -124,6 +169,26 @@ export default class Materia extends React.Component { } } + getInstructions() { + return ( + + Embedded Materia widget. + Play the embedded Materia widget to receive a score. Your highest score will be saved. + + ) + } + + calculateScore() { + if (!this.props.score) { + return null + } + + return { + score: this.props.score, + details: null + } + } + render() { return (
diff --git a/packages/obonode/obojobo-chunks-materia/viewer-component.test.js b/packages/obonode/obojobo-chunks-materia/viewer-component.test.js index 943e0a7e72..e7e838d7aa 100644 --- a/packages/obonode/obojobo-chunks-materia/viewer-component.test.js +++ b/packages/obonode/obojobo-chunks-materia/viewer-component.test.js @@ -1,3 +1,20 @@ +jest.mock('obojobo-document-engine/src/scripts/viewer/util/api', () => ({ + get: jest.fn(), + processJsonResults: jest.fn() +})) + +jest.mock('obojobo-document-engine/src/scripts/viewer/util/question-util', () => ({ + setResponse: jest.fn() +})) + +jest.mock('obojobo-document-engine/src/scripts/viewer', () => ({ + util: { + NavUtil: { + getContext: jest.fn().mockReturnValue('mock:module:context') + } + } +})) + jest.mock('react-dom') jest.mock( 'obojobo-chunks-iframe/viewer-component', @@ -18,6 +35,19 @@ require('./viewer') // used to register this oboModel describe('Materia viewer component', () => { let model let moduleData + let questionModel + + const flushPromises = global.flushPromises + const API = require('obojobo-document-engine/src/scripts/viewer/util/api') + const Questionutil = require('obojobo-document-engine/src/scripts/viewer/util/question-util') + + const Viewer = require('obojobo-document-engine/src/scripts/viewer') + const { NavUtil } = Viewer.util + + const mockModuleTitle = 'mocked-module-title' + const mockVisitId = 'mock-visit-id' + const mockNodeId = 'mock-obo-id' + const mockQuestionId = 'mock-question-id' beforeEach(() => { jest.resetAllMocks() @@ -26,7 +56,7 @@ describe('Materia viewer component', () => { OboModel.__setNextGeneratedLocalId('mock-uuid') model = OboModel.create({ - id: 'mock-obo-id', + id: mockNodeId, type: 'ObojoboDraft.Chunks.Materia', content: { src: 'http://www.example.com' @@ -35,12 +65,16 @@ describe('Materia viewer component', () => { moduleData = { model: { - title: 'mocked-module-title' + title: mockModuleTitle }, navState: { - visitId: 'mock-visit-id' + visitId: mockVisitId } } + + questionModel = { + get: jest.fn().mockReturnValue(mockQuestionId) + } }) afterEach(() => {}) @@ -53,6 +87,11 @@ describe('Materia viewer component', () => { } const component = renderer.create() expect(component.toJSON()).toMatchSnapshot() + + const inst = component.getInstance() + expect(inst.state.model.modelState.src).toBe( + `http://localhost/materia-lti-launch?visitId=${mockVisitId}&nodeId=${mockNodeId}` + ) }) test('adds and removes listener for postmessage when mounting and unmounting', () => { @@ -86,7 +125,7 @@ describe('Materia viewer component', () => { ) }) - test('onPostMessageFromMateria without an iframe ref doesnt update state', () => { + test('onPostMessageFromMateria with an empty iframe ref doesnt update state', () => { expect.hasAssertions() const props = { model, @@ -102,6 +141,22 @@ describe('Materia viewer component', () => { expect(inst.state).toHaveProperty('score', null) }) + test('onPostMessageFromMateria without an iframe ref object doesnt update state', () => { + expect.hasAssertions() + const props = { + model, + moduleData + } + + const component = renderer.create() + + const inst = component.getInstance() + inst.iframeRef = null + const event = { source: '', data: JSON.stringify({ score: 100 }) } + inst.onPostMessageFromMateria(event) + expect(inst.state).toHaveProperty('score', null) + }) + test('onPostMessageFromMateria without a matching iframe and event source doesnt update state', () => { expect.hasAssertions() const props = { @@ -112,7 +167,7 @@ describe('Materia viewer component', () => { const component = renderer.create() const inst = component.getInstance() - inst.iframeRef = { current: { contentWindow: 'different-mock-window' } } + inst.iframeRef = { current: { refs: { iframe: { contentWindow: 'different-mock-window' } } } } const event = { source: 'mock-window', data: JSON.stringify({ score: 100 }) } inst.onPostMessageFromMateria(event) expect(inst.state).toHaveProperty('score', null) @@ -128,7 +183,9 @@ describe('Materia viewer component', () => { const component = renderer.create() const inst = component.getInstance() - inst.iframeRef = { current: { contentWindow: 'http://not-localhost/' } } + inst.iframeRef = { + current: { refs: { iframe: { contentWindow: 'http://not-localhost/whatever' } } } + } const event = { source: 'http://localhost/whatever', data: JSON.stringify({ score: 100 }) } inst.onPostMessageFromMateria(event) expect(inst.state).toHaveProperty('score', null) @@ -144,7 +201,9 @@ describe('Materia viewer component', () => { const component = renderer.create() const inst = component.getInstance() - inst.iframeRef = { current: { contentWindow: 'http://localhost/whatever' } } + inst.iframeRef = { + current: { refs: { iframe: { contentWindow: 'http://localhost/whatever' } } } + } const event = { origin: 'http://not-localhost', source: 'http://localhost/whatever', @@ -164,7 +223,9 @@ describe('Materia viewer component', () => { const component = renderer.create() const inst = component.getInstance() - inst.iframeRef = { current: { contentWindow: 'http://localhost/whatever' } } + inst.iframeRef = { + current: { refs: { iframe: { contentWindow: 'http://localhost/whatever' } } } + } const event = { origin: 'http://localhost', source: 'http://localhost/whatever', @@ -184,7 +245,9 @@ describe('Materia viewer component', () => { const component = renderer.create() const inst = component.getInstance() - inst.iframeRef = { current: { contentWindow: 'http://localhost/whatever' } } + inst.iframeRef = { + current: { refs: { iframe: { contentWindow: 'http://localhost/whatever' } } } + } const event = { origin: 'http://localhost', source: 'http://localhost/whatever', @@ -194,24 +257,67 @@ describe('Materia viewer component', () => { expect(inst.state).toHaveProperty('score', null) }) - test('onPostMessageFromMateria updates the score', () => { + test('onPostMessageFromMateria makes an API call to verify the score', () => { + API.get = jest.fn().mockResolvedValue(true) + API.processJsonResults = jest.fn().mockResolvedValue({ score: 100, success: true }) + + NavUtil.getContext = jest.fn().mockReturnValue('mock:module:context') + expect.hasAssertions() const props = { model, - moduleData + moduleData, + questionModel } const component = renderer.create() + // While we're here, make sure the score dialog appears correctly. + // It shouldn't exist by default + expect(component.root.findAllByProps({ className: 'materia-score verified' }).length).toBe(0) + const inst = component.getInstance() - inst.iframeRef = { current: { contentWindow: 'http://localhost/whatever' } } + inst.iframeRef = { + current: { refs: { iframe: { contentWindow: 'http://localhost/whatever' } } } + } const event = { origin: 'http://localhost', source: 'http://localhost/whatever', - data: JSON.stringify({ type: 'materiaScoreRecorded', score: 100 }) + data: JSON.stringify({ + type: 'materiaScoreRecorded', + score: 100, + score_url: 'http://localhost/score' + }) } inst.onPostMessageFromMateria(event) - expect(inst.state).toHaveProperty('score', 100) + + return flushPromises().then(() => { + expect(API.get).toHaveBeenCalledTimes(1) + expect(API.get).toHaveBeenCalledWith( + `/materia-lti-score-verify?visitId=${mockVisitId}&nodeId=${mockNodeId}`, + 'json' + ) + + expect(inst.state).toHaveProperty('score', 100) + + expect(Questionutil.setResponse).toHaveBeenCalledTimes(1) + expect(Questionutil.setResponse).toHaveBeenCalledWith( + mockQuestionId, + { + score: 100, + scoreUrl: 'http://localhost/score', + verifiedScore: true + }, + null, + 'mock:module:context', + 'module', + 'context', + false + ) + + const scoreRender = component.root.findByProps({ className: 'materia-score verified' }) + expect(scoreRender.children.join('')).toBe('Your highest score: 100%') + }) }) test('onPostMessageFromMateria handles json parsing errors', () => { @@ -224,7 +330,9 @@ describe('Materia viewer component', () => { const component = renderer.create() const inst = component.getInstance() - inst.iframeRef = { current: { contentWindow: 'http://localhost/whatever' } } + inst.iframeRef = { + current: { refs: { iframe: { contentWindow: 'http://localhost/whatever' } } } + } const event = { origin: 'http://localhost', source: 'http://localhost/whatever', @@ -321,4 +429,76 @@ describe('Materia viewer component', () => { // it shouldn't render one expect(component.root.findAllByProps({ groupIndex: '0' })).toHaveLength(0) }) + + test('rendering in review mode changes the iframe src', () => { + const mockScoreUrl = 'http://localhost/url/to/score/screen' + const props = { + model, + moduleData, + mode: 'review', + response: { + scoreUrl: mockScoreUrl + } + } + const component = renderer.create() + + const inst = component.getInstance() + + expect(inst.state.model.modelState.src).toBe(mockScoreUrl) + }) + + test('isResponseEmpty returns true if the response score is not verified - does not exist', () => { + expect(Materia.isResponseEmpty({})).toBe(true) + expect(Materia.isResponseEmpty({ score: 100, verifiedScore: false })).toBe(true) + }) + test('isResponseEmpty returns true if the response score is not verified - false value', () => { + expect(Materia.isResponseEmpty({ score: 100, verifiedScore: false })).toBe(true) + }) + test('isResponseEmpty returns false if the response score is verified', () => { + expect(Materia.isResponseEmpty({ score: 100, verifiedScore: true })).toBe(false) + }) + + test('calculateScore returns null when no score is recorded', () => { + const props = { + model, + moduleData + } + const component = renderer.create() + + const inst = component.getInstance() + expect(inst.calculateScore()).toBe(null) + }) + + test('calculateScore returns properly when a score is recorded', () => { + const mockScore = 90 + + const props = { + model, + moduleData, + score: mockScore + } + const component = renderer.create() + + const inst = component.getInstance() + expect(inst.calculateScore()).toEqual({ score: mockScore, details: null }) + }) + + test('getInstructions returns properly', () => { + const props = { + model, + moduleData + } + const component = renderer.create() + + const inst = component.getInstance() + const instructionsFragment = renderer.create(inst.getInstructions()) + + const fragmentRender = instructionsFragment.root.findByProps({ + className: 'for-screen-reader-only' + }) + expect(fragmentRender.children[0]).toBe('Embedded Materia widget.') + expect(fragmentRender.parent.children[1]).toBe( + 'Play the embedded Materia widget to receive a score. Your highest score will be saved.' + ) + }) }) diff --git a/packages/obonode/obojobo-chunks-question/__snapshots__/adapter.test.js.snap b/packages/obonode/obojobo-chunks-question/__snapshots__/adapter.test.js.snap index 9d83d08912..98177e4280 100644 --- a/packages/obonode/obojobo-chunks-question/__snapshots__/adapter.test.js.snap +++ b/packages/obonode/obojobo-chunks-question/__snapshots__/adapter.test.js.snap @@ -15,6 +15,11 @@ Object { "z", ], "needsUpdate": false, + "partialLabels": Array [ + "l", + "m", + "n", + ], "revealAnswer": "when-incorrect", "solution": Object { "children": Array [ @@ -58,6 +63,7 @@ Object { "editing": false, "incorrectLabels": null, "needsUpdate": false, + "partialLabels": null, "revealAnswer": "default", "solution": null, "type": "default", @@ -71,6 +77,7 @@ Object { "editing": false, "incorrectLabels": null, "needsUpdate": false, + "partialLabels": null, "revealAnswer": "default", "solution": null, "type": "default", @@ -82,6 +89,7 @@ Object { "content": Object { "correctLabels": "a|b|c", "incorrectLabels": "d|e|f", + "partialLabels": null, "revealAnswer": "default", "solution": Object { "children": Array [ @@ -124,6 +132,7 @@ Object { "content": Object { "correctLabels": null, "incorrectLabels": null, + "partialLabels": null, "revealAnswer": "default", "solution": null, "type": "default", diff --git a/packages/obonode/obojobo-chunks-question/__snapshots__/editor-component.test.js.snap b/packages/obonode/obojobo-chunks-question/__snapshots__/editor-component.test.js.snap index 5340f9659b..35e70f3a8f 100644 --- a/packages/obonode/obojobo-chunks-question/__snapshots__/editor-component.test.js.snap +++ b/packages/obonode/obojobo-chunks-question/__snapshots__/editor-component.test.js.snap @@ -32,6 +32,11 @@ exports[`Question Editor Node Question builds the expected component (not in ass > Input a number +