diff --git a/cypress/e2e/DoenetML/tagSpecific/answer.cy.js b/cypress/e2e/DoenetML/tagSpecific/answer.cy.js index 8b9d851470..1256579885 100644 --- a/cypress/e2e/DoenetML/tagSpecific/answer.cy.js +++ b/cypress/e2e/DoenetML/tagSpecific/answer.cy.js @@ -22177,4 +22177,282 @@ describe('Answer Tag Tests', function () { }); + it('hand-graded answers', () => { + cy.window().then(async (win) => { + win.postMessage({ + doenetML: ` + a +

Maths: + + + + x + x + x + x +

+

Submitted responses: + + + + + + + +

+

Credit achieved: + + + + + + + +

+

Texts: + + + hello + hello +

+

Submitted responses: + + + + +

+

Credit achieved: + + + + +

+

Multiple inputs + + + +

+

Submitted responses: + + + + + + +

+

Credit achieved: + + + +

+

Inputs outside answer + $oi1i1$oi1i2 + $oi2i1$oi2i2 + $oi3i1$oi3i2 +

+

Submitted responses: + + + + + + +

+

Credit achieved: + + + +

+ + + `}, "*"); + }); + + + cy.get("#\\/_text1").should("have.text", "a"); + + cy.get('#\\/m1 textarea').type("x{enter}", { force: true }) + cy.get('#\\/m2 textarea').type("x{enter}", { force: true }) + cy.get('#\\/m3 textarea').type("x{enter}", { force: true }) + cy.get('#\\/m4 textarea').type("x{enter}", { force: true }) + cy.get('#\\/m5 textarea').type("x{enter}", { force: true }) + cy.get('#\\/m6 textarea').type("x{enter}", { force: true }) + cy.get('#\\/m7 textarea').type("x{enter}", { force: true }) + + cy.get('#\\/t1 input').type("hello{enter}") + cy.get('#\\/t2 input').type("hello{enter}") + cy.get('#\\/t3 input').type("hello{enter}") + cy.get('#\\/t4 input').type("hello{enter}") + + cy.get('#\\/mi1 textarea').eq(0).type("x{enter}", { force: true }) + cy.get('#\\/mi1 textarea').eq(1).type("y{enter}", { force: true }) + cy.get('#\\/mi1_submit').click(); + cy.get('#\\/mi2 input').eq(0).type("hello{enter}") + cy.get('#\\/mi2 input').eq(1).type("bye{enter}") + cy.get('#\\/mi2_submit').click(); + cy.get('#\\/mi3 textarea').type("x{enter}", { force: true }) + cy.get('#\\/mi3 input').type("bye{enter}") + cy.get('#\\/mi3_submit').click(); + + + cy.get('#\\/oi1i1 textarea').type("x{enter}", { force: true }) + cy.get('#\\/oi1i2 textarea').type("y{enter}", { force: true }) + cy.get('#\\/oi1_submit').click(); + cy.get('#\\/oi2i1 input').type("hello{enter}") + cy.get('#\\/oi2i2 input').type("bye{enter}") + cy.get('#\\/oi2_submit').click(); + cy.get('#\\/oi3i1 textarea').type("x{enter}", { force: true }) + cy.get('#\\/oi3i2 input').type("bye{enter}") + cy.get('#\\/oi3_submit').click(); + + + cy.get('#\\/oi3sr2').should('have.text', 'bye') + + cy.get('#\\/m1sr .mjx-mrow').eq(0).should('have.text', "x") + cy.get('#\\/m2sr .mjx-mrow').eq(0).should('have.text', "x") + cy.get('#\\/m3sr .mjx-mrow').eq(0).should('have.text', "x") + cy.get('#\\/m4sr .mjx-mrow').eq(0).should('have.text', "x") + cy.get('#\\/m5sr .mjx-mrow').eq(0).should('have.text', "x") + cy.get('#\\/m6sr .mjx-mrow').eq(0).should('have.text', "x") + cy.get('#\\/m7sr .mjx-mrow').eq(0).should('have.text', "x") + + cy.get('#\\/t1sr').should('have.text', "hello") + cy.get('#\\/t2sr').should('have.text', "hello") + cy.get('#\\/t3sr').should('have.text', "hello") + cy.get('#\\/t4sr').should('have.text', "hello") + + cy.get('#\\/mi1sr1 .mjx-mrow').eq(0).should('have.text', "x") + cy.get('#\\/mi1sr2 .mjx-mrow').eq(0).should('have.text', "y") + cy.get('#\\/mi2sr1').should('have.text', "hello") + cy.get('#\\/mi2sr2').should('have.text', "bye") + cy.get('#\\/mi3sr1 .mjx-mrow').eq(0).should('have.text', "x") + cy.get('#\\/mi3sr2').should('have.text', "bye") + + cy.get('#\\/oi1sr1 .mjx-mrow').eq(0).should('have.text', "x") + cy.get('#\\/oi1sr2 .mjx-mrow').eq(0).should('have.text', "y") + cy.get('#\\/oi2sr1').should('have.text', "hello") + cy.get('#\\/oi2sr2').should('have.text', "bye") + cy.get('#\\/oi3sr1 .mjx-mrow').eq(0).should('have.text', "x") + cy.get('#\\/oi3sr2').should('have.text', "bye") + + + cy.get('#\\/m1ca').should('have.text', '0') + cy.get('#\\/m2ca').should('have.text', '0') + cy.get('#\\/m3ca').should('have.text', '0') + cy.get('#\\/m4ca').should('have.text', '0') + cy.get('#\\/m5ca').should('have.text', '0') + cy.get('#\\/m6ca').should('have.text', '0') + cy.get('#\\/m7ca').should('have.text', '0') + + cy.get('#\\/t1ca').should('have.text', '0') + cy.get('#\\/t2ca').should('have.text', '0') + cy.get('#\\/t3ca').should('have.text', '0') + cy.get('#\\/t4ca').should('have.text', '0') + + cy.get('#\\/mi1ca').should('have.text', '0') + cy.get('#\\/mi2ca').should('have.text', '0') + cy.get('#\\/mi3ca').should('have.text', '0') + + cy.get('#\\/oi1ca').should('have.text', '0') + cy.get('#\\/oi2ca').should('have.text', '0') + cy.get('#\\/oi3ca').should('have.text', '0') + + + cy.log("revise answers and submit") + + cy.get('#\\/m1 textarea').type("{end}y{enter}", { force: true }) + cy.get('#\\/m2 textarea').type("{end}y{enter}", { force: true }) + cy.get('#\\/m3 textarea').type("{end}y{enter}", { force: true }) + cy.get('#\\/m4 textarea').type("{end}y{enter}", { force: true }) + cy.get('#\\/m5 textarea').type("{end}y{enter}", { force: true }) + cy.get('#\\/m6 textarea').type("{end}y{enter}", { force: true }) + cy.get('#\\/m7 textarea').type("{end}y{enter}", { force: true }) + + cy.get('#\\/t1 input').type(" there{enter}") + cy.get('#\\/t2 input').type(" there{enter}") + cy.get('#\\/t3 input').type(" there{enter}") + cy.get('#\\/t4 input').type(" there{enter}") + + + cy.get('#\\/mi1 textarea').eq(0).type("{end}z{enter}", { force: true }) + cy.get('#\\/mi1_submit').click(); + cy.get('#\\/mi1 textarea').eq(1).type("{end}z{enter}", { force: true }) + cy.get('#\\/mi1_submit').click(); + cy.get('#\\/mi2 input').eq(0).type(" there{enter}") + cy.get('#\\/mi2_submit').click(); + cy.get('#\\/mi2 input').eq(1).type(" now{enter}") + cy.get('#\\/mi2_submit').click(); + cy.get('#\\/mi3 textarea').type("{end}y{enter}", { force: true }) + cy.get('#\\/mi3_submit').click(); + cy.get('#\\/mi3 input').type(" now{enter}") + cy.get('#\\/mi3_submit').click(); + + + + cy.get('#\\/oi1i1 textarea').type("{end}z{enter}", { force: true }) + cy.get('#\\/oi1_submit').click(); + cy.get('#\\/oi1i2 textarea').type("{end}z{enter}", { force: true }) + cy.get('#\\/oi1_submit').click(); + cy.get('#\\/oi2i1 input').type(" there{enter}") + cy.get('#\\/oi2_submit').click(); + cy.get('#\\/oi2i2 input').type(" now{enter}") + cy.get('#\\/oi2_submit').click(); + cy.get('#\\/oi3i1 textarea').type("{end}y{enter}", { force: true }) + cy.get('#\\/oi3_submit').click(); + cy.get('#\\/oi3i2 input').type(" now{enter}") + cy.get('#\\/oi3_submit').click(); + + + cy.get('#\\/oi3sr2').should('have.text', 'bye now') + + cy.get('#\\/m1sr .mjx-mrow').eq(0).should('have.text', "xy") + cy.get('#\\/m2sr .mjx-mrow').eq(0).should('have.text', "xy") + cy.get('#\\/m3sr .mjx-mrow').eq(0).should('have.text', "xy") + cy.get('#\\/m4sr .mjx-mrow').eq(0).should('have.text', "xy") + cy.get('#\\/m5sr .mjx-mrow').eq(0).should('have.text', "xy") + cy.get('#\\/m6sr .mjx-mrow').eq(0).should('have.text', "xy") + cy.get('#\\/m7sr .mjx-mrow').eq(0).should('have.text', "xy") + + cy.get('#\\/t1sr').should('have.text', "hello there") + cy.get('#\\/t2sr').should('have.text', "hello there") + cy.get('#\\/t3sr').should('have.text', "hello there") + cy.get('#\\/t4sr').should('have.text', "hello there") + + cy.get('#\\/mi1sr1 .mjx-mrow').eq(0).should('have.text', "xz") + cy.get('#\\/mi1sr2 .mjx-mrow').eq(0).should('have.text', "yz") + cy.get('#\\/mi2sr1').should('have.text', "hello there") + cy.get('#\\/mi2sr2').should('have.text', "bye now") + cy.get('#\\/mi3sr1 .mjx-mrow').eq(0).should('have.text', "xy") + cy.get('#\\/mi3sr2').should('have.text', "bye now") + + cy.get('#\\/oi1sr1 .mjx-mrow').eq(0).should('have.text', "xz") + cy.get('#\\/oi1sr2 .mjx-mrow').eq(0).should('have.text', "yz") + cy.get('#\\/oi2sr1').should('have.text', "hello there") + cy.get('#\\/oi2sr2').should('have.text', "bye now") + cy.get('#\\/oi3sr1 .mjx-mrow').eq(0).should('have.text', "xy") + cy.get('#\\/oi3sr2').should('have.text', "bye now") + + + cy.get('#\\/m1ca').should('have.text', '0') + cy.get('#\\/m2ca').should('have.text', '0') + cy.get('#\\/m3ca').should('have.text', '0') + cy.get('#\\/m4ca').should('have.text', '0') + cy.get('#\\/m5ca').should('have.text', '0') + cy.get('#\\/m6ca').should('have.text', '0') + cy.get('#\\/m7ca').should('have.text', '0') + + cy.get('#\\/t1ca').should('have.text', '0') + cy.get('#\\/t2ca').should('have.text', '0') + cy.get('#\\/t3ca').should('have.text', '0') + cy.get('#\\/t4ca').should('have.text', '0') + + cy.get('#\\/mi1ca').should('have.text', '0') + cy.get('#\\/mi2ca').should('have.text', '0') + cy.get('#\\/mi3ca').should('have.text', '0') + + cy.get('#\\/oi1ca').should('have.text', '0') + cy.get('#\\/oi2ca').should('have.text', '0') + cy.get('#\\/oi3ca').should('have.text', '0') + }); + }) diff --git a/src/Core/components/Answer.js b/src/Core/components/Answer.js index d3e9165179..434c4e27a3 100644 --- a/src/Core/components/Answer.js +++ b/src/Core/components/Answer.js @@ -36,6 +36,12 @@ export default class Answer extends InlineComponent { defaultValue: 1, public: true, }; + attributes.handGraded = { + createPrimitiveOfType: "boolean", + createStateVariable: "handGraded", + defaultValue: false, + public: true, + }; attributes.inline = { createComponentOfType: "boolean", createStateVariable: "inline", @@ -266,6 +272,7 @@ export default class Answer extends InlineComponent { let nChoicesFound = 0; let definitelyDoNotAddInput = false, mayNeedInput = false; let foundResponse = false; + let foundAward = false; let childIsWrappable = []; for (let child of matchedChildren) { @@ -301,6 +308,7 @@ export default class Answer extends InlineComponent { childIsWrappable.push(true); nChoicesFound++; } else if (componentIsSpecifiedType(child, "award")) { + foundAward = true; childIsWrappable.push(false); if (child.attributes?.sourcesAreResponses) { foundResponse = true; @@ -407,6 +415,10 @@ export default class Answer extends InlineComponent { } + if (componentAttributes.handGraded && !foundAward) { + mayNeedInput = true; + } + if (!mayNeedInput && !foundResponse) { // recurse to all descendants of awards to see if have a response for (let child of matchedChildren) { @@ -588,6 +600,10 @@ export default class Answer extends InlineComponent { showCorrectnessFlag: { dependencyType: "flag", flagName: "showCorrectness" + }, + handGraded: { + dependencyType: "stateVariable", + variableName: "handGraded" } }), definition({ dependencyValues, usedDefault }) { @@ -595,7 +611,7 @@ export default class Answer extends InlineComponent { if (!usedDefault.showCorrectnessPreliminary) { showCorrectness = dependencyValues.showCorrectnessPreliminary } else { - showCorrectness = dependencyValues.showCorrectnessFlag !== false; + showCorrectness = dependencyValues.showCorrectnessFlag !== false && !dependencyValues.handGraded; } return { setValue: { showCorrectness } } } @@ -645,6 +661,10 @@ export default class Answer extends InlineComponent { haveAwardThatRequiresInput: { dependencyType: "stateVariable", variableName: "haveAwardThatRequiresInput" + }, + handGraded: { + dependencyType: "stateVariable", + variableName: "handGraded" } }; @@ -674,7 +694,7 @@ export default class Answer extends InlineComponent { let skipFirstSugaredInput = inputChildren[0]?.componentType !== "choiceInput" && ( - !dependencyValues.haveAwardThatRequiresInput + !(dependencyValues.haveAwardThatRequiresInput || dependencyValues.handGraded) || dependencyValues.allInputChildrenIncludingSugared.length > 1 ); @@ -1252,7 +1272,7 @@ export default class Answer extends InlineComponent { inputChildren: { dependencyType: "child", childGroups: ["inputs"], - variableNames: ["creditAchievedIfSubmit"], + variableNames: ["creditAchievedIfSubmit", "value"], // include value so inputs always make dependency values change childIndices: stateValues.inputChildIndices, variablesOptional: true, }, @@ -1747,6 +1767,9 @@ export default class Answer extends InlineComponent { } let creditAchieved = await this.stateValues.creditAchievedIfSubmit; + if (await this.stateValues.handGraded) { + creditAchieved = 0; + } let awardsUsed = await this.stateValues.awardsUsedIfSubmit; let inputUsed = await this.stateValues.inputUsedIfSubmit;