From 2a96d8600ec68cc1e35ff560c518f25a0bf91251 Mon Sep 17 00:00:00 2001 From: Duane Nykamp Date: Fri, 11 Oct 2024 17:38:35 -0500 Subject: [PATCH] give warning when an answer's award depends on its submitted response --- package-lock.json | 6 +- packages/doenetml-iframe/package.json | 2 +- packages/doenetml-worker/src/Core.js | 22 ++-- packages/doenetml-worker/src/CoreWorker.js | 12 +- .../doenetml-worker/src/components/Answer.js | 26 +++- .../src/test/tagSpecific/answer.test.ts | 117 ++++++++++++++++++ packages/doenetml/package.json | 2 +- packages/standalone/package.json | 2 +- 8 files changed, 172 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6d72297c9..10176fe6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22819,7 +22819,7 @@ }, "packages/doenetml": { "name": "@doenet/doenetml", - "version": "0.7.0-alpha19", + "version": "0.7.0-alpha20", "license": "AGPL-3.0-or-later", "dependencies": { "@chakra-ui/icons": "^2.0.19", @@ -22861,7 +22861,7 @@ }, "packages/doenetml-iframe": { "name": "@doenet/doenetml-iframe", - "version": "0.7.0-alpha19", + "version": "0.7.0-alpha20", "license": "AGPL-3.0-or-later", "devDependencies": {}, "peerDependencies": { @@ -22939,7 +22939,7 @@ }, "packages/standalone": { "name": "@doenet/standalone", - "version": "0.7.0-alpha19", + "version": "0.7.0-alpha20", "license": "AGPL-3.0-or-later", "devDependencies": {} }, diff --git a/packages/doenetml-iframe/package.json b/packages/doenetml-iframe/package.json index ba01a4216..789d40702 100644 --- a/packages/doenetml-iframe/package.json +++ b/packages/doenetml-iframe/package.json @@ -2,7 +2,7 @@ "name": "@doenet/doenetml-iframe", "type": "module", "description": "A renderer for DoenetML contained in an iframe", - "version": "0.7.0-alpha19", + "version": "0.7.0-alpha20", "license": "AGPL-3.0-or-later", "homepage": "https://github.com/Doenet/DoenetML#readme", "private": true, diff --git a/packages/doenetml-worker/src/Core.js b/packages/doenetml-worker/src/Core.js index f0b44c84a..66ddc01fc 100644 --- a/packages/doenetml-worker/src/Core.js +++ b/packages/doenetml-worker/src/Core.js @@ -196,33 +196,39 @@ export default class Core { this.parameterStack.parameters.prerender = prerender; this.initialized = false; - this.initializedPromiseResolves = []; + this.initializedPromiseResolveRejects = []; this.postInitializedMessages = []; this.resolveInitialized = () => { - this.initializedPromiseResolves.forEach((resolve) => resolve(true)); + this.initializedPromiseResolveRejects.forEach(({ resolve }) => + resolve(true), + ); this.initialized = true; for (let message of this.postInitializedMessages) { postMessage(message); } this.postInitializedMessages = []; }; + this.rejectInitialized = (e) => { + this.initializedPromiseResolveRejects.forEach(({ reject }) => + reject(e), + ); + }; this.getInitializedPromise = () => { if (this.initialized) { return Promise.resolve(true); } else { return new Promise((resolve, reject) => { - this.initializedPromiseResolves.push(resolve); + this.initializedPromiseResolveRejects.push({ + resolve, + reject, + }); }); } }; this.finishCoreConstruction().catch((e) => { // throw e; - postMessage({ - messageType: "inErrorState", - coreId: this.coreId, - args: { errMsg: e.message }, - }); + this.rejectInitialized(e); }); } diff --git a/packages/doenetml-worker/src/CoreWorker.js b/packages/doenetml-worker/src/CoreWorker.js index 51002fcce..1b02bbfc7 100644 --- a/packages/doenetml-worker/src/CoreWorker.js +++ b/packages/doenetml-worker/src/CoreWorker.js @@ -174,13 +174,21 @@ async function createCore(args) { Object.assign(coreArgs, args); core = new Core(coreArgs); - core.getInitializedPromise().then(() => { + + try { + await core.getInitializedPromise(); // console.log('actions to process', queuedRequestActions) for (let action of queuedRequestActions) { core.requestAction(action); } queuedRequestActions = []; - }); + } catch (e) { + postMessage({ + messageType: "inErrorState", + coreId: coreArgs.coreId, + args: { errMsg: e.message }, + }); + } } else { let errMsg = initializeResult.success === false diff --git a/packages/doenetml-worker/src/components/Answer.js b/packages/doenetml-worker/src/components/Answer.js index e6a458301..91b82a4d2 100644 --- a/packages/doenetml-worker/src/components/Answer.js +++ b/packages/doenetml-worker/src/components/Answer.js @@ -1710,22 +1710,46 @@ export default class Answer extends InlineComponent { includeOnlyEssentialValues: true, }, }), - definition({ dependencyValues }) { + definition({ dependencyValues, componentName }) { // Use stringify from json-stringify-deterministic // so that the string will be the same // even if the object was built in a different order // (as can happen when reloading from a database) + let warnings = []; + + let selfDependencies = + dependencyValues.currentCreditAchievedDependencies.find( + (x) => x.componentName === componentName, + ); + + if (selfDependencies) { + // look for a dependency on a submitted response + if ( + Object.keys(selfDependencies.stateValues).find( + (x) => x.substring(0, 17) === "submittedResponse", + ) + ) { + warnings.push({ + message: + "An award for this answer is based on the answer's own submitted response, which will lead to unexpected behavior.", + level: 1, + }); + } + } + let stringified = stringify( dependencyValues.currentCreditAchievedDependencies, { replacer: serializedComponentsReplacer }, ); + return { setValue: { creditAchievedDependencies: Base64.stringify( sha1(stringified), ), }, + sendWarnings: warnings, }; }, markStale: () => ({ answerCreditPotentiallyChanged: true }), diff --git a/packages/doenetml-worker/src/test/tagSpecific/answer.test.ts b/packages/doenetml-worker/src/test/tagSpecific/answer.test.ts index 96fd72580..e94a9ad80 100644 --- a/packages/doenetml-worker/src/test/tagSpecific/answer.test.ts +++ b/packages/doenetml-worker/src/test/tagSpecific/answer.test.ts @@ -1428,6 +1428,123 @@ describe("Answer tag tests", async () => { expect(stateVariables["/answer1"].stateValues.creditAchieved).eq(1); }); + async function check_award_based_on_submitted_response( + core: any, + eventually_correct = true, + ) { + let errorWarnings = core.errorWarnings; + + expect(errorWarnings.errors.length).eq(0); + expect(errorWarnings.warnings.length).eq(1); + + expect(errorWarnings.warnings[0].message).contain( + "An award for this answer is based on the answer's own submitted response", + ); + expect(errorWarnings.warnings[0].level).eq(1); + expect(errorWarnings.warnings[0].doenetMLrange.lineBegin).eq(2); + expect(errorWarnings.warnings[0].doenetMLrange.charBegin).eq(5); + expect(errorWarnings.warnings[0].doenetMLrange.lineEnd).eq(5); + expect(errorWarnings.warnings[0].doenetMLrange.charEnd).eq(13); + + let stateVariables = await returnAllStateVariables(core); + let mathInputName = + stateVariables["/ans"].stateValues.inputChildren[0].componentName; + + // have to submit the correct answer twice before it is marked correct + await updateMathInputValue({ + latex: "5", + componentName: mathInputName, + core, + }); + await core.requestAction({ + componentName: "/ans", + actionName: "submitAnswer", + args: {}, + event: null, + }); + stateVariables = await returnAllStateVariables(core); + + // answer is not correct because the submitted response was initially blank + expect(stateVariables["/ans"].stateValues.creditAchieved).eq(0); + // justSubmitted becomes false at the criteria (based on submitted response) + // change when the answer is submitted + expect(stateVariables["/ans"].stateValues.justSubmitted).eq(false); + + await core.requestAction({ + componentName: "/ans", + actionName: "submitAnswer", + args: {}, + event: null, + }); + stateVariables = await returnAllStateVariables(core); + + // if `eventually correct` is set to `true`, then + // the second time, the answer is marked correct and justSubmitted stays true + // because the submitted response starts off correct and doesn't change + expect(stateVariables["/ans"].stateValues.creditAchieved).eq( + eventually_correct ? 1 : 0, + ); + expect(stateVariables["/ans"].stateValues.justSubmitted).eq(true); + } + + it("warning for award depending on submitted response", async () => { + const doenetMLs = [ + ` + + + $ans + `, + ` + + + $ans.submittedResponse=5 + `, + ` + + + $ans.submittedResponse1=5 + `, + ` + + + $ans.submittedResponses=5 + `, + ]; + + for (let doenetML of doenetMLs) { + let core = await createTestCore({ + doenetML, + }); + await check_award_based_on_submitted_response(core); + } + + let doenetML = ` + + + $ans.submittedResponse2=5 + `; + + let core = await createTestCore({ + doenetML, + }); + await check_award_based_on_submitted_response(core, false); + }); + + it("award depending on current response throws error", async () => { + const doenetMLs = [ + `$ans.currentResponse=5`, + `$ans.currentResponse1=5`, + `$ans.currentResponse2=5`, + `$ans.currentResponses=5`, + ]; + + for (let doenetML of doenetMLs) { + await expect(createTestCore({ doenetML })).rejects.toThrow( + "Circular dependency", + ); + } + }); + it("answer award with math", async () => { const doenetML = ` x+y diff --git a/packages/doenetml/package.json b/packages/doenetml/package.json index 25c61d5d9..6c6044a65 100644 --- a/packages/doenetml/package.json +++ b/packages/doenetml/package.json @@ -2,7 +2,7 @@ "name": "@doenet/doenetml", "type": "module", "description": "Semantic markup for building interactive web activities", - "version": "0.7.0-alpha19", + "version": "0.7.0-alpha20", "license": "AGPL-3.0-or-later", "homepage": "https://github.com/Doenet/DoenetML#readme", "private": true, diff --git a/packages/standalone/package.json b/packages/standalone/package.json index 5a6f95dcd..94a1ff434 100644 --- a/packages/standalone/package.json +++ b/packages/standalone/package.json @@ -2,7 +2,7 @@ "name": "@doenet/standalone", "type": "module", "description": "Standalone renderer for DoenetML suitable for being included in a web page", - "version": "0.7.0-alpha19", + "version": "0.7.0-alpha20", "license": "AGPL-3.0-or-later", "homepage": "https://github.com/Doenet/DoenetML#readme", "private": true,