Skip to content

Commit

Permalink
give warning when an answer's award depends on its submitted response
Browse files Browse the repository at this point in the history
  • Loading branch information
dqnykamp committed Oct 11, 2024
1 parent 7510013 commit 2a96d86
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 17 deletions.
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/doenetml-iframe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
22 changes: 14 additions & 8 deletions packages/doenetml-worker/src/Core.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}

Expand Down
12 changes: 10 additions & 2 deletions packages/doenetml-worker/src/CoreWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 25 additions & 1 deletion packages/doenetml-worker/src/components/Answer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
Expand Down
117 changes: 117 additions & 0 deletions packages/doenetml-worker/src/test/tagSpecific/answer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
`
<answer name="ans">
<mathInput />
<award><when>$ans</when></award>
</answer>`,
`
<answer name="ans">
<mathInput />
<award><when>$ans.submittedResponse=5</when></award>
</answer>`,
`
<answer name="ans">
<mathInput />
<award><when>$ans.submittedResponse1=5</when></award>
</answer>`,
`
<answer name="ans">
<mathInput />
<award><when>$ans.submittedResponses=5</when></award>
</answer>`,
];

for (let doenetML of doenetMLs) {
let core = await createTestCore({
doenetML,
});
await check_award_based_on_submitted_response(core);
}

let doenetML = `
<answer name="ans">
<mathInput />
<award><when>$ans.submittedResponse2=5</when></award>
</answer>`;

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 = [
`<answer name="ans"><mathInput /><award><when>$ans.currentResponse=5</when></award></answer>`,
`<answer name="ans"><mathInput /><award><when>$ans.currentResponse1=5</when></award></answer>`,
`<answer name="ans"><mathInput /><award><when>$ans.currentResponse2=5</when></award></answer>`,
`<answer name="ans"><mathInput /><award><when>$ans.currentResponses=5</when></award></answer>`,
];

for (let doenetML of doenetMLs) {
await expect(createTestCore({ doenetML })).rejects.toThrow(
"Circular dependency",
);
}
});

it("answer award with math", async () => {
const doenetML = `
<answer name="answer1"><award><math>x+y</math></award></answer>
Expand Down
2 changes: 1 addition & 1 deletion packages/doenetml/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/standalone/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 2a96d86

Please sign in to comment.