From 841551a65732a276266690ddaaa51a3810398d03 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Tue, 26 Nov 2024 13:06:24 -0700 Subject: [PATCH 01/10] Migrate PerseusItem.answerArea, removing unused fields (#1895) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `answerArea` sometimes includes `options` and `type` fields. `type` always seems to be set to `multiple` if it's present. I couldn't find evidence in Perseus or webapp that this data is used anywhere, so I am removing it. This is necessary to make parsePerseusItem return a valid PerseusItem while still being able to handle existing data that has the extra fields. Issue: https://khanacademy.atlassian.net/browse/LEMS-2582 ## Test plan: `yarn test` Author: benchristel Reviewers: jeremywiebe, catandthemachines, benchristel, anakaren-rojas, nishasy Required Reviewers: Approved By: jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1895 --- .changeset/rotten-peaches-move.md | 5 + .../general-purpose-parsers/index.ts | 2 + .../general-purpose-parsers/pair.test.ts | 6 +- .../pipe-parsers.test.ts | 41 +++++ .../{compose-parsers.ts => pipe-parsers.ts} | 18 ++- .../pipe-parsers.typetest.ts | 31 ++++ .../general-purpose-parsers/trio.test.ts | 10 +- .../general-purpose-parsers/unknown.ts | 4 + .../perseus-parsers/perseus-item.test.ts | 16 ++ .../perseus-parsers/perseus-item.ts | 28 +++- .../perseus-parsers/perseus-renderer.ts | 21 ++- .../parse-perseus-json-snapshot.test.ts.snap | 143 ++++++++++++++++++ .../data/orderer-option-missing-widgets.json | 103 +++++++++++++ 13 files changed, 408 insertions(+), 20 deletions(-) create mode 100644 .changeset/rotten-peaches-move.md create mode 100644 packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/pipe-parsers.test.ts rename packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/{compose-parsers.ts => pipe-parsers.ts} (58%) create mode 100644 packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/pipe-parsers.typetest.ts create mode 100644 packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/unknown.ts create mode 100644 packages/perseus/src/util/parse-perseus-json/regression-tests/data/orderer-option-missing-widgets.json diff --git a/.changeset/rotten-peaches-move.md b/.changeset/rotten-peaches-move.md new file mode 100644 index 0000000000..695cab47de --- /dev/null +++ b/.changeset/rotten-peaches-move.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Internal: remove unused fields from `answerArea` when parsing `PerseusItem`s. diff --git a/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/index.ts b/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/index.ts index 22b58df322..1b664a0d40 100644 --- a/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/index.ts +++ b/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/index.ts @@ -9,7 +9,9 @@ export * from "./number"; export * from "./object"; export * from "./optional"; export * from "./pair"; +export * from "./pipe-parsers"; export * from "./record"; export * from "./string"; export * from "./trio"; export * from "./union"; +export * from "./unknown"; diff --git a/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/pair.test.ts b/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/pair.test.ts index e9c567cf74..c9fe8b5e52 100644 --- a/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/pair.test.ts +++ b/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/pair.test.ts @@ -1,8 +1,8 @@ import {success} from "../result"; -import {composeParsers} from "./compose-parsers"; import {number} from "./number"; import {pair} from "./pair"; +import {pipeParsers} from "./pipe-parsers"; import {string} from "./string"; import {anyFailure, ctx, parseFailureWith} from "./test-helpers"; @@ -72,9 +72,9 @@ describe("pair", () => { }); it("returns the parsed values from each of its sub-parsers", () => { - const increment = composeParsers(number, (x, ctx) => + const increment = pipeParsers(number).then((x, ctx) => ctx.success(x + 1), - ); + ).parser; const incrementBoth = pair(increment, increment); expect(incrementBoth([1, 5], ctx())).toEqual(success([2, 6])); }); diff --git a/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/pipe-parsers.test.ts b/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/pipe-parsers.test.ts new file mode 100644 index 0000000000..b383ccd4b1 --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/pipe-parsers.test.ts @@ -0,0 +1,41 @@ +import {success} from "../result"; + +import {pipeParsers} from "./pipe-parsers"; +import {string} from "./string"; +import {anyFailure, ctx} from "./test-helpers"; + +import type {PartialParser} from "../parser-types"; + +describe("pipeParsers given a single parser", () => { + const string2 = pipeParsers(string).parser; + it("accepts a valid value", () => { + expect(string2("abc", ctx())).toEqual(success("abc")); + }); + + it("rejects an invalid value", () => { + expect(string2(99, ctx())).toEqual(anyFailure); + }); +}); + +describe("pipeParsers given a chain of parsers", () => { + const stringToNumber: PartialParser = (rawVal, ctx) => { + if (/^\d+$/.test(rawVal)) { + return ctx.success(parseInt(rawVal, 10)); + } + return ctx.failure("a numeric string", rawVal); + }; + + const numericString = pipeParsers(string).then(stringToNumber).parser; + + it("accepts a valid value", () => { + expect(numericString("7", ctx())).toEqual(success(7)); + }); + + it("rejects a value that fails the first parser", () => { + expect(numericString(99, ctx())).toEqual(anyFailure); + }); + + it("rejects a value that fails the second parser", () => { + expect(numericString("abc", ctx())).toEqual(anyFailure); + }); +}); diff --git a/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/compose-parsers.ts b/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/pipe-parsers.ts similarity index 58% rename from packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/compose-parsers.ts rename to packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/pipe-parsers.ts index f50d867f1b..0eb6b9a8da 100644 --- a/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/compose-parsers.ts +++ b/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/pipe-parsers.ts @@ -1,13 +1,25 @@ import {isFailure} from "../result"; import type { - ParsedValue, - PartialParser, ParseContext, + ParsedValue, Parser, + PartialParser, } from "../parser-types"; -export function composeParsers< +export function pipeParsers(p: Parser): ParserPipeline { + return new ParserPipeline(p); +} + +export class ParserPipeline { + constructor(public readonly parser: Parser) {} + + then(nextParser: PartialParser): ParserPipeline { + return new ParserPipeline(composeParsers(this.parser, nextParser)); + } +} + +function composeParsers< A extends Parser, B extends PartialParser, any>, >(parserA: A, parserB: B): Parser> { diff --git a/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/pipe-parsers.typetest.ts b/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/pipe-parsers.typetest.ts new file mode 100644 index 0000000000..55ccce8d38 --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/pipe-parsers.typetest.ts @@ -0,0 +1,31 @@ +// Test: pipeParsers()...then().parser returns the expected type + +import {pipeParsers} from "./pipe-parsers"; +import {string} from "./string"; + +import type {Parser, PartialParser} from "../parser-types"; + +const stringToNumber = summon>(); +const numberToBoolean = summon>(); + +{ + pipeParsers(string).then(stringToNumber).then(numberToBoolean) + .parser satisfies Parser; +} + +{ + // @ts-expect-error - partial parser types don't match + pipeParsers(string).then(stringToNumber).then(stringToNumber).parser; +} + +{ + const p = pipeParsers(string) + .then(stringToNumber) + .then(numberToBoolean).parser; + // @ts-expect-error - return value is not assignable to Parser + p satisfies Parser; +} + +function summon(): T { + return "fake summoned value" as any; +} diff --git a/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/trio.test.ts b/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/trio.test.ts index 0df070566b..a4a87349de 100644 --- a/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/trio.test.ts +++ b/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/trio.test.ts @@ -1,8 +1,8 @@ import {success} from "../result"; import {boolean} from "./boolean"; -import {composeParsers} from "./compose-parsers"; import {number} from "./number"; +import {pipeParsers} from "./pipe-parsers"; import {string} from "./string"; import {anyFailure, ctx, parseFailureWith} from "./test-helpers"; import {trio} from "./trio"; @@ -86,10 +86,10 @@ describe("trio()", () => { }); it("returns the parsed values from each of its sub-parsers", () => { - const increment = composeParsers(number, (x, ctx) => + const increment = pipeParsers(number).then((x, ctx) => ctx.success(x + 1), - ); - const incrementBoth = trio(increment, increment, increment); - expect(incrementBoth([1, 5, 10], ctx())).toEqual(success([2, 6, 11])); + ).parser; + const incrementAll = trio(increment, increment, increment); + expect(incrementAll([1, 5, 10], ctx())).toEqual(success([2, 6, 11])); }); }); diff --git a/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/unknown.ts b/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/unknown.ts new file mode 100644 index 0000000000..01fcd45d56 --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/unknown.ts @@ -0,0 +1,4 @@ +import type {Parser} from "../parser-types"; + +export const unknown: Parser = (rawValue, ctx) => + ctx.success(rawValue); diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-item.test.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-item.test.ts index 51ae3c4507..03fed86b9a 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-item.test.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-item.test.ts @@ -40,4 +40,20 @@ describe("parsePerseusItem", () => { `At (root).answerArea.bork -- expected "calculator", "chi2Table", "financialCalculatorMonthlyPayment", "financialCalculatorTotalAmount", "financialCalculatorTimeToPayOff", "periodicTable", "periodicTableWithKey", "tTable", or "zTable", but got "bork"`, ); }); + + it("removes 'type' and 'options' keys from answerArea", () => { + const item = { + ...baseItem, + answerArea: { + calculator: true, + type: "should get removed", + options: {}, + }, + }; + + const result = parse(item, parsePerseusItem); + + assertSuccess(result); + expect(result.value.answerArea).toEqual({calculator: true}); + }); }); diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-item.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-item.ts index adfd6d3625..008cb133e3 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-item.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-item.ts @@ -6,6 +6,7 @@ import { enumeration, number, object, + pipeParsers, record, } from "../general-purpose-parsers"; @@ -13,12 +14,14 @@ import {parseHint} from "./hint"; import {parsePerseusRenderer} from "./perseus-renderer"; import type {PerseusItem} from "../../../perseus-types"; -import type {Parser} from "../parser-types"; +import type {ParseContext, Parser, ParseResult} from "../parser-types"; export const parsePerseusItem: Parser = object({ question: parsePerseusRenderer, hints: array(parseHint), - answerArea: record(enumeration(...ItemExtras), boolean), + answerArea: pipeParsers(object({})) + .then(migrateAnswerArea) + .then(record(enumeration(...ItemExtras), boolean)).parser, itemDataVersion: object({ major: number, minor: number, @@ -26,3 +29,24 @@ export const parsePerseusItem: Parser = object({ // Deprecated field answer: any, }); + +// Some answerAreas have extra fields, like: +// +// "answerArea": { +// "type": "multiple", +// "options": { +// "content": "", +// "images": {}, +// "widgets": {} +// } +// } +// +// The "type" and "options" fields don't seem to be used anywhere. This +// migration function removes them. +function migrateAnswerArea( + rawValue: {type?: unknown; options?: unknown}, + ctx: ParseContext, +): ParseResult { + const {type: _, options: __, ...rest} = rawValue; + return ctx.success(rest); +} diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-renderer.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-renderer.ts index 573f67cfa7..385827434e 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-renderer.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-renderer.ts @@ -6,6 +6,7 @@ import { record, string, } from "../general-purpose-parsers"; +import {defaulted} from "../general-purpose-parsers/defaulted"; import {parseWidgetsMap} from "./widgets-map"; @@ -18,13 +19,19 @@ export const parsePerseusRenderer: Parser = object({ // `group` widget can contain another renderer. // The anonymous function below ensures that we don't try to access // parseWidgetsMap before it's defined. - widgets: (rawVal, ctx) => parseWidgetsMap(rawVal, ctx), + widgets: defaulted( + (rawVal, ctx) => parseWidgetsMap(rawVal, ctx), + () => ({}), + ), metadata: optional(array(string)), - images: record( - string, - object({ - width: number, - height: number, - }), + images: defaulted( + record( + string, + object({ + width: number, + height: number, + }), + ), + () => ({}), ), }); diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/__snapshots__/parse-perseus-json-snapshot.test.ts.snap b/packages/perseus/src/util/parse-perseus-json/regression-tests/__snapshots__/parse-perseus-json-snapshot.test.ts.snap index c1b40da162..3a58119d93 100644 --- a/packages/perseus/src/util/parse-perseus-json/regression-tests/__snapshots__/parse-perseus-json-snapshot.test.ts.snap +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/__snapshots__/parse-perseus-json-snapshot.test.ts.snap @@ -1,5 +1,148 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`parseAndTypecheckPerseusItem correctly parses data/orderer-option-missing-widgets.json 1`] = ` +{ + "answer": undefined, + "answerArea": { + "calculator": false, + }, + "hints": [ + { + "content": "Extinction is a natural process, and the rate of extinction can be determined by studying changes in biodiversity during the history of life on Earth. +", + "images": {}, + "metadata": undefined, + "replace": undefined, + "widgets": { + "orderer 1": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "correctOptions": [ + { + "content": "$x$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + ], + "height": "normal", + "layout": "horizontal", + "options": [ + { + "content": "$x$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + { + "content": "$y$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + ], + "otherOptions": [ + { + "content": "$y$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + ], + }, + "static": undefined, + "type": "orderer", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, + { + "content": "Humans are causing a current sixth mass extinction event, but are not the only reason species go extinct.", + "images": {}, + "metadata": undefined, + "replace": undefined, + "widgets": {}, + }, + ], + "itemDataVersion": { + "major": 0, + "minor": 1, + }, + "question": { + "content": "What is the definition of the natural background rate of extinction? + +![](https://ka-perseus-images.s3.amazonaws.com/1f68a48ffbc7b137e99fbcbb11e9747ae799fe01.jpg) + +[[☃ radio 1]]", + "images": { + "https://ka-perseus-images.s3.amazonaws.com/1f68a48ffbc7b137e99fbcbb11e9747ae799fe01.jpg": { + "height": 253, + "width": 396, + }, + }, + "metadata": undefined, + "widgets": { + "radio 1": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "choices": [ + { + "clue": undefined, + "content": "a. the rate of extinction that balances the rate of speciation", + "correct": false, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + { + "clue": undefined, + "content": "b. the average rate at which extinctions have occurred naturally in the geologic past, without human involvement", + "correct": true, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + { + "clue": undefined, + "content": "c. the rate of extinction that is currently occurring in protected areas like national parks and marine sanctuaries", + "correct": false, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + { + "clue": undefined, + "content": "d. the rate of extinction that can be determined by using Geiger counters and detecting radiation", + "correct": false, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + ], + "countChoices": undefined, + "deselectEnabled": false, + "displayCount": null, + "hasNoneOfTheAbove": undefined, + "multipleSelect": false, + "noneOfTheAbove": false, + "onePerLine": true, + "randomize": false, + }, + "static": undefined, + "type": "radio", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, +} +`; + exports[`parseAndTypecheckPerseusItem correctly parses data/radio-missing-noneOfTheAbove.json 1`] = ` { "answer": undefined, diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/data/orderer-option-missing-widgets.json b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/orderer-option-missing-widgets.json new file mode 100644 index 0000000000..bba1e462b5 --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/orderer-option-missing-widgets.json @@ -0,0 +1,103 @@ +{ + "answerArea": { + "calculator": false, + "options": { + "content": "", + "images": {}, + "widgets": {} + }, + "type": "multiple" + }, + "hints": [ + { + "content": "Extinction is a natural process, and the rate of extinction can be determined by studying changes in biodiversity during the history of life on Earth.\n", + "images": {}, + "widgets": { + "orderer 1": { + "graded": true, + "options": { + "correctOptions": [ + { + "content": "$x$" + } + ], + "height": "normal", + "layout": "horizontal", + "options": [ + { + "content": "$x$" + }, + { + "content": "$y$" + } + ], + "otherOptions": [ + { + "content": "$y$" + } + ] + }, + "type": "orderer", + "version": { + "major": 0, + "minor": 0 + } + } + } + }, + { + "content": "Humans are causing a current sixth mass extinction event, but are not the only reason species go extinct.", + "images": {}, + "widgets": {} + } + ], + "itemDataVersion": { + "major": 0, + "minor": 1 + }, + "question": { + "content": "What is the definition of the natural background rate of extinction? \n\n![](https://ka-perseus-images.s3.amazonaws.com/1f68a48ffbc7b137e99fbcbb11e9747ae799fe01.jpg)\n\n[[☃ radio 1]]", + "images": { + "https://ka-perseus-images.s3.amazonaws.com/1f68a48ffbc7b137e99fbcbb11e9747ae799fe01.jpg": { + "height": 253, + "width": 396 + } + }, + "widgets": { + "radio 1": { + "graded": true, + "options": { + "choices": [ + { + "content": "a.\tthe rate of extinction that balances the rate of speciation", + "correct": false + }, + { + "content": "b.\tthe average rate at which extinctions have occurred naturally in the geologic past, without human involvement", + "correct": true + }, + { + "content": "c.\tthe rate of extinction that is currently occurring in protected areas like national parks and marine sanctuaries", + "correct": false + }, + { + "content": "d.\tthe rate of extinction that can be determined by using Geiger counters and detecting radiation", + "correct": false + } + ], + "deselectEnabled": false, + "displayCount": null, + "multipleSelect": false, + "noneOfTheAbove": false, + "onePerLine": true, + "randomize": false + }, + "type": "radio", + "version": { + "major": 0, + "minor": 0 + } + } + } + } +} From 3dbca965a2bbaa2d980c1cc4c439469157e0bd33 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Tue, 26 Nov 2024 14:16:18 -0700 Subject: [PATCH 02/10] Add and pass regression tests for PerseusItem parser (#1907) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue: LEMS-2582 ## Test plan: `yarn test` Author: benchristel Reviewers: jeremywiebe, benchristel, anakaren-rojas, catandthemachines, nishasy Required Reviewers: Approved By: jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1907 --- .changeset/chilled-turtles-drive.md | 5 + packages/perseus/src/perseus-types.ts | 19 +- .../general-purpose-parsers/defaulted.ts | 2 +- .../parse-perseus-json.test.ts | 6 +- .../perseus-parsers/categorizer-widget.ts | 3 +- .../perseus-parsers/dropdown-widget.ts | 3 +- .../perseus-parsers/expression-widget.ts | 7 +- .../perseus-parsers/graded-group-widget.ts | 3 +- .../perseus-parsers/hint.ts | 14 +- .../perseus-parsers/iframe-widget.ts | 3 +- .../perseus-parsers/images-map.ts | 17 + .../perseus-parsers/matrix-widget.ts | 12 +- .../perseus-parsers/perseus-item.ts | 16 +- .../perseus-parsers/perseus-renderer.ts | 53 +- .../perseus-parsers/radio-widget.ts | 3 +- .../perseus-parsers/widget.ts | 10 +- .../parse-perseus-json-snapshot.test.ts.snap | 1819 +++++++++++++++++ .../data/categorizer-missing-static.json | 44 + .../data/dropdown-missing-version.json | 45 + .../data/hint-missing-images.json | 189 ++ .../data/iframe-missing-static.json | 49 + .../data/item-missing-answerArea.json | 16 + .../data/matrix-missing-version.json | 62 + .../data/orderer-option-missing-images.json | 600 ++++++ .../data/question-missing-content.json | 7 + .../data/radio-choice-missing-content.json | 166 ++ .../src/widgets/graded-group/graded-group.tsx | 12 + .../perseus/src/widgets/matrix/matrix.tsx | 38 +- 28 files changed, 3148 insertions(+), 75 deletions(-) create mode 100644 .changeset/chilled-turtles-drive.md create mode 100644 packages/perseus/src/util/parse-perseus-json/perseus-parsers/images-map.ts create mode 100644 packages/perseus/src/util/parse-perseus-json/regression-tests/data/categorizer-missing-static.json create mode 100644 packages/perseus/src/util/parse-perseus-json/regression-tests/data/dropdown-missing-version.json create mode 100644 packages/perseus/src/util/parse-perseus-json/regression-tests/data/hint-missing-images.json create mode 100644 packages/perseus/src/util/parse-perseus-json/regression-tests/data/iframe-missing-static.json create mode 100644 packages/perseus/src/util/parse-perseus-json/regression-tests/data/item-missing-answerArea.json create mode 100644 packages/perseus/src/util/parse-perseus-json/regression-tests/data/matrix-missing-version.json create mode 100644 packages/perseus/src/util/parse-perseus-json/regression-tests/data/orderer-option-missing-images.json create mode 100644 packages/perseus/src/util/parse-perseus-json/regression-tests/data/question-missing-content.json create mode 100644 packages/perseus/src/util/parse-perseus-json/regression-tests/data/radio-choice-missing-content.json diff --git a/.changeset/chilled-turtles-drive.md b/.changeset/chilled-turtles-drive.md new file mode 100644 index 0000000000..5eb3519556 --- /dev/null +++ b/.changeset/chilled-turtles-drive.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Internal: add and pass regression tests for `PerseusItem` parser. diff --git a/packages/perseus/src/perseus-types.ts b/packages/perseus/src/perseus-types.ts index 786c00fbda..95dd9993d5 100644 --- a/packages/perseus/src/perseus-types.ts +++ b/packages/perseus/src/perseus-types.ts @@ -93,9 +93,14 @@ export type PerseusItem = { hints: ReadonlyArray; // Details about the tools the user might need to answer the question answerArea: PerseusAnswerArea | null | undefined; - // The version of the item. Not used by Perseus - itemDataVersion: Version; - // Deprecated field + /** + * The version of the item. + * @deprecated Not used. + */ + itemDataVersion: any; + /** + * @deprecated Superseded by per-widget answers. + */ answer: any; }; @@ -1034,17 +1039,17 @@ export type PerseusMatcherWidgetOptions = { export type PerseusMatrixWidgetAnswers = ReadonlyArray>; export type PerseusMatrixWidgetOptions = { // Translatable Text; Shown before the matrix - prefix: string; + prefix?: string | undefined; // Translatable Text; Shown after the matrix - suffix: string; + suffix?: string | undefined; // A data matrix representing the "correct" answers to be entered into the matrix answers: PerseusMatrixWidgetAnswers; // The coordinate location of the cursor position at start. default: [0, 0] - cursorPosition: ReadonlyArray; + cursorPosition?: ReadonlyArray | undefined; // The coordinate size of the matrix. Only supports 2-dimensional matrix. default: [3, 3] matrixBoardSize: ReadonlyArray; // Whether this is meant to statically display the answers (true) or be used as an input field, graded against the answers - static: boolean; + static?: boolean | undefined; }; export type PerseusMeasurerWidgetOptions = { diff --git a/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/defaulted.ts b/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/defaulted.ts index f5802741b7..543282b278 100644 --- a/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/defaulted.ts +++ b/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/defaulted.ts @@ -2,7 +2,7 @@ import {success} from "../result"; import type {Parser} from "../parser-types"; -export function defaulted( +export function defaulted( parser: Parser, fallback: (missingValue: null | undefined) => Default, ): Parser { diff --git a/packages/perseus/src/util/parse-perseus-json/parse-perseus-json.test.ts b/packages/perseus/src/util/parse-perseus-json/parse-perseus-json.test.ts index 4be4dd92c1..a5852537a2 100644 --- a/packages/perseus/src/util/parse-perseus-json/parse-perseus-json.test.ts +++ b/packages/perseus/src/util/parse-perseus-json/parse-perseus-json.test.ts @@ -24,11 +24,13 @@ describe("parseAndTypecheckPerseusItem", () => { }); it("returns an error given an invalid PerseusItem", () => { - const result = parseAndTypecheckPerseusItem(`{ "bad": "value" }`); + const result = parseAndTypecheckPerseusItem( + `{"question": "bad value"}`, + ); assertFailure(result); expect(result.detail).toContain( - "At (root).question -- expected object, but got undefined", + `At (root).question -- expected object, but got "bad value"`, ); }); }); diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/categorizer-widget.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/categorizer-widget.ts index 7abfceb975..cf1e1ec7da 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/categorizer-widget.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/categorizer-widget.ts @@ -7,6 +7,7 @@ import { optional, string, } from "../general-purpose-parsers"; +import {defaulted} from "../general-purpose-parsers/defaulted"; import {parseWidget} from "./widget"; @@ -19,7 +20,7 @@ export const parseCategorizerWidget: Parser = parseWidget( items: array(string), categories: array(string), randomizeItems: boolean, - static: boolean, + static: defaulted(boolean, () => false), values: array(number), highlightLint: optional(boolean), linterContext: optional( diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/dropdown-widget.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/dropdown-widget.ts index d65ba1c4c7..9077556aef 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/dropdown-widget.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/dropdown-widget.ts @@ -5,6 +5,7 @@ import { object, string, } from "../general-purpose-parsers"; +import {defaulted} from "../general-purpose-parsers/defaulted"; import {parseWidget} from "./widget"; @@ -15,7 +16,7 @@ export const parseDropdownWidget: Parser = parseWidget( constant("dropdown"), object({ placeholder: string, - static: boolean, + static: defaulted(boolean, () => false), choices: array( object({ content: string, diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/expression-widget.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/expression-widget.ts index e1f607c562..0622c99aa2 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/expression-widget.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/expression-widget.ts @@ -3,9 +3,12 @@ import { boolean, constant, enumeration, + number, object, optional, + pipeParsers, string, + union, } from "../general-purpose-parsers"; import {parseWidget} from "./widget"; @@ -21,7 +24,9 @@ const parseAnswerForm: Parser = object({ form: boolean, simplify: boolean, considered: enumeration("correct", "wrong", "ungraded"), - key: optional(string), + key: pipeParsers(optional(union(string).or(number).parser)).then( + (key, ctx) => ctx.success(String(key)), + ).parser, }); export const parseExpressionWidget: Parser = parseWidget( diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/graded-group-widget.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/graded-group-widget.ts index 39e98c835f..4c24db22e6 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/graded-group-widget.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/graded-group-widget.ts @@ -8,6 +8,7 @@ import { record, string, } from "../general-purpose-parsers"; +import {defaulted} from "../general-purpose-parsers/defaulted"; import {parsePerseusRenderer} from "./perseus-renderer"; import {parseWidget} from "./widget"; @@ -17,7 +18,7 @@ import type {GradedGroupWidget} from "../../../perseus-types"; import type {Parser} from "../parser-types"; export const parseGradedGroupWidgetOptions = object({ - title: string, + title: defaulted(string, () => ""), hasHint: optional(nullable(boolean)), // This module has an import cycle with parsePerseusRenderer. // The anonymous function below ensures that we don't try to access diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/hint.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/hint.ts index e362ff355f..f415c80533 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/hint.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/hint.ts @@ -1,13 +1,13 @@ import { array, boolean, - number, object, optional, - record, string, } from "../general-purpose-parsers"; +import {defaulted} from "../general-purpose-parsers/defaulted"; +import {parseImages} from "./images-map"; import {parseWidgetsMap} from "./widgets-map"; import type {Hint} from "../../../perseus-types"; @@ -16,13 +16,7 @@ import type {Parser} from "../parser-types"; export const parseHint: Parser = object({ replace: optional(boolean), content: string, - widgets: parseWidgetsMap, + widgets: defaulted(parseWidgetsMap, () => ({})), metadata: optional(array(string)), - images: record( - string, - object({ - width: number, - height: number, - }), - ), + images: parseImages, }); diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/iframe-widget.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/iframe-widget.ts index 490484c115..59ce0cc371 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/iframe-widget.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/iframe-widget.ts @@ -8,6 +8,7 @@ import { string, union, } from "../general-purpose-parsers"; +import {defaulted} from "../general-purpose-parsers/defaulted"; import {parseWidget} from "./widget"; @@ -23,6 +24,6 @@ export const parseIframeWidget: Parser = parseWidget( height: union(number).or(string).parser, allowFullScreen: boolean, allowTopNavigation: optional(boolean), - static: boolean, + static: defaulted(boolean, () => false), }), ); diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/images-map.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/images-map.ts new file mode 100644 index 0000000000..616d82d8d9 --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/images-map.ts @@ -0,0 +1,17 @@ +import {number, object, record, string} from "../general-purpose-parsers"; +import {defaulted} from "../general-purpose-parsers/defaulted"; + +import type {PerseusImageDetail} from "../../../perseus-types"; +import type {Parser} from "../parser-types"; + +export const parseImages: Parser<{[key: string]: PerseusImageDetail}> = + defaulted( + record( + string, + object({ + width: number, + height: number, + }), + ), + () => ({}), + ); diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/matrix-widget.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/matrix-widget.ts index 6cd932b520..71f8455784 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/matrix-widget.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/matrix-widget.ts @@ -4,8 +4,10 @@ import { constant, number, object, + optional, string, } from "../general-purpose-parsers"; +import {defaulted} from "../general-purpose-parsers/defaulted"; import {parseWidget} from "./widget"; @@ -13,13 +15,13 @@ import type {MatrixWidget} from "../../../perseus-types"; import type {Parser} from "../parser-types"; export const parseMatrixWidget: Parser = parseWidget( - constant("matrix"), + defaulted(constant("matrix"), () => "matrix"), object({ - prefix: string, - suffix: string, + prefix: optional(string), + suffix: optional(string), answers: array(array(number)), - cursorPosition: array(number), + cursorPosition: optional(array(number)), matrixBoardSize: array(number), - static: boolean, + static: optional(boolean), }), ); diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-item.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-item.ts index 008cb133e3..3364e110c5 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-item.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-item.ts @@ -6,9 +6,11 @@ import { enumeration, number, object, + optional, pipeParsers, record, } from "../general-purpose-parsers"; +import {defaulted} from "../general-purpose-parsers/defaulted"; import {parseHint} from "./hint"; import {parsePerseusRenderer} from "./perseus-renderer"; @@ -18,14 +20,16 @@ import type {ParseContext, Parser, ParseResult} from "../parser-types"; export const parsePerseusItem: Parser = object({ question: parsePerseusRenderer, - hints: array(parseHint), - answerArea: pipeParsers(object({})) + hints: defaulted(array(parseHint), () => []), + answerArea: pipeParsers(defaulted(object({}), () => ({}))) .then(migrateAnswerArea) .then(record(enumeration(...ItemExtras), boolean)).parser, - itemDataVersion: object({ - major: number, - minor: number, - }), + itemDataVersion: optional( + object({ + major: number, + minor: number, + }), + ), // Deprecated field answer: any, }); diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-renderer.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-renderer.ts index 385827434e..c354541935 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-renderer.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-renderer.ts @@ -1,37 +1,32 @@ -import { - array, - number, - object, - optional, - record, - string, -} from "../general-purpose-parsers"; +import {array, object, optional, string} from "../general-purpose-parsers"; import {defaulted} from "../general-purpose-parsers/defaulted"; +import {parseImages} from "./images-map"; import {parseWidgetsMap} from "./widgets-map"; import type {PerseusRenderer} from "../../../perseus-types"; import type {Parser} from "../parser-types"; -export const parsePerseusRenderer: Parser = object({ - content: string, - // This module has an import cycle with parseWidgetsMap, because the - // `group` widget can contain another renderer. - // The anonymous function below ensures that we don't try to access - // parseWidgetsMap before it's defined. - widgets: defaulted( - (rawVal, ctx) => parseWidgetsMap(rawVal, ctx), - () => ({}), - ), - metadata: optional(array(string)), - images: defaulted( - record( - string, - object({ - width: number, - height: number, - }), +export const parsePerseusRenderer: Parser = defaulted( + object({ + // TODO(benchristel): content is also defaulted to empty string in + // renderer.tsx. See if we can remove one default or the other. + content: defaulted(string, () => ""), + // This module has an import cycle with parseWidgetsMap, because the + // `group` widget can contain another renderer. + // The anonymous function below ensures that we don't try to access + // parseWidgetsMap before it's defined. + widgets: defaulted( + (rawVal, ctx) => parseWidgetsMap(rawVal, ctx), + () => ({}), ), - () => ({}), - ), -}); + metadata: optional(array(string)), + images: parseImages, + }), + // Default value + () => ({ + content: "", + widgets: {}, + images: {}, + }), +); diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/radio-widget.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/radio-widget.ts index 4678fdaf8b..0ee53e81be 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/radio-widget.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/radio-widget.ts @@ -7,6 +7,7 @@ import { optional, string, } from "../general-purpose-parsers"; +import {defaulted} from "../general-purpose-parsers/defaulted"; import {parseWidget} from "./widget"; import {parseWidgetsMap} from "./widgets-map"; @@ -19,7 +20,7 @@ export const parseRadioWidget: Parser = parseWidget( object({ choices: array( object({ - content: string, + content: defaulted(string, () => ""), clue: optional(string), correct: optional(boolean), isNoneOfTheAbove: optional(boolean), diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widget.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widget.ts index fd129d5bdc..8fb94450fa 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widget.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widget.ts @@ -20,9 +20,11 @@ export function parseWidget( alignment: optional(string), options: parseOptions, key: optional(number), - version: object({ - major: number, - minor: number, - }), + version: optional( + object({ + major: number, + minor: number, + }), + ), }); } diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/__snapshots__/parse-perseus-json-snapshot.test.ts.snap b/packages/perseus/src/util/parse-perseus-json/regression-tests/__snapshots__/parse-perseus-json-snapshot.test.ts.snap index 3a58119d93..b47b140bdf 100644 --- a/packages/perseus/src/util/parse-perseus-json/regression-tests/__snapshots__/parse-perseus-json-snapshot.test.ts.snap +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/__snapshots__/parse-perseus-json-snapshot.test.ts.snap @@ -1,5 +1,1496 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`parseAndTypecheckPerseusItem correctly parses data/categorizer-missing-static.json 1`] = ` +{ + "answer": undefined, + "answerArea": { + "calculator": false, + }, + "hints": [], + "itemDataVersion": { + "major": 0, + "minor": 1, + }, + "question": { + "content": " + +[[☃ categorizer 1]]", + "images": {}, + "metadata": undefined, + "widgets": { + "categorizer 1": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "categories": [ + "one", + "two", + "three", + ], + "highlightLint": undefined, + "items": [ + "this", + "that", + "other", + ], + "linterContext": undefined, + "randomizeItems": true, + "static": false, + "values": [], + }, + "static": undefined, + "type": "categorizer", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, +} +`; + +exports[`parseAndTypecheckPerseusItem correctly parses data/dropdown-missing-version.json 1`] = ` +{ + "answer": undefined, + "answerArea": { + "calculator": false, + }, + "hints": [ + { + "content": "While the Articles of Confederation did not provide for a federal executive, delegates to the Continental Congress did elect a president.", + "images": {}, + "metadata": undefined, + "replace": undefined, + "widgets": {}, + }, + ], + "itemDataVersion": undefined, + "question": { + "content": "**Under the Articles of Confederation[[☃ dropdown 1]] chose the president of the United States.** ", + "images": {}, + "metadata": undefined, + "widgets": { + "dropdown 1": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "choices": [ + { + "content": "state legislatures", + "correct": false, + }, + { + "content": "delegates to the Continental Congress", + "correct": true, + }, + { + "content": "the people", + "correct": false, + }, + ], + "placeholder": "", + "static": false, + }, + "static": undefined, + "type": "dropdown", + "version": undefined, + }, + }, + }, +} +`; + +exports[`parseAndTypecheckPerseusItem correctly parses data/hint-missing-images.json 1`] = ` +{ + "answer": undefined, + "answerArea": { + "calculator": false, + "chi2Table": false, + "periodicTable": false, + "tTable": false, + "zTable": false, + }, + "hints": [ + { + "content": "##Scaling equations to solve systems of equations with elimination + +###What is it, and why is it useful? + +The elimination strategy is a popular method for mathematicians who like working with integers. When the coefficients of a variable are already opposites, we can just add the equations to get a new equation without that variable. Otherwise, we may want to multiply both sides of one equation (or both) by a constant so that we do get opposite coefficients. + +###Practice + +If you haven't practiced [Systems of equations with elimination](/e/systems_of_equations_with_elimination_0.5?getready=pre) yet, we suggest you do that first. + + + +[[☃ graded-group-set 2]] + +For more practice, go to [Systems of equations with elimination challenge](/e/systems_of_equations_with_elimination?getready=post).", + "images": {}, + "metadata": undefined, + "replace": false, + "widgets": { + "graded-group-set 2": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "gradedGroups": [ + { + "content": "**Solve the system of equations.** + +$\\begin{align} +&-5x-3y +9 =0 +\\\\\\\\ +&4x-18y-14=0 +\\end{align}$ + +$x=$ [[☃ numeric-input 1]] + +$y=$ [[☃ numeric-input 2]]", + "hasHint": undefined, + "hint": { + "content": "Let's solve the system by using the *elimination method.* In order to eliminate one of the variables, we need to manipulate the equations so that variable has coefficients of the same size but opposite signs in each equation. + +Let's take a look at the system: + +$\\begin{align} +&-5x-3y + 9=0 +\\\\\\\\ +&4x-18y-14=0 +\\end{align}$ + +The coefficient of $y$ in the second equation, $-18$, is exactly $6$ times the coefficient of $y$ in the first equation, $-3$. Therefore, we can multiply the first equation by $\\purpleD{-6}$ in order to eliminate $y$. + +$\\begin{align} \\purpleD{-6}\\cdot(-5x)-(\\purpleD{-6})\\cdot3y+(\\purpleD{-6})\\cdot 9&=(\\purpleD{-6})\\cdot0\\\\\\\\ +30x+18y-54&=0\\end{align}$ + +Now we can eliminate $y$ as follows: + +$\\begin{align} {30x}+\\maroonD{18y} -54&=0 \\\\\\\\ +\\underline{{}\\tealE{+} +\\left({4x}-\\maroonD{18y}-14\\right)}&=\\underline{{}\\tealE{+}0}\\\\ +34x-0 -68&=0\\end{align}$ + +When we solve the resulting equation we obtain that $x = 2$. Then, we can substitute this into one of the original equations and solve for $y$ to obtain $y=-\\dfrac{1}{3}$. + +This is the solution of the system: + +$\\begin{align} +&x=2 +\\\\\\\\ +&y=-\\dfrac{1}{3} +\\end{align}$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + "images": {}, + "immutableWidgets": false, + "title": "Problem 1.1", + "widgetEnabled": true, + "widgets": { + "numeric-input 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "answerForms": undefined, + "answers": [ + { + "answerForms": undefined, + "maxError": null, + "message": "", + "simplify": "required", + "status": "correct", + "strict": false, + "value": 2, + }, + ], + "coefficient": false, + "labelText": "value of x", + "multipleNumberInput": false, + "rightAlign": false, + "size": "small", + "static": false, + }, + "static": false, + "type": "numeric-input", + "version": { + "major": 0, + "minor": 0, + }, + }, + "numeric-input 2": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "answerForms": undefined, + "answers": [ + { + "answerForms": [ + "proper", + "improper", + ], + "maxError": null, + "message": "", + "simplify": "required", + "status": "correct", + "strict": false, + "value": -0.3333333333333333, + }, + ], + "coefficient": false, + "labelText": "value of y", + "multipleNumberInput": false, + "rightAlign": false, + "size": "small", + "static": false, + }, + "static": false, + "type": "numeric-input", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, + { + "content": "**Solve the system of equations.** + +$\\begin{align} +&9x-5y = -55 +\\\\\\\\ +&-7x+8y=51 +\\end{align}$ + +$x=$ [[☃ numeric-input 1]] + +$y=$ [[☃ numeric-input 2]]", + "hasHint": undefined, + "hint": { + "content": "Let's solve the system by using the *elimination method.* In order to eliminate one of the variables, we need to manipulate the equations so that variable has coefficients of the same size but opposite signs in each equation. + +Let's take a look at the system: + +$\\begin{align} +&9x-5y = -55 +\\\\\\\\ +&-7x+8y=51 +\\end{align}$ + +Since the coefficients of either variable aren't multiples of each other, we must multiply both equations in order to eliminate a variable of our choice. Let's eliminate $y$. + +Since the *least common multiple* of the coefficients of $y$ in the two equations is $-40$, we can *multiply* the first equation by $\\purpleD{8}$ and the second equation by $\\tealE{5}$ in order to eliminate $y$. + +$ \\begin{align}\\purpleD{8}\\cdot9x-\\purpleD{8}\\cdot5y&=\\purpleD{8}\\cdot(-55)\\\\\\\\ +72x-40y&=-440\\end{align}$ + +$ \\begin{align} \\tealE{5}\\cdot(-7x)+\\tealE{5}\\cdot8y&=\\tealE{5}\\cdot 51\\\\\\\\ +-35x+40y&=255\\end{align}$ + +Now we can eliminate $y$ as follows: + +$\\begin{align} {72x}-\\maroonD{40y} &= -440 \\\\\\\\ +\\underline{{}\\blueE{+} +{-35x}+\\maroonD{40y}}&=\\underline{{}\\blueE{+}255}\\\\\\\\ +37x-0 &=-185\\end{align}$ + +When we solve the resulting equation we obtain that $x = -5$. Then, we can substitute this into one of the original equations and solve for $y$ to obtain $y=2$.", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + "images": {}, + "immutableWidgets": false, + "title": "Problem 1.2", + "widgetEnabled": true, + "widgets": { + "numeric-input 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "answerForms": undefined, + "answers": [ + { + "answerForms": undefined, + "maxError": null, + "message": "", + "simplify": "required", + "status": "correct", + "strict": false, + "value": -5, + }, + ], + "coefficient": false, + "labelText": "value of x", + "multipleNumberInput": false, + "rightAlign": false, + "size": "small", + "static": false, + }, + "static": false, + "type": "numeric-input", + "version": { + "major": 0, + "minor": 0, + }, + }, + "numeric-input 2": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "answerForms": undefined, + "answers": [ + { + "answerForms": undefined, + "maxError": null, + "message": "", + "simplify": "required", + "status": "correct", + "strict": false, + "value": 2, + }, + ], + "coefficient": false, + "labelText": "value of y", + "multipleNumberInput": false, + "rightAlign": false, + "size": "small", + "static": false, + }, + "static": false, + "type": "numeric-input", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, + ], + }, + "static": false, + "type": "graded-group-set", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, + { + "content": "##Why wasn't this exercise part of the strategic supplement course? + +It is possible to solve any system of linear equations using the substitution method as long as you're comfortable working with fractions in your equations.", + "images": {}, + "metadata": undefined, + "replace": false, + "widgets": {}, + }, + ], + "itemDataVersion": { + "major": 0, + "minor": 1, + }, + "question": { + "content": "Congratulations on all you've learned about systems of equations! + +Our strategic supplement version of algebra 1 is for students who are getting classroom instruction in algebra and only using Khan Academy for a short time each week. We only included select exercises, videos, and articles to help you focus your limited time. + +In case you would like a fuller experience, here is a taste of a skill you can learn in our full algebra 1 course.", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, +} +`; + +exports[`parseAndTypecheckPerseusItem correctly parses data/iframe-missing-static.json 1`] = ` +{ + "answer": undefined, + "answerArea": { + "calculator": false, + "periodicTable": false, + }, + "hints": [ + { + "content": "This is a hint... with a video! + +[[☃ iframe 1]]", + "images": {}, + "metadata": undefined, + "replace": undefined, + "widgets": { + "iframe 1": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "allowFullScreen": true, + "allowTopNavigation": undefined, + "height": "235", + "settings": [ + { + "name": "", + "value": "", + }, + ], + "static": false, + "url": "https://www.youtube.com/embed/qYD5iwhLzm8?enablejsapi=1&wmode=transparent&modestbranding=1&rel=0&frameborder='0'", + "width": "425", + }, + "static": undefined, + "type": "iframe", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, + ], + "itemDataVersion": { + "major": 0, + "minor": 1, + }, + "question": { + "content": "This is a question.", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, +} +`; + +exports[`parseAndTypecheckPerseusItem correctly parses data/item-missing-answerArea.json 1`] = ` +{ + "_multi": { + "blurb": { + "__type": "content", + "content": "", + "images": {}, + "widgets": {}, + }, + "hints": [], + "question": { + "__type": "content", + "content": "", + "widgets": {}, + }, + }, + "answer": undefined, + "answerArea": {}, + "hints": [], + "itemDataVersion": undefined, + "question": { + "content": "", + "images": {}, + "widgets": {}, + }, +} +`; + +exports[`parseAndTypecheckPerseusItem correctly parses data/matrix-missing-version.json 1`] = ` +{ + "answer": undefined, + "answerArea": { + "calculator": false, + }, + "hints": [ + { + "content": "The inverse of a matrix is equal to the adjugate of the matrix divided by the determinant of the matrix. + + $ \\textbf A^{-1} = \\frac{1}{det(\\textbf A)}adj(\\textbf A) $", + "images": {}, + "metadata": undefined, + "replace": undefined, + "widgets": {}, + }, + { + "content": "**Step 1: Find the adjugate** + + First, compute the matrix of minors of $\\textbf A$. + + $ \\left[\\begin{array}{rrr}\\left|\\begin{array}{rr}1 & 1 \\\\ 1 & 1\\end{array}\\right| & \\left|\\begin{array}{rr}0 & 1 \\\\ 1 & 1\\end{array}\\right| & \\left|\\begin{array}{rr}0 & 1 \\\\ 1 & 1\\end{array}\\right| \\\\ \\left|\\begin{array}{rr}0 & 1 \\\\ 1 & 1\\end{array}\\right| & \\left|\\begin{array}{rr}2 & 1 \\\\ 1 & 1\\end{array}\\right| & \\left|\\begin{array}{rr}2 & 0 \\\\ 1 & 1\\end{array}\\right| \\\\ \\left|\\begin{array}{rr}0 & 1 \\\\ 1 & 1\\end{array}\\right| & \\left|\\begin{array}{rr}2 & 1 \\\\ 0 & 1\\end{array}\\right| & \\left|\\begin{array}{rr}2 & 0 \\\\ 0 & 1\\end{array}\\right|\\end{array}\\right] = \\left[\\begin{array}{rrr}0 & -1 & -1 \\\\ -1 & 1 & 2 \\\\ -1 & 2 & 2\\end{array}\\right] $", + "images": {}, + "metadata": undefined, + "replace": undefined, + "widgets": {}, + }, + { + "content": "Next, multiply the elements of the matrix of minors by the following pattern: + + $\\left[\\begin{array}{rrr}+ & - & + \\\\ - & + & - \\\\ + & - & +\\end{array}\\right]$ This gives us what is called the matrix of cofactors: + + $ \\left[\\begin{array}{rrr}0 & 1 & -1 \\\\ 1 & 1 & -2 \\\\ -1 & -2 & 2\\end{array}\\right] $", + "images": {}, + "metadata": undefined, + "replace": undefined, + "widgets": {}, + }, + { + "content": "Next, transpose the matrix of cofactors to get the adjugate. + + $ adj(\\textbf A) = \\left[\\begin{array}{rrr}0 & 1 & -1 \\\\ 1 & 1 & -2 \\\\ -1 & -2 & 2\\end{array}\\right]^T = \\left[\\begin{array}{rrr}\\color{\\#6495ED}{0} & \\color{\\#6495ED}{1} & \\color{\\#6495ED}{-1} \\\\ \\color{\\#6495ED}{1} & \\color{\\#6495ED}{1} & \\color{\\#6495ED}{-2} \\\\ \\color{\\#6495ED}{-1} & \\color{\\#6495ED}{-2} & \\color{\\#6495ED}{2}\\end{array}\\right] $", + "images": {}, + "metadata": undefined, + "replace": undefined, + "widgets": {}, + }, + { + "content": "**Step 2: Find the determinant** + + Compute the determinant of the original matrix. + + $ det(\\textbf A) = \\left|\\begin{array}{rrr}2 & 0 & 1 \\\\ 0 & 1 & 1 \\\\ 1 & 1 & 1\\end{array}\\right|= \\color{\\#DF0030}{-1} $", + "images": {}, + "metadata": undefined, + "replace": undefined, + "widgets": {}, + }, + { + "content": "**Step 3: Put it all together** + + Now that we have both the determinant and the adjugate, we can compute the inverse. + + $ \\textbf A^{-1} = \\frac{1}{\\color{\\#DF0030}{-1}} \\left[\\begin{array}{rrr}\\color{\\#6495ED}{0} & \\color{\\#6495ED}{1} & \\color{\\#6495ED}{-1} \\\\ \\color{\\#6495ED}{1} & \\color{\\#6495ED}{1} & \\color{\\#6495ED}{-2} \\\\ \\color{\\#6495ED}{-1} & \\color{\\#6495ED}{-2} & \\color{\\#6495ED}{2}\\end{array}\\right] $", + "images": {}, + "metadata": undefined, + "replace": undefined, + "widgets": {}, + }, + { + "content": "$ = \\left[\\begin{array}{rrr}0 & -1 & 1 \\\\ -1 & -1 & 2 \\\\ 1 & 2 & -2\\end{array}\\right]$", + "images": {}, + "metadata": undefined, + "replace": undefined, + "widgets": {}, + }, + ], + "itemDataVersion": undefined, + "question": { + "content": "$\\textbf A = \\left[\\begin{array}{rrr}2 & 0 & 1 \\\\ 0 & 1 & 1 \\\\ 1 & 1 & 1\\end{array}\\right]$ + + ** What is $\\textbf A^{-1}$? ** + +[[☃ matrix 1]]", + "images": {}, + "metadata": undefined, + "widgets": { + "matrix 1": { + "alignment": undefined, + "graded": undefined, + "key": undefined, + "options": { + "answers": [ + [ + 0, + -1, + 1, + ], + [ + -1, + -1, + 2, + ], + [ + 1, + 2, + -2, + ], + ], + "cursorPosition": undefined, + "matrixBoardSize": [ + 3, + 3, + ], + "prefix": undefined, + "static": undefined, + "suffix": undefined, + }, + "static": undefined, + "type": "matrix", + "version": undefined, + }, + }, + }, +} +`; + +exports[`parseAndTypecheckPerseusItem correctly parses data/orderer-option-missing-images.json 1`] = ` +{ + "answer": undefined, + "answerArea": { + "calculator": false, + "chi2Table": false, + "periodicTable": false, + "tTable": false, + "zTable": false, + }, + "hints": [ + { + "content": "##Decomposing fractions into unit fractions + +To **decompose** a number, we break it into smaller parts. +[[☃ explanation 1]] + +A **unit fraction** is a fraction with a numerator of $1$. [[☃ explanation 2]]", + "images": {}, + "metadata": undefined, + "replace": false, + "widgets": { + "explanation 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "explanation": "We can decompose $54$ into $50+4$.", + "hidePrompt": "Okay, got it", + "showPrompt": "Show me an example", + "static": false, + "widgets": {}, + }, + "static": false, + "type": "explanation", + "version": { + "major": 0, + "minor": 0, + }, + }, + "explanation 2": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "explanation": "$\\dfrac15$ is a unit fraction because the numerator (top number) is $1$.", + "hidePrompt": "Okay, got it", + "showPrompt": "Show me an example", + "static": false, + "widgets": {}, + }, + "static": false, + "type": "explanation", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, + { + "content": "###Example 1: Tape diagram + +Let's decompose $\\dfrac59$ into unit fractions. + +![](web+graphie://ka-perseus-graphie.s3.amazonaws.com/a65a2cd2728b1fa6a12775cdfd353559f99d6582) + + + + +[[☃ graded-group 1]] + +[[☃ explanation 1]]", + "images": { + "web+graphie://ka-perseus-graphie.s3.amazonaws.com/a65a2cd2728b1fa6a12775cdfd353559f99d6582": { + "height": 30, + "width": 400, + }, + }, + "metadata": undefined, + "replace": false, + "widgets": { + "explanation 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "explanation": " + +![](web+graphie://ka-perseus-graphie.s3.amazonaws.com/bcf2a7df34a1d6994e7ed3c7120cc98f1e5c0aa8) + +$\\dfrac59$ can be broken up into $\\dfrac19+\\dfrac19+\\dfrac19+\\dfrac19+\\dfrac19$.", + "hidePrompt": "Hide solution", + "showPrompt": "Show solution", + "static": false, + "widgets": {}, + }, + "static": false, + "type": "explanation", + "version": { + "major": 0, + "minor": 0, + }, + }, + "graded-group 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "content": "**Drag the cards to create an expression that is equivalent to $\\dfrac59$.** + +[[☃ orderer 1]]", + "hasHint": undefined, + "hint": undefined, + "images": {}, + "immutableWidgets": undefined, + "title": "", + "widgetEnabled": undefined, + "widgets": { + "orderer 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "correctOptions": [ + { + "content": "$\\dfrac19$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + { + "content": "$+$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + { + "content": "$\\dfrac19$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + { + "content": "$+$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + { + "content": "$\\dfrac19$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + { + "content": "$+$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + { + "content": "$\\dfrac19$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + { + "content": "$+$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + { + "content": "$\\dfrac19$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + ], + "height": "normal", + "layout": "horizontal", + "options": [ + { + "content": "$\\dfrac19$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + { + "content": "$+$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + ], + "otherOptions": [], + }, + "static": false, + "type": "orderer", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, + "static": false, + "type": "graded-group", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, + { + "content": "###Example 2: Fraction model + +How can we decompose $\\dfrac38$? + +![](web+graphie://ka-perseus-graphie.s3.amazonaws.com/36325871333a2a9bdd2918c6b87d9104758c95e3) + +[[☃ graded-group 1]] + +[[☃ explanation 1]]", + "images": { + "web+graphie://ka-perseus-graphie.s3.amazonaws.com/36325871333a2a9bdd2918c6b87d9104758c95e3": { + "height": 90, + "width": 96, + }, + "web+graphie://ka-perseus-graphie.s3.amazonaws.com/7165c486a581fb615a85de15378d40b81b266a20": { + "height": 96, + "width": 96, + }, + }, + "metadata": undefined, + "replace": false, + "widgets": { + "explanation 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "explanation": "$\\dfrac18+\\dfrac18+\\dfrac18$ + +![](web+graphie://ka-perseus-graphie.s3.amazonaws.com/7165c486a581fb615a85de15378d40b81b266a20)", + "hidePrompt": "Hide solution", + "showPrompt": "Show solution", + "static": false, + "widgets": {}, + }, + "static": false, + "type": "explanation", + "version": { + "major": 0, + "minor": 0, + }, + }, + "graded-group 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "content": "**Write an expression decomposing $\\dfrac38$ into unit fractions.** + + + +[[☃ expression 1]] + +", + "hasHint": undefined, + "hint": undefined, + "images": {}, + "immutableWidgets": undefined, + "title": "", + "widgetEnabled": undefined, + "widgets": { + "expression 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "answerForms": [ + { + "considered": "correct", + "form": true, + "key": "0", + "simplify": false, + "value": "\\frac{1}{8}+\\frac{1}{8}+\\frac{1}{8}", + }, + ], + "ariaLabel": undefined, + "buttonSets": [ + "basic", + ], + "buttonsVisible": undefined, + "functions": [ + "f", + "g", + "h", + ], + "times": true, + "visibleLabel": undefined, + }, + "static": false, + "type": "expression", + "version": { + "major": 1, + "minor": 0, + }, + }, + }, + }, + "static": false, + "type": "graded-group", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, + { + "content": "###Example 3: Number line + +How can we decompose $\\dfrac64$? + +![](web+graphie://ka-perseus-graphie.s3.amazonaws.com/7ebb22693e18c660e133351a0f6e4acd6678e871) + +[[☃ graded-group 1]] + +[[☃ explanation 1]]", + "images": { + "web+graphie://ka-perseus-graphie.s3.amazonaws.com/7ebb22693e18c660e133351a0f6e4acd6678e871": { + "height": 116, + "width": 410, + }, + }, + "metadata": undefined, + "replace": false, + "widgets": { + "explanation 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "explanation": "$\\dfrac14+\\dfrac14+\\dfrac14+\\dfrac14+\\dfrac14+\\dfrac14$", + "hidePrompt": "Hide solution", + "showPrompt": "Show solution", + "static": false, + "widgets": {}, + }, + "static": false, + "type": "explanation", + "version": { + "major": 0, + "minor": 0, + }, + }, + "graded-group 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "content": "**Drag the cards to create an expression that is equivalent to $\\dfrac64$.** + +[[☃ orderer 1]] + +", + "hasHint": undefined, + "hint": undefined, + "images": {}, + "immutableWidgets": undefined, + "title": "", + "widgetEnabled": undefined, + "widgets": { + "orderer 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "correctOptions": [ + { + "content": "$\\dfrac14$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + { + "content": "$+$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + { + "content": "$\\dfrac14$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + { + "content": "$+$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + { + "content": "$\\dfrac14$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + { + "content": "$+$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + { + "content": "$\\dfrac14$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + { + "content": "$+$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + { + "content": "$\\dfrac14$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + { + "content": "$+$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + { + "content": "$\\dfrac14$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + ], + "height": "normal", + "layout": "horizontal", + "options": [ + { + "content": "$\\dfrac14$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + { + "content": "$+$", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, + ], + "otherOptions": [], + }, + "static": false, + "type": "orderer", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, + "static": false, + "type": "graded-group", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, + { + "content": "##Let's try a few more. + +###Problem 1 + +[[☃ graded-group 1]] + +[[☃ explanation 1]] + + + +###Problem 2 + +[[☃ graded-group 2]] + +[[☃ explanation 2]] + + +###Problem 3 + +[[☃ graded-group 3]] + +[[☃ explanation 3]] + + +###Problem 4 + +[[☃ graded-group 4]] + +[[☃ explanation 4]]", + "images": {}, + "metadata": undefined, + "replace": false, + "widgets": { + "explanation 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "explanation": "$\\dfrac29=\\dfrac19+\\dfrac19$", + "hidePrompt": "Hide solution", + "showPrompt": "Show solution", + "static": false, + "widgets": {}, + }, + "static": false, + "type": "explanation", + "version": { + "major": 0, + "minor": 0, + }, + }, + "explanation 2": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "explanation": "$\\dfrac13+\\dfrac13+\\dfrac13+\\dfrac13+\\dfrac13$", + "hidePrompt": "Hide solution", + "showPrompt": "Show solution", + "static": false, + "widgets": {}, + }, + "static": false, + "type": "explanation", + "version": { + "major": 0, + "minor": 0, + }, + }, + "explanation 3": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "explanation": "$\\dfrac16+\\dfrac16+\\dfrac16+\\dfrac16+\\dfrac16=\\dfrac56$ +", + "hidePrompt": "Hide solution", + "showPrompt": "Show solution", + "static": false, + "widgets": {}, + }, + "static": false, + "type": "explanation", + "version": { + "major": 0, + "minor": 0, + }, + }, + "explanation 4": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "explanation": " + +Fraction | Solution +- | - | - +$\\dfrac42$ | $\\dfrac12+\\dfrac12+\\dfrac12+\\dfrac12$ +$\\dfrac34$ | $\\dfrac14+\\dfrac14+\\dfrac14$ +$\\dfrac23$ | $\\dfrac13+\\dfrac13$", + "hidePrompt": "Hide solution", + "showPrompt": "Show solution", + "static": false, + "widgets": {}, + }, + "static": false, + "type": "explanation", + "version": { + "major": 0, + "minor": 0, + }, + }, + "graded-group 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "content": "**Which shows $\\dfrac29$ decomposed into unit fractions?** + +[[☃ radio 1]] + +", + "hasHint": undefined, + "hint": undefined, + "images": {}, + "immutableWidgets": undefined, + "title": "", + "widgetEnabled": undefined, + "widgets": { + "radio 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "choices": [ + { + "clue": undefined, + "content": "$\\dfrac19+\\dfrac19$", + "correct": true, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + { + "clue": undefined, + "content": "$\\dfrac12+\\dfrac12+\\dfrac12+\\dfrac12+\\dfrac12+\\dfrac12+\\dfrac12+\\dfrac12+\\dfrac12$", + "correct": false, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + { + "clue": undefined, + "content": "$\\dfrac19+\\dfrac29$", + "correct": false, + "isNoneOfTheAbove": false, + "widgets": undefined, + }, + ], + "countChoices": undefined, + "deselectEnabled": false, + "displayCount": null, + "hasNoneOfTheAbove": false, + "multipleSelect": false, + "noneOfTheAbove": undefined, + "onePerLine": true, + "randomize": false, + }, + "static": false, + "type": "radio", + "version": { + "major": 1, + "minor": 0, + }, + }, + }, + }, + "static": false, + "type": "graded-group", + "version": { + "major": 0, + "minor": 0, + }, + }, + "graded-group 2": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "content": "**Write an expression showing $\\dfrac53$ decomposed into unit fractions.** + +[[☃ expression 1]] + + + +", + "hasHint": undefined, + "hint": undefined, + "images": {}, + "immutableWidgets": undefined, + "title": "", + "widgetEnabled": undefined, + "widgets": { + "expression 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "answerForms": [ + { + "considered": "correct", + "form": true, + "key": "0", + "simplify": false, + "value": "\\frac{1}{3}+\\frac{1}{3}+\\frac{1}{3}+\\frac{1}{3}+\\frac{1}{3}", + }, + ], + "ariaLabel": undefined, + "buttonSets": [ + "basic", + ], + "buttonsVisible": undefined, + "functions": [ + "f", + "g", + "h", + ], + "times": false, + "visibleLabel": undefined, + }, + "static": false, + "type": "expression", + "version": { + "major": 1, + "minor": 0, + }, + }, + }, + }, + "static": false, + "type": "graded-group", + "version": { + "major": 0, + "minor": 0, + }, + }, + "graded-group 3": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "content": "**What fraction is equal to $\\dfrac16+\\dfrac16+\\dfrac16+\\dfrac16+\\dfrac16$?** + +[[☃ numeric-input 1]] + + + +", + "hasHint": undefined, + "hint": undefined, + "images": {}, + "immutableWidgets": undefined, + "title": "", + "widgetEnabled": undefined, + "widgets": { + "numeric-input 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "answerForms": undefined, + "answers": [ + { + "answerForms": [ + "proper", + "improper", + ], + "maxError": null, + "message": "", + "simplify": "required", + "status": "correct", + "strict": false, + "value": 0.8333333333333334, + }, + ], + "coefficient": false, + "labelText": "", + "rightAlign": undefined, + "size": "normal", + "static": false, + }, + "static": false, + "type": "numeric-input", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, + "static": false, + "type": "graded-group", + "version": { + "major": 0, + "minor": 0, + }, + }, + "graded-group 4": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "content": "**Match each fraction to its equivalent expression.** + +[[☃ matcher 1]] + + + +", + "hasHint": undefined, + "hint": undefined, + "images": {}, + "immutableWidgets": undefined, + "title": "", + "widgetEnabled": undefined, + "widgets": { + "matcher 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "labels": [ + "Fraction", + "Expression", + ], + "left": [ + "$\\dfrac42$", + "$\\dfrac34$", + "$\\dfrac23$", + ], + "orderMatters": false, + "padding": true, + "right": [ + "$\\dfrac12+\\dfrac12+\\dfrac12+\\dfrac12$", + "$\\dfrac14+\\dfrac14+\\dfrac14$", + "$\\dfrac13+\\dfrac13$", + ], + }, + "static": false, + "type": "matcher", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, + "static": false, + "type": "graded-group", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, + ], + "itemDataVersion": { + "major": 0, + "minor": 1, + }, + "question": { + "content": "", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, +} +`; + exports[`parseAndTypecheckPerseusItem correctly parses data/orderer-option-missing-widgets.json 1`] = ` { "answer": undefined, @@ -143,6 +1634,334 @@ exports[`parseAndTypecheckPerseusItem correctly parses data/orderer-option-missi } `; +exports[`parseAndTypecheckPerseusItem correctly parses data/question-missing-content.json 1`] = ` +{ + "answer": undefined, + "answerArea": {}, + "hints": [], + "itemDataVersion": undefined, + "question": { + "content": "", + "images": {}, + "metadata": undefined, + "widgets": {}, + }, +} +`; + +exports[`parseAndTypecheckPerseusItem correctly parses data/radio-choice-missing-content.json 1`] = ` +{ + "answer": undefined, + "answerArea": { + "calculator": false, + }, + "hints": [], + "itemDataVersion": { + "major": 0, + "minor": 1, + }, + "question": { + "content": " + +[[☃ group 1]] + +[[☃ group 2]] + +[[☃ group 3]] + +[[☃ group 4]]", + "images": {}, + "metadata": undefined, + "widgets": { + "group 1": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "content": " + +[[☃ radio 1]]", + "images": {}, + "metadata": undefined, + "widgets": { + "radio 1": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "choices": [ + { + "clue": undefined, + "content": "", + "correct": undefined, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + { + "clue": undefined, + "content": "", + "correct": undefined, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + { + "clue": undefined, + "content": "", + "correct": undefined, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + { + "clue": undefined, + "content": "", + "correct": undefined, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + ], + "countChoices": undefined, + "deselectEnabled": false, + "displayCount": null, + "hasNoneOfTheAbove": undefined, + "multipleSelect": false, + "noneOfTheAbove": false, + "onePerLine": true, + "randomize": false, + }, + "static": undefined, + "type": "radio", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, + "static": undefined, + "type": "group", + "version": { + "major": 0, + "minor": 0, + }, + }, + "group 2": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "content": " + +[[☃ radio 1]]", + "images": {}, + "metadata": undefined, + "widgets": { + "radio 1": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "choices": [ + { + "clue": undefined, + "content": "", + "correct": undefined, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + { + "clue": undefined, + "content": "", + "correct": undefined, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + { + "clue": undefined, + "content": "", + "correct": undefined, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + { + "clue": undefined, + "content": "", + "correct": undefined, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + ], + "countChoices": undefined, + "deselectEnabled": false, + "displayCount": null, + "hasNoneOfTheAbove": undefined, + "multipleSelect": false, + "noneOfTheAbove": false, + "onePerLine": true, + "randomize": false, + }, + "static": undefined, + "type": "radio", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, + "static": undefined, + "type": "group", + "version": { + "major": 0, + "minor": 0, + }, + }, + "group 3": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "content": " + +[[☃ radio 1]]", + "images": {}, + "metadata": undefined, + "widgets": { + "radio 1": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "choices": [ + { + "clue": undefined, + "content": "", + "correct": undefined, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + { + "clue": undefined, + "content": "", + "correct": undefined, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + { + "clue": undefined, + "content": "", + "correct": undefined, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + { + "clue": undefined, + "content": "", + "correct": undefined, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + ], + "countChoices": undefined, + "deselectEnabled": false, + "displayCount": null, + "hasNoneOfTheAbove": undefined, + "multipleSelect": false, + "noneOfTheAbove": false, + "onePerLine": true, + "randomize": false, + }, + "static": undefined, + "type": "radio", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, + "static": undefined, + "type": "group", + "version": { + "major": 0, + "minor": 0, + }, + }, + "group 4": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "content": " + +[[☃ radio 1]]", + "images": {}, + "metadata": undefined, + "widgets": { + "radio 1": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "choices": [ + { + "clue": undefined, + "content": "", + "correct": undefined, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + { + "clue": undefined, + "content": "", + "correct": undefined, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + { + "clue": undefined, + "content": "", + "correct": undefined, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + { + "clue": undefined, + "content": "", + "correct": undefined, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + ], + "countChoices": undefined, + "deselectEnabled": false, + "displayCount": null, + "hasNoneOfTheAbove": undefined, + "multipleSelect": false, + "noneOfTheAbove": false, + "onePerLine": true, + "randomize": false, + }, + "static": undefined, + "type": "radio", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, + "static": undefined, + "type": "group", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, +} +`; + exports[`parseAndTypecheckPerseusItem correctly parses data/radio-missing-noneOfTheAbove.json 1`] = ` { "answer": undefined, diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/data/categorizer-missing-static.json b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/categorizer-missing-static.json new file mode 100644 index 0000000000..06cb534966 --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/categorizer-missing-static.json @@ -0,0 +1,44 @@ +{ + "answerArea": { + "calculator": false, + "options": { + "content": "", + "images": {}, + "widgets": {} + }, + "type": "multiple" + }, + "hints": [], + "itemDataVersion": { + "major": 0, + "minor": 1 + }, + "question": { + "content": "\n\n[[☃ categorizer 1]]", + "images": {}, + "widgets": { + "categorizer 1": { + "graded": true, + "options": { + "categories": [ + "one", + "two", + "three" + ], + "items": [ + "this", + "that", + "other" + ], + "randomizeItems": true, + "values": [] + }, + "type": "categorizer", + "version": { + "major": 0, + "minor": 0 + } + } + } + } +} diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/data/dropdown-missing-version.json b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/dropdown-missing-version.json new file mode 100644 index 0000000000..a8e107060b --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/dropdown-missing-version.json @@ -0,0 +1,45 @@ +{ + "answerArea": { + "calculator": false, + "options": { + "content": "", + "images": {}, + "widgets": {} + }, + "type": "multiple" + }, + "hints": [ + { + "content": "While the Articles of Confederation did not provide for a federal executive, delegates to the Continental Congress did elect a president.", + "images": {}, + "widgets": {} + } + ], + "question": { + "content": "**Under the Articles of Confederation[[☃ dropdown 1]] chose the president of the United States.** ", + "images": {}, + "widgets": { + "dropdown 1": { + "graded": true, + "options": { + "choices": [ + { + "content": "state legislatures", + "correct": false + }, + { + "content": "delegates to the Continental Congress", + "correct": true + }, + { + "content": "the people", + "correct": false + } + ], + "placeholder": "" + }, + "type": "dropdown" + } + } + } +} diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/data/hint-missing-images.json b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/hint-missing-images.json new file mode 100644 index 0000000000..e2dff0af70 --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/hint-missing-images.json @@ -0,0 +1,189 @@ +{ + "question": { + "content": "Congratulations on all you've learned about systems of equations!\n\nOur strategic supplement version of algebra 1 is for students who are getting classroom instruction in algebra and only using Khan Academy for a short time each week. We only included select exercises, videos, and articles to help you focus your limited time.\n\nIn case you would like a fuller experience, here is a taste of a skill you can learn in our full algebra 1 course.", + "images": {}, + "widgets": {} + }, + "answerArea": { + "calculator": false, + "chi2Table": false, + "periodicTable": false, + "tTable": false, + "zTable": false + }, + "itemDataVersion": { + "major": 0, + "minor": 1 + }, + "hints": [ + { + "replace": false, + "content": "##Scaling equations to solve systems of equations with elimination\n\n###What is it, and why is it useful?\n\nThe elimination strategy is a popular method for mathematicians who like working with integers. When the coefficients of a variable are already opposites, we can just add the equations to get a new equation without that variable. Otherwise, we may want to multiply both sides of one equation (or both) by a constant so that we do get opposite coefficients.\n\n###Practice\n\nIf you haven't practiced [Systems of equations with elimination](/e/systems_of_equations_with_elimination_0.5?getready=pre) yet, we suggest you do that first.\n\n\n\n[[☃ graded-group-set 2]]\n\nFor more practice, go to [Systems of equations with elimination challenge](/e/systems_of_equations_with_elimination?getready=post).", + "images": {}, + "widgets": { + "graded-group-set 2": { + "type": "graded-group-set", + "alignment": "default", + "static": false, + "graded": true, + "options": { + "gradedGroups": [ + { + "title": "Problem 1.1", + "content": "**Solve the system of equations.**\n\n$\\begin{align}\n&-5x-3y +9 =0\n\\\\\\\\\n&4x-18y-14=0\n\\end{align}$\n\n$x=$ [[☃ numeric-input 1]]\n\n$y=$ [[☃ numeric-input 2]]", + "widgets": { + "numeric-input 1": { + "type": "numeric-input", + "alignment": "default", + "static": false, + "graded": true, + "options": { + "static": false, + "answers": [ + { + "value": 2, + "status": "correct", + "message": "", + "simplify": "required", + "strict": false, + "maxError": null + } + ], + "size": "small", + "coefficient": false, + "labelText": "value of x", + "multipleNumberInput": false, + "rightAlign": false + }, + "version": { + "major": 0, + "minor": 0 + } + }, + "numeric-input 2": { + "type": "numeric-input", + "alignment": "default", + "static": false, + "graded": true, + "options": { + "static": false, + "answers": [ + { + "value": -0.3333333333333333, + "status": "correct", + "message": "", + "simplify": "required", + "strict": false, + "maxError": null, + "answerForms": [ + "proper", + "improper" + ] + } + ], + "size": "small", + "coefficient": false, + "labelText": "value of y", + "multipleNumberInput": false, + "rightAlign": false + }, + "version": { + "major": 0, + "minor": 0 + } + } + }, + "images": {}, + "hint": { + "content": "Let's solve the system by using the *elimination method.* In order to eliminate one of the variables, we need to manipulate the equations so that variable has coefficients of the same size but opposite signs in each equation.\n\nLet's take a look at the system:\n\n$\\begin{align}\n&-5x-3y + 9=0\n\\\\\\\\\n&4x-18y-14=0\n\\end{align}$\n\nThe coefficient of $y$ in the second equation, $-18$, is exactly $6$ times the coefficient of $y$ in the first equation, $-3$. Therefore, we can multiply the first equation by $\\purpleD{-6}$ in order to eliminate $y$.\n\n$\\begin{align} \\purpleD{-6}\\cdot(-5x)-(\\purpleD{-6})\\cdot3y+(\\purpleD{-6})\\cdot 9&=(\\purpleD{-6})\\cdot0\\\\\\\\\n30x+18y-54&=0\\end{align}$\n\nNow we can eliminate $y$ as follows:\n\n$\\begin{align} {30x}+\\maroonD{18y} -54&=0 \\\\\\\\\n\\underline{{}\\tealE{+}\n\\left({4x}-\\maroonD{18y}-14\\right)}&=\\underline{{}\\tealE{+}0}\\\\\n34x-0 -68&=0\\end{align}$\n\nWhen we solve the resulting equation we obtain that $x = 2$. Then, we can substitute this into one of the original equations and solve for $y$ to obtain $y=-\\dfrac{1}{3}$.\n\nThis is the solution of the system:\n\n$\\begin{align}\n&x=2\n\\\\\\\\\n&y=-\\dfrac{1}{3}\n\\end{align}$", + "widgets": {} + }, + "widgetEnabled": true, + "immutableWidgets": false + }, + { + "title": "Problem 1.2", + "content": "**Solve the system of equations.**\n\n$\\begin{align}\n&9x-5y = -55\n\\\\\\\\\n&-7x+8y=51\n\\end{align}$\n\n$x=$ [[☃ numeric-input 1]]\n\n$y=$ [[☃ numeric-input 2]]", + "widgets": { + "numeric-input 1": { + "type": "numeric-input", + "version": { + "major": 0, + "minor": 0 + }, + "graded": true, + "alignment": "default", + "static": false, + "options": { + "static": false, + "answers": [ + { + "value": -5, + "status": "correct", + "message": "", + "simplify": "required", + "strict": false, + "maxError": null + } + ], + "size": "small", + "coefficient": false, + "labelText": "value of x", + "multipleNumberInput": false, + "rightAlign": false + } + }, + "numeric-input 2": { + "type": "numeric-input", + "version": { + "major": 0, + "minor": 0 + }, + "graded": true, + "alignment": "default", + "static": false, + "options": { + "static": false, + "answers": [ + { + "value": 2, + "status": "correct", + "message": "", + "simplify": "required", + "strict": false, + "maxError": null + } + ], + "size": "small", + "coefficient": false, + "labelText": "value of y", + "multipleNumberInput": false, + "rightAlign": false + } + } + }, + "images": {}, + "hint": { + "content": "Let's solve the system by using the *elimination method.* In order to eliminate one of the variables, we need to manipulate the equations so that variable has coefficients of the same size but opposite signs in each equation.\n\nLet's take a look at the system:\n\n$\\begin{align}\n&9x-5y = -55\n\\\\\\\\\n&-7x+8y=51\n\\end{align}$\n\nSince the coefficients of either variable aren't multiples of each other, we must multiply both equations in order to eliminate a variable of our choice. Let's eliminate $y$.\n\nSince the *least common multiple* of the coefficients of $y$ in the two equations is $-40$, we can *multiply* the first equation by $\\purpleD{8}$ and the second equation by $\\tealE{5}$ in order to eliminate $y$.\n\n$ \\begin{align}\\purpleD{8}\\cdot9x-\\purpleD{8}\\cdot5y&=\\purpleD{8}\\cdot(-55)\\\\\\\\\n72x-40y&=-440\\end{align}$\n\n$ \\begin{align} \\tealE{5}\\cdot(-7x)+\\tealE{5}\\cdot8y&=\\tealE{5}\\cdot 51\\\\\\\\\n-35x+40y&=255\\end{align}$\n\nNow we can eliminate $y$ as follows:\n\n$\\begin{align} {72x}-\\maroonD{40y} &= -440 \\\\\\\\\n\\underline{{}\\blueE{+}\n{-35x}+\\maroonD{40y}}&=\\underline{{}\\blueE{+}255}\\\\\\\\\n37x-0 &=-185\\end{align}$\n\nWhen we solve the resulting equation we obtain that $x = -5$. Then, we can substitute this into one of the original equations and solve for $y$ to obtain $y=2$.", + "widgets": {} + }, + "widgetEnabled": true, + "immutableWidgets": false + } + ] + }, + "version": { + "major": 0, + "minor": 0 + } + } + } + }, + { + "replace": false, + "content": "##Why wasn't this exercise part of the strategic supplement course?\n\nIt is possible to solve any system of linear equations using the substitution method as long as you're comfortable working with fractions in your equations.", + "images": {}, + "widgets": {} + } + ] +} diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/data/iframe-missing-static.json b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/iframe-missing-static.json new file mode 100644 index 0000000000..03a77893ae --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/iframe-missing-static.json @@ -0,0 +1,49 @@ +{ + "answerArea": { + "calculator": false, + "options": { + "content": "", + "images": {}, + "widgets": {} + }, + "periodicTable": false, + "type": "multiple" + }, + "hints": [ + { + "content": "This is a hint... with a video!\n\n[[☃ iframe 1]]", + "images": {}, + "widgets": { + "iframe 1": { + "graded": true, + "options": { + "allowFullScreen": true, + "height": "235", + "settings": [ + { + "name": "", + "value": "" + } + ], + "url": "https://www.youtube.com/embed/qYD5iwhLzm8?enablejsapi=1&wmode=transparent&modestbranding=1&rel=0&frameborder='0'", + "width": "425" + }, + "type": "iframe", + "version": { + "major": 0, + "minor": 0 + } + } + } + } + ], + "itemDataVersion": { + "major": 0, + "minor": 1 + }, + "question": { + "content": "This is a question.", + "images": {}, + "widgets": {} + } +} diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/data/item-missing-answerArea.json b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/item-missing-answerArea.json new file mode 100644 index 0000000000..41e1dbc06a --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/item-missing-answerArea.json @@ -0,0 +1,16 @@ +{ + "_multi": { + "blurb": { + "__type": "content", + "content": "", + "images": {}, + "widgets": {} + }, + "hints": [], + "question": { + "__type": "content", + "content": "", + "widgets": {} + } + } +} diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/data/matrix-missing-version.json b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/matrix-missing-version.json new file mode 100644 index 0000000000..626e63d149 --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/matrix-missing-version.json @@ -0,0 +1,62 @@ +{ + "answerArea": { + "calculator": false, + "options": { + "content": "" + }, + "type": "multiple" + }, + "hints": [ + { + "content": "The inverse of a matrix is equal to the adjugate of the matrix divided by the determinant of the matrix.\n\n $ \\textbf A^{-1} = \\frac{1}{det(\\textbf A)}adj(\\textbf A) $" + }, + { + "content": "**Step 1: Find the adjugate**\n\n First, compute the matrix of minors of $\\textbf A$.\n\n $ \\left[\\begin{array}{rrr}\\left|\\begin{array}{rr}1 & 1 \\\\ 1 & 1\\end{array}\\right| & \\left|\\begin{array}{rr}0 & 1 \\\\ 1 & 1\\end{array}\\right| & \\left|\\begin{array}{rr}0 & 1 \\\\ 1 & 1\\end{array}\\right| \\\\ \\left|\\begin{array}{rr}0 & 1 \\\\ 1 & 1\\end{array}\\right| & \\left|\\begin{array}{rr}2 & 1 \\\\ 1 & 1\\end{array}\\right| & \\left|\\begin{array}{rr}2 & 0 \\\\ 1 & 1\\end{array}\\right| \\\\ \\left|\\begin{array}{rr}0 & 1 \\\\ 1 & 1\\end{array}\\right| & \\left|\\begin{array}{rr}2 & 1 \\\\ 0 & 1\\end{array}\\right| & \\left|\\begin{array}{rr}2 & 0 \\\\ 0 & 1\\end{array}\\right|\\end{array}\\right] = \\left[\\begin{array}{rrr}0 & -1 & -1 \\\\ -1 & 1 & 2 \\\\ -1 & 2 & 2\\end{array}\\right] $" + }, + { + "content": "Next, multiply the elements of the matrix of minors by the following pattern: \n\n $\\left[\\begin{array}{rrr}+ & - & + \\\\ - & + & - \\\\ + & - & +\\end{array}\\right]$ This gives us what is called the matrix of cofactors:\n\n $ \\left[\\begin{array}{rrr}0 & 1 & -1 \\\\ 1 & 1 & -2 \\\\ -1 & -2 & 2\\end{array}\\right] $" + }, + { + "content": "Next, transpose the matrix of cofactors to get the adjugate.\n\n $ adj(\\textbf A) = \\left[\\begin{array}{rrr}0 & 1 & -1 \\\\ 1 & 1 & -2 \\\\ -1 & -2 & 2\\end{array}\\right]^T = \\left[\\begin{array}{rrr}\\color{\\#6495ED}{0} & \\color{\\#6495ED}{1} & \\color{\\#6495ED}{-1} \\\\ \\color{\\#6495ED}{1} & \\color{\\#6495ED}{1} & \\color{\\#6495ED}{-2} \\\\ \\color{\\#6495ED}{-1} & \\color{\\#6495ED}{-2} & \\color{\\#6495ED}{2}\\end{array}\\right] $" + }, + { + "content": "**Step 2: Find the determinant**\n\n Compute the determinant of the original matrix.\n\n $ det(\\textbf A) = \\left|\\begin{array}{rrr}2 & 0 & 1 \\\\ 0 & 1 & 1 \\\\ 1 & 1 & 1\\end{array}\\right|= \\color{\\#DF0030}{-1} $" + }, + { + "content": "**Step 3: Put it all together**\n\n Now that we have both the determinant and the adjugate, we can compute the inverse.\n\n $ \\textbf A^{-1} = \\frac{1}{\\color{\\#DF0030}{-1}} \\left[\\begin{array}{rrr}\\color{\\#6495ED}{0} & \\color{\\#6495ED}{1} & \\color{\\#6495ED}{-1} \\\\ \\color{\\#6495ED}{1} & \\color{\\#6495ED}{1} & \\color{\\#6495ED}{-2} \\\\ \\color{\\#6495ED}{-1} & \\color{\\#6495ED}{-2} & \\color{\\#6495ED}{2}\\end{array}\\right] $" + }, + { + "content": "$ = \\left[\\begin{array}{rrr}0 & -1 & 1 \\\\ -1 & -1 & 2 \\\\ 1 & 2 & -2\\end{array}\\right]$" + } + ], + "question": { + "content": "$\\textbf A = \\left[\\begin{array}{rrr}2 & 0 & 1 \\\\ 0 & 1 & 1 \\\\ 1 & 1 & 1\\end{array}\\right]$\n\n ** What is $\\textbf A^{-1}$? **\n\n[[☃ matrix 1]]", + "widgets": { + "matrix 1": { + "options": { + "answers": [ + [ + 0, + -1, + 1 + ], + [ + -1, + -1, + 2 + ], + [ + 1, + 2, + -2 + ] + ], + "matrixBoardSize": [ + 3, + 3 + ] + } + } + } + } +} diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/data/orderer-option-missing-images.json b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/orderer-option-missing-images.json new file mode 100644 index 0000000000..49d03525ef --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/orderer-option-missing-images.json @@ -0,0 +1,600 @@ +{ + "answerArea": { + "calculator": false, + "chi2Table": false, + "periodicTable": false, + "tTable": false, + "zTable": false + }, + "hints": [ + { + "content": "##Decomposing fractions into unit fractions\n\nTo **decompose** a number, we break it into smaller parts.\n[[☃ explanation 1]]\n\nA **unit fraction** is a fraction with a numerator of $1$. [[☃ explanation 2]]", + "images": {}, + "replace": false, + "widgets": { + "explanation 1": { + "alignment": "default", + "graded": true, + "options": { + "explanation": "We can decompose $54$ into $50+4$.", + "hidePrompt": "Okay, got it", + "showPrompt": "Show me an example", + "static": false, + "widgets": {} + }, + "static": false, + "type": "explanation", + "version": { + "major": 0, + "minor": 0 + } + }, + "explanation 2": { + "alignment": "default", + "graded": true, + "options": { + "explanation": "$\\dfrac15$ is a unit fraction because the numerator (top number) is $1$.", + "hidePrompt": "Okay, got it", + "showPrompt": "Show me an example", + "static": false, + "widgets": {} + }, + "static": false, + "type": "explanation", + "version": { + "major": 0, + "minor": 0 + } + } + } + }, + { + "content": "###Example 1: Tape diagram\n\nLet's decompose $\\dfrac59$ into unit fractions.\n\n![](web+graphie://ka-perseus-graphie.s3.amazonaws.com/a65a2cd2728b1fa6a12775cdfd353559f99d6582)\n\n\n\n\n[[☃ graded-group 1]]\n\n[[☃ explanation 1]]", + "images": { + "web+graphie://ka-perseus-graphie.s3.amazonaws.com/a65a2cd2728b1fa6a12775cdfd353559f99d6582": { + "height": 30, + "width": 400 + } + }, + "replace": false, + "widgets": { + "explanation 1": { + "alignment": "default", + "graded": true, + "options": { + "explanation": "\n\n![](web+graphie://ka-perseus-graphie.s3.amazonaws.com/bcf2a7df34a1d6994e7ed3c7120cc98f1e5c0aa8)\n\n$\\dfrac59$ can be broken up into $\\dfrac19+\\dfrac19+\\dfrac19+\\dfrac19+\\dfrac19$.", + "hidePrompt": "Hide solution", + "showPrompt": "Show solution", + "static": false, + "widgets": {} + }, + "static": false, + "type": "explanation", + "version": { + "major": 0, + "minor": 0 + } + }, + "graded-group 1": { + "alignment": "default", + "graded": true, + "options": { + "content": "**Drag the cards to create an expression that is equivalent to $\\dfrac59$.**\n\n[[☃ orderer 1]]", + "images": {}, + "widgets": { + "orderer 1": { + "alignment": "default", + "graded": true, + "options": { + "correctOptions": [ + { + "content": "$\\dfrac19$" + }, + { + "content": "$+$" + }, + { + "content": "$\\dfrac19$" + }, + { + "content": "$+$" + }, + { + "content": "$\\dfrac19$" + }, + { + "content": "$+$" + }, + { + "content": "$\\dfrac19$" + }, + { + "content": "$+$" + }, + { + "content": "$\\dfrac19$" + } + ], + "height": "normal", + "layout": "horizontal", + "options": [ + { + "content": "$\\dfrac19$" + }, + { + "content": "$+$" + } + ], + "otherOptions": [] + }, + "static": false, + "type": "orderer", + "version": { + "major": 0, + "minor": 0 + } + } + } + }, + "static": false, + "type": "graded-group", + "version": { + "major": 0, + "minor": 0 + } + } + } + }, + { + "content": "###Example 2: Fraction model\n\nHow can we decompose $\\dfrac38$?\n\n![](web+graphie://ka-perseus-graphie.s3.amazonaws.com/36325871333a2a9bdd2918c6b87d9104758c95e3)\n\n[[☃ graded-group 1]]\n\n[[☃ explanation 1]]", + "images": { + "web+graphie://ka-perseus-graphie.s3.amazonaws.com/36325871333a2a9bdd2918c6b87d9104758c95e3": { + "height": 90, + "width": 96 + }, + "web+graphie://ka-perseus-graphie.s3.amazonaws.com/7165c486a581fb615a85de15378d40b81b266a20": { + "height": 96, + "width": 96 + } + }, + "replace": false, + "widgets": { + "explanation 1": { + "alignment": "default", + "graded": true, + "options": { + "explanation": "$\\dfrac18+\\dfrac18+\\dfrac18$\n\n![](web+graphie://ka-perseus-graphie.s3.amazonaws.com/7165c486a581fb615a85de15378d40b81b266a20)", + "hidePrompt": "Hide solution", + "showPrompt": "Show solution", + "static": false, + "widgets": {} + }, + "static": false, + "type": "explanation", + "version": { + "major": 0, + "minor": 0 + } + }, + "graded-group 1": { + "alignment": "default", + "graded": true, + "options": { + "content": "**Write an expression decomposing $\\dfrac38$ into unit fractions.**\n\n\n\n[[☃ expression 1]]\n\n", + "images": {}, + "widgets": { + "expression 1": { + "alignment": "default", + "graded": true, + "options": { + "answerForms": [ + { + "considered": "correct", + "form": true, + "key": 0, + "simplify": false, + "value": "\\frac{1}{8}+\\frac{1}{8}+\\frac{1}{8}" + } + ], + "buttonSets": [ + "basic" + ], + "functions": [ + "f", + "g", + "h" + ], + "times": true + }, + "static": false, + "type": "expression", + "version": { + "major": 1, + "minor": 0 + } + } + } + }, + "static": false, + "type": "graded-group", + "version": { + "major": 0, + "minor": 0 + } + } + } + }, + { + "content": "###Example 3: Number line\n\nHow can we decompose $\\dfrac64$?\n\n![](web+graphie://ka-perseus-graphie.s3.amazonaws.com/7ebb22693e18c660e133351a0f6e4acd6678e871)\n\n[[☃ graded-group 1]]\n\n[[☃ explanation 1]]", + "images": { + "web+graphie://ka-perseus-graphie.s3.amazonaws.com/7ebb22693e18c660e133351a0f6e4acd6678e871": { + "height": 116, + "width": 410 + } + }, + "replace": false, + "widgets": { + "explanation 1": { + "alignment": "default", + "graded": true, + "options": { + "explanation": "$\\dfrac14+\\dfrac14+\\dfrac14+\\dfrac14+\\dfrac14+\\dfrac14$", + "hidePrompt": "Hide solution", + "showPrompt": "Show solution", + "static": false, + "widgets": {} + }, + "static": false, + "type": "explanation", + "version": { + "major": 0, + "minor": 0 + } + }, + "graded-group 1": { + "alignment": "default", + "graded": true, + "options": { + "content": "**Drag the cards to create an expression that is equivalent to $\\dfrac64$.**\n\n[[☃ orderer 1]]\n\n", + "images": {}, + "widgets": { + "orderer 1": { + "alignment": "default", + "graded": true, + "options": { + "correctOptions": [ + { + "content": "$\\dfrac14$" + }, + { + "content": "$+$" + }, + { + "content": "$\\dfrac14$" + }, + { + "content": "$+$" + }, + { + "content": "$\\dfrac14$" + }, + { + "content": "$+$" + }, + { + "content": "$\\dfrac14$" + }, + { + "content": "$+$" + }, + { + "content": "$\\dfrac14$" + }, + { + "content": "$+$" + }, + { + "content": "$\\dfrac14$" + } + ], + "height": "normal", + "layout": "horizontal", + "options": [ + { + "content": "$\\dfrac14$" + }, + { + "content": "$+$" + } + ], + "otherOptions": [] + }, + "static": false, + "type": "orderer", + "version": { + "major": 0, + "minor": 0 + } + } + } + }, + "static": false, + "type": "graded-group", + "version": { + "major": 0, + "minor": 0 + } + } + } + }, + { + "content": "##Let's try a few more.\n\n###Problem 1\n\n[[☃ graded-group 1]]\n\n[[☃ explanation 1]]\n\n\n\n###Problem 2\n\n[[☃ graded-group 2]]\n\n[[☃ explanation 2]]\n\n\n###Problem 3\n\n[[☃ graded-group 3]]\n\n[[☃ explanation 3]]\n\n\n###Problem 4\n\n[[☃ graded-group 4]]\n\n[[☃ explanation 4]]", + "images": {}, + "replace": false, + "widgets": { + "explanation 1": { + "alignment": "default", + "graded": true, + "options": { + "explanation": "$\\dfrac29=\\dfrac19+\\dfrac19$", + "hidePrompt": "Hide solution", + "showPrompt": "Show solution", + "static": false, + "widgets": {} + }, + "static": false, + "type": "explanation", + "version": { + "major": 0, + "minor": 0 + } + }, + "explanation 2": { + "alignment": "default", + "graded": true, + "options": { + "explanation": "$\\dfrac13+\\dfrac13+\\dfrac13+\\dfrac13+\\dfrac13$", + "hidePrompt": "Hide solution", + "showPrompt": "Show solution", + "static": false, + "widgets": {} + }, + "static": false, + "type": "explanation", + "version": { + "major": 0, + "minor": 0 + } + }, + "explanation 3": { + "alignment": "default", + "graded": true, + "options": { + "explanation": "$\\dfrac16+\\dfrac16+\\dfrac16+\\dfrac16+\\dfrac16=\\dfrac56$\n", + "hidePrompt": "Hide solution", + "showPrompt": "Show solution", + "static": false, + "widgets": {} + }, + "static": false, + "type": "explanation", + "version": { + "major": 0, + "minor": 0 + } + }, + "explanation 4": { + "alignment": "default", + "graded": true, + "options": { + "explanation": "\n\nFraction | Solution\n- | - | -\n$\\dfrac42$ | $\\dfrac12+\\dfrac12+\\dfrac12+\\dfrac12$\n$\\dfrac34$ | $\\dfrac14+\\dfrac14+\\dfrac14$\n$\\dfrac23$ | $\\dfrac13+\\dfrac13$", + "hidePrompt": "Hide solution", + "showPrompt": "Show solution", + "static": false, + "widgets": {} + }, + "static": false, + "type": "explanation", + "version": { + "major": 0, + "minor": 0 + } + }, + "graded-group 1": { + "alignment": "default", + "graded": true, + "options": { + "content": "**Which shows $\\dfrac29$ decomposed into unit fractions?**\n\n[[☃ radio 1]]\n\n", + "images": {}, + "widgets": { + "radio 1": { + "alignment": "default", + "graded": true, + "options": { + "choices": [ + { + "content": "$\\dfrac19+\\dfrac19$", + "correct": true + }, + { + "content": "$\\dfrac12+\\dfrac12+\\dfrac12+\\dfrac12+\\dfrac12+\\dfrac12+\\dfrac12+\\dfrac12+\\dfrac12$", + "correct": false + }, + { + "content": "$\\dfrac19+\\dfrac29$", + "correct": false, + "isNoneOfTheAbove": false + } + ], + "deselectEnabled": false, + "displayCount": null, + "hasNoneOfTheAbove": false, + "multipleSelect": false, + "onePerLine": true, + "randomize": false + }, + "static": false, + "type": "radio", + "version": { + "major": 1, + "minor": 0 + } + } + } + }, + "static": false, + "type": "graded-group", + "version": { + "major": 0, + "minor": 0 + } + }, + "graded-group 2": { + "alignment": "default", + "graded": true, + "options": { + "content": "**Write an expression showing $\\dfrac53$ decomposed into unit fractions.**\n\n[[☃ expression 1]]\n\n\n\n", + "images": {}, + "widgets": { + "expression 1": { + "alignment": "default", + "graded": true, + "options": { + "answerForms": [ + { + "considered": "correct", + "form": true, + "key": 0, + "simplify": false, + "value": "\\frac{1}{3}+\\frac{1}{3}+\\frac{1}{3}+\\frac{1}{3}+\\frac{1}{3}" + } + ], + "buttonSets": [ + "basic" + ], + "functions": [ + "f", + "g", + "h" + ], + "times": false + }, + "static": false, + "type": "expression", + "version": { + "major": 1, + "minor": 0 + } + } + } + }, + "static": false, + "type": "graded-group", + "version": { + "major": 0, + "minor": 0 + } + }, + "graded-group 3": { + "alignment": "default", + "graded": true, + "options": { + "content": "**What fraction is equal to $\\dfrac16+\\dfrac16+\\dfrac16+\\dfrac16+\\dfrac16$?**\n\n[[☃ numeric-input 1]]\n\n\n\n", + "images": {}, + "widgets": { + "numeric-input 1": { + "alignment": "default", + "graded": true, + "options": { + "answers": [ + { + "answerForms": [ + "proper", + "improper" + ], + "maxError": null, + "message": "", + "simplify": "required", + "status": "correct", + "strict": false, + "value": 0.8333333333333334 + } + ], + "coefficient": false, + "labelText": "", + "size": "normal", + "static": false + }, + "static": false, + "type": "numeric-input", + "version": { + "major": 0, + "minor": 0 + } + } + } + }, + "static": false, + "type": "graded-group", + "version": { + "major": 0, + "minor": 0 + } + }, + "graded-group 4": { + "alignment": "default", + "graded": true, + "options": { + "content": "**Match each fraction to its equivalent expression.**\n\n[[☃ matcher 1]]\n\n\n\n", + "images": {}, + "widgets": { + "matcher 1": { + "alignment": "default", + "graded": true, + "options": { + "labels": [ + "Fraction", + "Expression" + ], + "left": [ + "$\\dfrac42$", + "$\\dfrac34$", + "$\\dfrac23$" + ], + "orderMatters": false, + "padding": true, + "right": [ + "$\\dfrac12+\\dfrac12+\\dfrac12+\\dfrac12$", + "$\\dfrac14+\\dfrac14+\\dfrac14$", + "$\\dfrac13+\\dfrac13$" + ] + }, + "static": false, + "type": "matcher", + "version": { + "major": 0, + "minor": 0 + } + } + } + }, + "static": false, + "type": "graded-group", + "version": { + "major": 0, + "minor": 0 + } + } + } + } + ], + "itemDataVersion": { + "major": 0, + "minor": 1 + }, + "question": { + "content": "", + "images": {}, + "widgets": {} + } +} diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/data/question-missing-content.json b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/question-missing-content.json new file mode 100644 index 0000000000..b4a8c0eed2 --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/question-missing-content.json @@ -0,0 +1,7 @@ +{ + "answerArea": { + "type": "multiple" + }, + "hints": [], + "question": {} +} diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/data/radio-choice-missing-content.json b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/radio-choice-missing-content.json new file mode 100644 index 0000000000..4ff48cb254 --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/radio-choice-missing-content.json @@ -0,0 +1,166 @@ +{ + "answerArea": { + "calculator": false, + "options": { + "content": "", + "images": {}, + "widgets": {} + }, + "type": "multiple" + }, + "hints": [], + "itemDataVersion": { + "major": 0, + "minor": 1 + }, + "question": { + "content": "\n\n[[☃ group 1]]\n\n[[☃ group 2]]\n\n[[☃ group 3]]\n\n[[☃ group 4]]", + "images": {}, + "widgets": { + "group 1": { + "graded": true, + "options": { + "content": "\n\n[[☃ radio 1]]", + "images": {}, + "widgets": { + "radio 1": { + "graded": true, + "options": { + "choices": [ + {}, + {}, + {}, + {} + ], + "deselectEnabled": false, + "displayCount": null, + "multipleSelect": false, + "noneOfTheAbove": false, + "onePerLine": true, + "randomize": false + }, + "type": "radio", + "version": { + "major": 0, + "minor": 0 + } + } + } + }, + "type": "group", + "version": { + "major": 0, + "minor": 0 + } + }, + "group 2": { + "graded": true, + "options": { + "content": "\n\n[[☃ radio 1]]", + "images": {}, + "widgets": { + "radio 1": { + "graded": true, + "options": { + "choices": [ + {}, + {}, + {}, + {} + ], + "deselectEnabled": false, + "displayCount": null, + "multipleSelect": false, + "noneOfTheAbove": false, + "onePerLine": true, + "randomize": false + }, + "type": "radio", + "version": { + "major": 0, + "minor": 0 + } + } + } + }, + "type": "group", + "version": { + "major": 0, + "minor": 0 + } + }, + "group 3": { + "graded": true, + "options": { + "content": "\n\n[[☃ radio 1]]", + "images": {}, + "widgets": { + "radio 1": { + "graded": true, + "options": { + "choices": [ + {}, + {}, + {}, + {} + ], + "deselectEnabled": false, + "displayCount": null, + "multipleSelect": false, + "noneOfTheAbove": false, + "onePerLine": true, + "randomize": false + }, + "type": "radio", + "version": { + "major": 0, + "minor": 0 + } + } + } + }, + "type": "group", + "version": { + "major": 0, + "minor": 0 + } + }, + "group 4": { + "graded": true, + "options": { + "content": "\n\n[[☃ radio 1]]", + "images": {}, + "widgets": { + "radio 1": { + "graded": true, + "options": { + "choices": [ + {}, + {}, + {}, + {} + ], + "deselectEnabled": false, + "displayCount": null, + "multipleSelect": false, + "noneOfTheAbove": false, + "onePerLine": true, + "randomize": false + }, + "type": "radio", + "version": { + "major": 0, + "minor": 0 + } + } + } + }, + "type": "group", + "version": { + "major": 0, + "minor": 0 + } + } + } + } +} diff --git a/packages/perseus/src/widgets/graded-group/graded-group.tsx b/packages/perseus/src/widgets/graded-group/graded-group.tsx index 23eaeef1bc..443b37d41e 100644 --- a/packages/perseus/src/widgets/graded-group/graded-group.tsx +++ b/packages/perseus/src/widgets/graded-group/graded-group.tsx @@ -37,6 +37,7 @@ import type { } from "../../types"; import type {PerseusGradedGroupRubric} from "../../validation.types"; import type {GradedGroupPromptJSON} from "../../widget-ai-utils/graded-group/graded-group-ai-utils"; +import type {PropsFor} from "@khanacademy/wonder-blocks-core"; const GRADING_STATUSES = { ungraded: "ungraded" as const, @@ -93,6 +94,17 @@ type State = { answerBarState: ANSWER_BAR_STATES; }; +// Assert that the PerseusGradedGroupWidgetOptions parsed from JSON can be +// passed as props to this component. This ensures that the +// PerseusGradedGroupWidgetOptions stays in sync with the prop types. The +// PropsFor type takes defaultProps into account, which is important +// because PerseusGradedGroupWidgetOptions has optional fields which receive defaults +// via defaultProps. +0 as any as WidgetProps< + PerseusGradedGroupWidgetOptions, + PerseusGradedGroupRubric +> satisfies PropsFor; + // A Graded Group is more or less a Group widget that displays a check // answer button below the rendered content. When clicked, the widget grades // the stuff inside and displays feedback about whether the inputted answer was diff --git a/packages/perseus/src/widgets/matrix/matrix.tsx b/packages/perseus/src/widgets/matrix/matrix.tsx index c252bdaa89..eb0dc3bd64 100644 --- a/packages/perseus/src/widgets/matrix/matrix.tsx +++ b/packages/perseus/src/widgets/matrix/matrix.tsx @@ -17,13 +17,17 @@ import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/matrix/matr import scoreMatrix from "./score-matrix"; -import type {PerseusMatrixWidgetOptions} from "../../perseus-types"; +import type { + PerseusMatrixWidgetAnswers, + PerseusMatrixWidgetOptions, +} from "../../perseus-types"; import type {WidgetExports, WidgetProps, Widget, FocusPath} from "../../types"; import type { PerseusMatrixRubric, PerseusMatrixUserInput, } from "../../validation.types"; import type {MatrixPromptJSON} from "../../widget-ai-utils/matrix/matrix-ai-utils"; +import type {PropsFor} from "@khanacademy/wonder-blocks-core"; const {assert} = InteractiveUtil; const {stringArrayOfSize} = Util; @@ -100,10 +104,34 @@ export function getMatrixSize(matrix: ReadonlyArray>) { } type ExternalProps = WidgetProps< - PerseusMatrixWidgetOptions, + { + // Translatable Text; Shown before the matrix + prefix: string; + // Translatable Text; Shown after the matrix + suffix: string; + // A data matrix representing the "correct" answers to be entered into the matrix + answers: PerseusMatrixWidgetAnswers; + // The coordinate location of the cursor position at start. default: [0, 0] + cursorPosition: ReadonlyArray; + // The coordinate size of the matrix. Only supports 2-dimensional matrix. default: [3, 3] + matrixBoardSize: ReadonlyArray; + // Whether this is meant to statically display the answers (true) or be used as an input field, graded against the answers + static?: boolean | undefined; + }, PerseusMatrixRubric >; +// Assert that the PerseusMatrixWidgetOptions parsed from JSON can be passed +// as props to this component. This ensures that the PerseusMatrixWidgetOptions +// stays in sync with the prop types. The PropsFor type takes +// defaultProps into account, which is important because +// PerseusMatrixWidgetOptions has optional fields which receive defaults via +// defaultProps. +0 as any as WidgetProps< + PerseusMatrixWidgetOptions, + PerseusMatrixRubric +> satisfies PropsFor; + type Props = ExternalProps & { onChange: ( arg1: { @@ -118,9 +146,9 @@ type Props = ExternalProps & { type DefaultProps = { matrixBoardSize: Props["matrixBoardSize"]; answers: Props["answers"]; - prefix: Props["prefix"]; - suffix: Props["suffix"]; - cursorPosition: Props["cursorPosition"]; + prefix: string; + suffix: string; + cursorPosition: ReadonlyArray; apiOptions: Props["apiOptions"]; linterContext: Props["linterContext"]; }; From 7f2866cf401aa4fc7a3b2b15d8cdc247a953e9f8 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Tue, 26 Nov 2024 14:59:30 -0700 Subject: [PATCH 03/10] Migrate expression widget to latest version in PerseusItem parser (#1908) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This demonstrates how we can upgrade/migrate widget options in the parsing code, obviating the need for `propUpgrades`. The fact that `version` is an object makes this really annoying because TypeScript can't discriminate unions based on nested properties like `version.major`. Given that constraint, I think the approach I came up with is reasonable, but I'm certainly open to feedback on how to improve it. Issue: LEMS-2582 ## Test plan: `yarn test` Author: benchristel Reviewers: jeremywiebe, anakaren-rojas, catandthemachines, nishasy Required Reviewers: Approved By: jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1908 --- .changeset/yellow-ducks-march.md | 5 ++ .../perseus-parsers/expression-widget.test.ts | 76 +++++++++++++++++++ .../perseus-parsers/expression-widget.ts | 74 +++++++++++++++++- .../perseus-parsers/widget.ts | 20 +++++ .../perseus-parsers/widgets-map.test.ts | 2 +- 5 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 .changeset/yellow-ducks-march.md create mode 100644 packages/perseus/src/util/parse-perseus-json/perseus-parsers/expression-widget.test.ts diff --git a/.changeset/yellow-ducks-march.md b/.changeset/yellow-ducks-march.md new file mode 100644 index 0000000000..6346118468 --- /dev/null +++ b/.changeset/yellow-ducks-march.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Internal: Migrate expression widget options to the latest version in parseAndTypecheckPerseusItem (not yet used in production). diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/expression-widget.test.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/expression-widget.test.ts new file mode 100644 index 0000000000..a0e623246a --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/expression-widget.test.ts @@ -0,0 +1,76 @@ +import {parse} from "../parse"; +import {failure, success} from "../result"; + +import {parseExpressionWidget} from "./expression-widget"; + +describe("parseExpressionWidget", () => { + it("migrates v0 options to v1", () => { + const widget = { + type: "expression", + graded: true, + options: { + value: "the value", + form: false, + simplify: false, + times: false, + buttonsVisible: "never", + buttonSets: ["basic"], + functions: ["f", "g", "h"], + }, + version: { + major: 0, + minor: 1, + }, + }; + + expect(parse(widget, parseExpressionWidget)).toEqual( + success({ + type: "expression", + graded: true, + static: undefined, + key: undefined, + alignment: undefined, + options: { + times: false, + ariaLabel: undefined, + visibleLabel: undefined, + buttonsVisible: "never", + buttonSets: ["basic"], + functions: ["f", "g", "h"], + answerForms: [ + { + considered: "correct", + form: false, + simplify: false, + value: "the value", + }, + ], + }, + version: { + major: 1, + minor: 0, + }, + }), + ); + }); + + it("rejects a widget with unrecognized version", () => { + const widget = { + type: "expression", + version: { + major: -1, + minor: 0, + }, + graded: true, + options: {}, + }; + + expect(parse(widget, parseExpressionWidget)).toEqual( + failure( + expect.stringContaining( + "At (root).version.major -- expected 0, but got -1", + ), + ), + ); + }); +}); diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/expression-widget.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/expression-widget.ts index 0622c99aa2..3d181092fe 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/expression-widget.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/expression-widget.ts @@ -1,3 +1,4 @@ +import ExpressionWidgetModule from "../../../widgets/expression/expression"; import { array, boolean, @@ -11,13 +12,18 @@ import { union, } from "../general-purpose-parsers"; -import {parseWidget} from "./widget"; +import {parseWidgetWithVersion} from "./widget"; import type { ExpressionWidget, PerseusExpressionAnswerForm, } from "../../../perseus-types"; -import type {Parser} from "../parser-types"; +import type { + ParseContext, + ParsedValue, + Parser, + ParseResult, +} from "../parser-types"; const parseAnswerForm: Parser = object({ value: string, @@ -29,14 +35,42 @@ const parseAnswerForm: Parser = object({ ).parser, }); -export const parseExpressionWidget: Parser = parseWidget( +const parseExpressionWidgetV1: Parser = + parseWidgetWithVersion( + object({major: constant(1), minor: number}), + constant("expression"), + object({ + answerForms: array(parseAnswerForm), + functions: array(string), + times: boolean, + visibleLabel: optional(string), + ariaLabel: optional(string), + buttonSets: array( + enumeration( + "basic", + "basic+div", + "trig", + "prealgebra", + "logarithms", + "basic relations", + "advanced relations", + ), + ), + buttonsVisible: optional(enumeration("always", "never", "focused")), + }), + ); + +const parseExpressionWidgetV0 = parseWidgetWithVersion( + optional(object({major: constant(0), minor: number})), constant("expression"), object({ - answerForms: array(parseAnswerForm), functions: array(string), times: boolean, visibleLabel: optional(string), ariaLabel: optional(string), + form: boolean, + simplify: boolean, + value: string, buttonSets: array( enumeration( "basic", @@ -51,3 +85,35 @@ export const parseExpressionWidget: Parser = parseWidget( buttonsVisible: optional(enumeration("always", "never", "focused")), }), ); + +function migrateV0ToV1( + widget: ParsedValue, + ctx: ParseContext, +): ParseResult { + const {options} = widget; + return ctx.success({ + ...widget, + version: ExpressionWidgetModule.version, + options: { + times: options.times, + buttonSets: options.buttonSets, + functions: options.functions, + buttonsVisible: options.buttonsVisible, + visibleLabel: options.visibleLabel, + ariaLabel: options.ariaLabel, + + answerForms: [ + { + considered: "correct", + form: options.form, + simplify: options.simplify, + value: options.value, + }, + ], + }, + }); +} + +export const parseExpressionWidget: Parser = union( + parseExpressionWidgetV1, +).or(pipeParsers(parseExpressionWidgetV0).then(migrateV0ToV1).parser).parser; diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widget.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widget.ts index 8fb94450fa..330075c46a 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widget.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widget.ts @@ -28,3 +28,23 @@ export function parseWidget( ), }); } + +export function parseWidgetWithVersion< + Version extends {major: number; minor: number} | undefined, + Type extends string, + Options, +>( + parseVersion: Parser, + parseType: Parser, + parseOptions: Parser, +): Parser> { + return object({ + type: parseType, + static: optional(boolean), + graded: optional(boolean), + alignment: optional(string), + options: parseOptions, + key: optional(number), + version: parseVersion, + }); +} diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widgets-map.test.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widgets-map.test.ts index 89f0dead25..9d6a672364 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widgets-map.test.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widgets-map.test.ts @@ -134,7 +134,7 @@ describe("parseWidgetsMap", () => { const widgetsMap: unknown = { "expression 1": { type: "expression", - version: {major: 0, minor: 0}, + version: {major: 1, minor: 0}, options: { answerForms: [], buttonSets: [], From 799ffe4a50e3e3bc435d0ef96388c1e8fae2167d Mon Sep 17 00:00:00 2001 From: anakaren-rojas Date: Tue, 26 Nov 2024 14:07:53 -0800 Subject: [PATCH 04/10] feat(LEMS-2664): update use control point and movable point with optional props (#1906) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: - Updates _MovablePoint_ component and _useControlPoint_ method to include optional props for `aria label`, `aria described by`, `aria live`, and `sequence number` - Sets default for aria live to `polite`; based on design doc, only the removal of a point or trying to move a point of out bounds warrants the use of `assertive` - Set default for sequence number - if aria label and sequence number are not provided - sequence will default to 1 - Creates logic for optional aria label - custom aria label will take precedence over sequence number Issue: LEMS-2664 ## Test plan: - Using [chromatic](https://650db21c3f5d1b2f13c02952-jhskbowqxd.chromatic.com/?path=/docs/perseuseditor-editorpage--docs) link, selecting an interactive graph point reads out the point's coordinates ## Screen recording https://github.com/user-attachments/assets/f2414756-b0bf-48bf-98ed-08f30c41d1a1 Author: anakaren-rojas Reviewers: anakaren-rojas, nishasy, #perseus, benchristel, catandthemachines Required Reviewers: Approved By: nishasy Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1906 --- .changeset/lazy-geckos-suffer.md | 5 + .../__snapshots__/movable-line.test.tsx.snap | 12 +- .../graphs/components/movable-point.test.tsx | 108 ++++++++++++++++++ .../graphs/components/movable-point.tsx | 18 +-- .../graphs/components/use-control-point.tsx | 41 ++++--- .../src/widgets/interactive-graphs/types.ts | 2 + 6 files changed, 158 insertions(+), 28 deletions(-) create mode 100644 .changeset/lazy-geckos-suffer.md diff --git a/.changeset/lazy-geckos-suffer.md b/.changeset/lazy-geckos-suffer.md new file mode 100644 index 0000000000..9b5853b83f --- /dev/null +++ b/.changeset/lazy-geckos-suffer.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +update moveable point component and use control point method to have optional params diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/components/__snapshots__/movable-line.test.tsx.snap b/packages/perseus/src/widgets/interactive-graphs/graphs/components/__snapshots__/movable-line.test.tsx.snap index 4c129238f2..560d781cdf 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/components/__snapshots__/movable-line.test.tsx.snap +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/components/__snapshots__/movable-line.test.tsx.snap @@ -16,7 +16,7 @@ exports[`Rendering Does NOT render extensions of line when option is disabled 1` > { expect(focusSpy).toHaveBeenCalledTimes(1); }); + + describe("accessibility", () => { + it("uses the default sequence number when ariaLabel and sequence number are not provided", () => { + render( + + + , + ); + + expect( + screen.getByLabelText("Point 1 at 0 comma 0"), + ).toBeInTheDocument(); + }); + + it("uses sequence number when sequence is provided and aria label is not provided", () => { + render( + + + , + ); + + expect( + screen.getByLabelText("Point 2 at 0 comma 0"), + ).toBeInTheDocument(); + }); + + it("uses the ariaLabel when both sequence and ariaLabel are provided", () => { + render( + + + , + ); + + expect( + screen.getByLabelText( + "Aria Label being used instead of sequence number", + ), + ).toBeInTheDocument(); + }); + + it("uses the ariaLabel when only ariaLabel is provided", () => { + render( + + + , + ); + + expect( + screen.getByLabelText("Custom aria label"), + ).toBeInTheDocument(); + }); + + it("uses the ariaDescribedBy when provided", () => { + render( + + +

Aria is described by me

+
, + ); + + const pointElement = screen.getByRole("button", { + name: "Point 1 at 0 comma 0", + }); + expect(pointElement).toHaveAttribute( + "aria-describedby", + "description", + ); + + const descriptionElement = screen.getByText( + "Aria is described by me", + ); + expect(descriptionElement).toBeInTheDocument(); + }); + + it("uses the ariaLive when provided", () => { + render( + + + , + ); + + expect( + screen.getByLabelText("Point 1 at 0 comma 0"), + ).toHaveAttribute("aria-live", "assertive"); + }); + + it("uses the default ariaLive when not provided", () => { + render( + + + , + ); + + expect( + screen.getByLabelText("Point 1 at 0 comma 0"), + ).toHaveAttribute("aria-live", "polite"); + }); + }); }); diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/components/movable-point.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/components/movable-point.tsx index cebf76f435..b855da3e1c 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/components/movable-point.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/components/movable-point.tsx @@ -3,11 +3,18 @@ import * as React from "react"; import {useControlPoint} from "./use-control-point"; import type {CSSCursor} from "./css-cursor"; +import type {AriaLive} from "../../types"; import type {KeyboardMovementConstraint} from "../use-draggable"; import type {vec} from "mafs"; type Props = { point: vec.Vector2; + ariaDescribedBy?: string; + ariaLabel?: string; + ariaLive?: AriaLive; + color?: string; + constrain?: KeyboardMovementConstraint; + cursor?: CSSCursor | undefined; /** * Represents where this point stands in the overall point sequence. * This is used to provide screen readers with context about the point. @@ -16,14 +23,11 @@ type Props = { * Note: This number is 1-indexed, and should restart from 1 for each * interactive figure on the graph. */ - sequenceNumber: number; - onMove?: (newPoint: vec.Vector2) => unknown; + sequenceNumber?: number; + onBlur?: (event: React.FocusEvent) => unknown; onClick?: () => unknown; - color?: string; - cursor?: CSSCursor | undefined; - constrain?: KeyboardMovementConstraint; - onFocus?: ((event: React.FocusEvent) => unknown) | undefined; - onBlur?: ((event: React.FocusEvent) => unknown) | undefined; + onFocus?: (event: React.FocusEvent) => unknown; + onMove?: (newPoint: vec.Vector2) => unknown; }; export const MovablePoint = React.forwardRef( diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/components/use-control-point.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/components/use-control-point.tsx index 6687b79543..0cc35e3f6b 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/components/use-control-point.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/components/use-control-point.tsx @@ -10,13 +10,20 @@ import {useDraggable} from "../use-draggable"; import {MovablePointView} from "./movable-point-view"; import type {CSSCursor} from "./css-cursor"; +import type {AriaLive} from "../../types"; import type {KeyboardMovementConstraint} from "../use-draggable"; import type {vec} from "mafs"; type Params = { point: vec.Vector2; + ariaDescribedBy?: string; + ariaLabel?: string; + ariaLive?: AriaLive; color?: string | undefined; + constrain?: KeyboardMovementConstraint; cursor?: CSSCursor | undefined; + // The focusableHandle element is assigned to the forwarded ref. + forwardedRef?: React.ForwardedRef | undefined; /** * Represents where this point stands in the overall point sequence. * This is used to provide screen readers with context about the point. @@ -25,14 +32,11 @@ type Params = { * Note: This number is 1-indexed, and should restart from 1 for each * interactive figure on the graph. */ - sequenceNumber: number; - constrain?: KeyboardMovementConstraint; + sequenceNumber?: number; onMove?: ((newPoint: vec.Vector2) => unknown) | undefined; onClick?: (() => unknown) | undefined; onFocus?: ((event: React.FocusEvent) => unknown) | undefined; onBlur?: ((event: React.FocusEvent) => unknown) | undefined; - // The focusableHandle element is assigned to the forwarded ref. - forwardedRef?: React.ForwardedRef | undefined; }; type Return = { @@ -46,15 +50,18 @@ export function useControlPoint(params: Params): Return { const {snapStep, disableKeyboardInteraction} = useGraphConfig(); const { point, - sequenceNumber, + ariaDescribedBy, + ariaLabel, + ariaLive = "polite", color, - cursor, constrain = (p) => snap(snapStep, p), + cursor, + forwardedRef = noop, + sequenceNumber = 1, onMove = noop, onClick = noop, onFocus = noop, onBlur = noop, - forwardedRef = noop, } = params; const {strings, locale} = usePerseusI18n(); @@ -76,6 +83,15 @@ export function useControlPoint(params: Params): Return { constrainKeyboardMovement: constrain, }); + // if custom aria label is not provided, will use default of sequence number and point coordinates + const pointAriaLabel = + ariaLabel || + strings.srPointAtCoordinates({ + num: sequenceNumber, + x: srFormatNumber(point[X], locale), + y: srFormatNumber(point[Y], locale), + }); + useLayoutEffect(() => { setForwardedRef(forwardedRef, focusableHandleRef.current); }, [forwardedRef]); @@ -87,14 +103,9 @@ export function useControlPoint(params: Params): Return { tabIndex={disableKeyboardInteraction ? -1 : 0} ref={focusableHandleRef} role="button" - aria-label={strings.srPointAtCoordinates({ - num: sequenceNumber, - x: srFormatNumber(point[X], locale), - y: srFormatNumber(point[Y], locale), - })} - // aria-live="assertive" causes the new location of the point to be - // announced immediately on move. - aria-live="assertive" + aria-describedby={ariaDescribedBy} + aria-label={pointAriaLabel} + aria-live={ariaLive} onFocus={(event) => { onFocus(event); setFocused(true); diff --git a/packages/perseus/src/widgets/interactive-graphs/types.ts b/packages/perseus/src/widgets/interactive-graphs/types.ts index 565643ca81..140cea9f02 100644 --- a/packages/perseus/src/widgets/interactive-graphs/types.ts +++ b/packages/perseus/src/widgets/interactive-graphs/types.ts @@ -135,3 +135,5 @@ export type GraphDimensions = { width: number; // pixels height: number; // pixels }; + +export type AriaLive = "off" | "assertive" | "polite" | undefined; From 88ba71bef0cdd75fa0c8b467dcea2cccc637d034 Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 27 Nov 2024 08:46:57 -0600 Subject: [PATCH 05/10] remove some error suppressions (#1920) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: idk, just following some threads by un-excepting errors. Author: handeyeco Reviewers: handeyeco, jeremywiebe Required Reviewers: Approved By: jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1920 --- .changeset/few-jokes-travel.md | 8 ++ config/test/test-setup.ts | 1 - packages/kas/src/nodes.ts | 3 +- .../src/__stories__/item-editor.stories.tsx | 3 +- .../src/multirenderer-editor.tsx | 3 +- .../locked-function-settings.test.tsx | 3 - .../src/widgets/orderer-editor.tsx | 10 +-- packages/perseus/src/hints-renderer.tsx | 1 - .../src/interactive2/wrapped-drawing.ts | 1 - packages/perseus/src/perseus-markdown.tsx | 3 +- packages/perseus/src/perseus-types.ts | 4 +- packages/perseus/src/util.ts | 5 +- packages/perseus/src/util/graphie.ts | 1 - .../perseus-parsers/orderer-widget.ts | 6 +- .../perseus-parsers/widgets-map.test.ts | 4 +- packages/perseus/src/widgets/grapher/util.tsx | 89 +++++++++---------- .../perseus/src/widgets/orderer/orderer.tsx | 60 +++++-------- packages/simple-markdown/src/index.ts | 7 +- 18 files changed, 96 insertions(+), 116 deletions(-) create mode 100644 .changeset/few-jokes-travel.md diff --git a/.changeset/few-jokes-travel.md b/.changeset/few-jokes-travel.md new file mode 100644 index 0000000000..b4a57f6823 --- /dev/null +++ b/.changeset/few-jokes-travel.md @@ -0,0 +1,8 @@ +--- +"@khanacademy/kas": patch +"@khanacademy/perseus": patch +"@khanacademy/perseus-editor": patch +"@khanacademy/simple-markdown": patch +--- + +Fix some file-wide error suppressions diff --git a/config/test/test-setup.ts b/config/test/test-setup.ts index f907ec797a..cd4bf6f5cf 100644 --- a/config/test/test-setup.ts +++ b/config/test/test-setup.ts @@ -35,7 +35,6 @@ if (typeof window !== "undefined") { // Override the window.location implementation to mock out assign() // We need to access window.location.assign to verify that we're // redirecting to the right place. - /* eslint-disable no-restricted-syntax */ const oldLocation = window.location; // @ts-expect-error - TS2790 - The operand of a 'delete' operator must be optional. delete window.location; diff --git a/packages/kas/src/nodes.ts b/packages/kas/src/nodes.ts index 6aca9a49e2..9d33c1fe05 100644 --- a/packages/kas/src/nodes.ts +++ b/packages/kas/src/nodes.ts @@ -1,7 +1,7 @@ /* eslint-disable prettier/prettier */ /* eslint-disable import/order */ /* TODO(charlie): fix these lint errors (http://eslint.org/docs/rules): */ -/* eslint-disable indent, no-undef, no-var, one-var, no-dupe-keys, no-new-func, no-redeclare, @typescript-eslint/no-unused-vars, comma-dangle, max-len, prefer-spread, space-infix-ops, space-unary-ops */ +/* eslint-disable indent, no-undef, no-var, no-dupe-keys, no-new-func, no-redeclare, comma-dangle, max-len, prefer-spread, space-infix-ops, space-unary-ops */ import _ from "underscore"; import {unitParser} from "./__genfiles__/unitparser"; @@ -1414,7 +1414,6 @@ export class Mul extends Seq { rational = rational.addHint("fraction"); } - var result; if (num.n < 0 && right.n < 0) { rational.d = -rational.d; return left.replace(num, [NumNeg, rational]); diff --git a/packages/perseus-editor/src/__stories__/item-editor.stories.tsx b/packages/perseus-editor/src/__stories__/item-editor.stories.tsx index 26862e4c5d..cef53e11bf 100644 --- a/packages/perseus-editor/src/__stories__/item-editor.stories.tsx +++ b/packages/perseus-editor/src/__stories__/item-editor.stories.tsx @@ -10,7 +10,6 @@ import "../styles/perseus-editor.less"; type Props = React.ComponentProps; const Wrapper = (props: Props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars const {onChange, ...rest} = props; const [extras, setExtras] = React.useState>(rest); @@ -19,7 +18,7 @@ const Wrapper = (props: Props) => { { - props.onChange?.(e); // to register action in storybook + onChange?.(e); // to register action in storybook setExtras((prevExtras) => ({...prevExtras, ...e})); }} /> diff --git a/packages/perseus-editor/src/multirenderer-editor.tsx b/packages/perseus-editor/src/multirenderer-editor.tsx index 9a9264b1f8..2d0445c7fd 100644 --- a/packages/perseus-editor/src/multirenderer-editor.tsx +++ b/packages/perseus-editor/src/multirenderer-editor.tsx @@ -31,8 +31,7 @@ import type { // Multi-item item types Item, - // ItemTree is used below, the linter is confused. - ItemTree, // eslint-disable-line @typescript-eslint/no-unused-vars + ItemTree, ItemObjectNode, ItemArrayNode, ContentNode, diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-function-settings.test.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-function-settings.test.tsx index 5caaf10b6e..3d9df676d1 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-function-settings.test.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-function-settings.test.tsx @@ -5,9 +5,6 @@ import * as React from "react"; import {flags} from "../../../__stories__/flags-for-api-options"; -// Disabling the following linting error because the import is needed for mocking purposes. -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import examples from "./locked-function-examples"; import LockedFunctionSettings from "./locked-function-settings"; import { getDefaultFigureForType, diff --git a/packages/perseus-editor/src/widgets/orderer-editor.tsx b/packages/perseus-editor/src/widgets/orderer-editor.tsx index c4a9b20b97..d841ce0799 100644 --- a/packages/perseus-editor/src/widgets/orderer-editor.tsx +++ b/packages/perseus-editor/src/widgets/orderer-editor.tsx @@ -1,5 +1,5 @@ /* eslint-disable @khanacademy/ts-no-error-suppressions */ -/* eslint-disable one-var, react/forbid-prop-types */ +/* eslint-disable react/forbid-prop-types */ import {components} from "@khanacademy/perseus"; import PropTypes from "prop-types"; import * as React from "react"; @@ -7,10 +7,10 @@ import _ from "underscore"; const {InfoTip, TextListEditor} = components; -const NORMAL = "normal", - AUTO = "auto", - HORIZONTAL = "horizontal", - VERTICAL = "vertical"; +const NORMAL = "normal"; +const AUTO = "auto"; +const HORIZONTAL = "horizontal"; +const VERTICAL = "vertical"; type Props = any; diff --git a/packages/perseus/src/hints-renderer.tsx b/packages/perseus/src/hints-renderer.tsx index 8cf5145039..ee57f01b99 100644 --- a/packages/perseus/src/hints-renderer.tsx +++ b/packages/perseus/src/hints-renderer.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @khanacademy/ts-no-error-suppressions */ import * as PerseusLinter from "@khanacademy/perseus-linter"; import {StyleSheet, css} from "aphrodite"; import classnames from "classnames"; diff --git a/packages/perseus/src/interactive2/wrapped-drawing.ts b/packages/perseus/src/interactive2/wrapped-drawing.ts index a6e4599a5e..6b9c47b136 100644 --- a/packages/perseus/src/interactive2/wrapped-drawing.ts +++ b/packages/perseus/src/interactive2/wrapped-drawing.ts @@ -1,4 +1,3 @@ -/* eslint-disable @babel/no-invalid-this */ /** * Default methods for a wrapped movable. */ diff --git a/packages/perseus/src/perseus-markdown.tsx b/packages/perseus/src/perseus-markdown.tsx index 47be9731b1..472cce4403 100644 --- a/packages/perseus/src/perseus-markdown.tsx +++ b/packages/perseus/src/perseus-markdown.tsx @@ -1,4 +1,3 @@ -/* eslint-disable no-useless-escape, no-prototype-builtins */ import {pureMarkdownRules} from "@khanacademy/pure-markdown"; import SimpleMarkdown from "@khanacademy/simple-markdown"; import * as React from "react"; @@ -201,7 +200,7 @@ const rules = { // link was not put together properly, let's make sure it's there so we // don't break the entire page. const isKAUrl = href - ? href.match(/https?:\/\/[^\/]*khanacademy.org|^\//) + ? href.match(/https?:\/\/[^/]*khanacademy.org|^\//) : false; if (!isKAUrl) { // Prevents "reverse tabnabbing" phishing attacks diff --git a/packages/perseus/src/perseus-types.ts b/packages/perseus/src/perseus-types.ts index 95dd9993d5..f0d251c768 100644 --- a/packages/perseus/src/perseus-types.ts +++ b/packages/perseus/src/perseus-types.ts @@ -1171,9 +1171,9 @@ export type PerseusOrdererWidgetOptions = { // Cards that are not part of the answer otherOptions: ReadonlyArray; // "normal" for text options. "auto" for image options. - height: string; + height: "normal" | "auto"; // Use the "horizontal" layout for short text and small images. The "vertical" layout is best for longer text (e.g. proofs). - layout: string; + layout: "horizontal" | "vertical"; }; export type PerseusPassageWidgetOptions = { diff --git a/packages/perseus/src/util.ts b/packages/perseus/src/util.ts index 0efb5a3da8..d50781e975 100644 --- a/packages/perseus/src/util.ts +++ b/packages/perseus/src/util.ts @@ -1,4 +1,3 @@ -/* eslint-disable @babel/no-invalid-this, getter-return, one-var */ import {Errors, PerseusError} from "@khanacademy/perseus-core"; import _ from "underscore"; @@ -150,8 +149,8 @@ function shuffle( do { // Fischer-Yates shuffle for (let top = shuffled.length; top > 0; top--) { - const newEnd = Math.floor(random() * top), - temp = shuffled[newEnd]; + const newEnd = Math.floor(random() * top); + const temp = shuffled[newEnd]; // @ts-expect-error - TS2542 - Index signature in type 'readonly T[]' only permits reading. shuffled[newEnd] = shuffled[top - 1]; diff --git a/packages/perseus/src/util/graphie.ts b/packages/perseus/src/util/graphie.ts index 308efaf7f3..7909feeb95 100644 --- a/packages/perseus/src/util/graphie.ts +++ b/packages/perseus/src/util/graphie.ts @@ -1,4 +1,3 @@ -/* eslint-disable @babel/no-invalid-this */ import { point as kpoint, vector as kvector, diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/orderer-widget.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/orderer-widget.ts index 4c8f72e52c..b0c72ab602 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/orderer-widget.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/orderer-widget.ts @@ -1,4 +1,4 @@ -import {array, constant, object, string} from "../general-purpose-parsers"; +import {array, constant, enumeration, object} from "../general-purpose-parsers"; import {parsePerseusRenderer} from "./perseus-renderer"; import {parseWidget} from "./widget"; @@ -19,7 +19,7 @@ export const parseOrdererWidget: Parser = parseWidget( options: array(parseRenderer), correctOptions: array(parseRenderer), otherOptions: array(parseRenderer), - height: string, - layout: string, + height: enumeration("normal", "auto"), + layout: enumeration("horizontal", "vertical"), }), ); diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widgets-map.test.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widgets-map.test.ts index 9d6a672364..3b21b880e4 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widgets-map.test.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widgets-map.test.ts @@ -512,8 +512,8 @@ describe("parseWidgetsMap", () => { options: [], correctOptions: [], otherOptions: [], - height: "", - layout: "", + height: "normal", + layout: "horizontal", }, }, }; diff --git a/packages/perseus/src/widgets/grapher/util.tsx b/packages/perseus/src/widgets/grapher/util.tsx index fa3a684e58..ed7c6af887 100644 --- a/packages/perseus/src/widgets/grapher/util.tsx +++ b/packages/perseus/src/widgets/grapher/util.tsx @@ -1,4 +1,3 @@ -/* eslint-disable one-var */ import {point as kpoint} from "@khanacademy/kmath"; import * as React from "react"; import _ from "underscore"; @@ -141,15 +140,15 @@ const Linear: LinearType = _.extend({}, PlotDefaults, { }, getFunctionForCoeffs: function (coeffs: ReadonlyArray, x: number) { - const m = coeffs[0], - b = coeffs[1]; + const m = coeffs[0]; + const b = coeffs[1]; return m * x + b; }, getEquationString: function (coords: Coords) { const coeffs: ReadonlyArray = this.getCoefficients(coords); - const m: number = coeffs[0], - b: number = coeffs[1]; + const m: number = coeffs[0]; + const b: number = coeffs[1]; return "y = " + m.toFixed(3) + "x + " + b.toFixed(3); }, }); @@ -184,9 +183,9 @@ const Quadratic: QuadraticType = _.extend({}, PlotDefaults, { coeffs: ReadonlyArray, x: number, ): number { - const a = coeffs[0], - b = coeffs[1], - c = coeffs[2]; + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; return (a * x + b) * x + c; }, @@ -204,9 +203,9 @@ const Quadratic: QuadraticType = _.extend({}, PlotDefaults, { getEquationString: function (coords: Coords) { const coeffs = this.getCoefficients(coords); - const a = coeffs[0], - b = coeffs[1], - c = coeffs[2]; + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; return ( "y = " + a.toFixed(3) + @@ -241,10 +240,10 @@ const Sinusoid: SinusoidType = _.extend({}, PlotDefaults, { }, getFunctionForCoeffs: function (coeffs: ReadonlyArray, x: number) { - const a = coeffs[0], - b = coeffs[1], - c = coeffs[2], - d = coeffs[3]; + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; + const d = coeffs[3]; return a * Math.sin(b * x - c) + d; }, @@ -259,10 +258,10 @@ const Sinusoid: SinusoidType = _.extend({}, PlotDefaults, { getEquationString: function (coords: Coords) { const coeffs = this.getCoefficients(coords); - const a = coeffs[0], - b = coeffs[1], - c = coeffs[2], - d = coeffs[3]; + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; + const d = coeffs[3]; return ( "y = " + a.toFixed(3) + @@ -307,19 +306,19 @@ const Tangent: TangentType = _.extend({}, PlotDefaults, { }, getFunctionForCoeffs: function (coeffs: ReadonlyArray, x: number) { - const a = coeffs[0], - b = coeffs[1], - c = coeffs[2], - d = coeffs[3]; + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; + const d = coeffs[3]; return a * Math.tan(b * x - c) + d; }, getEquationString: function (coords: Coords) { const coeffs = this.getCoefficients(coords); - const a = coeffs[0], - b = coeffs[1], - c = coeffs[2], - d = coeffs[3]; + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; + const d = coeffs[3]; return ( "y = " + a.toFixed(3) + @@ -430,9 +429,9 @@ const Exponential: ExponentialType = _.extend({}, PlotDefaults, { coeffs: ReadonlyArray, x: number, ): number { - const a = coeffs[0], - b = coeffs[1], - c = coeffs[2]; + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; return a * Math.exp(b * x) + c; }, @@ -441,9 +440,9 @@ const Exponential: ExponentialType = _.extend({}, PlotDefaults, { return null; } const coeffs = this.getCoefficients(coords, asymptote); - const a = coeffs[0], - b = coeffs[1], - c = coeffs[2]; + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; return ( "y = " + a.toFixed(3) + @@ -537,9 +536,9 @@ const Logarithm: LogarithmType = _.extend({}, PlotDefaults, { x: number, asymptote: Coords, ) { - const a = coeffs[0], - b = coeffs[1], - c = coeffs[2]; + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; return a * Math.log(b * x + c); }, @@ -551,9 +550,9 @@ const Logarithm: LogarithmType = _.extend({}, PlotDefaults, { coords, asymptote, ); - const a = coeffs[0], - b = coeffs[1], - c = coeffs[2]; + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; return ( "y = ln(" + a.toFixed(3) + @@ -597,17 +596,17 @@ const AbsoluteValue: AbsoluteValueType = _.extend({}, PlotDefaults, { }, getFunctionForCoeffs: function (coeffs: ReadonlyArray, x: number) { - const m = coeffs[0], - horizontalOffset = coeffs[1], - verticalOffset = coeffs[2]; + const m = coeffs[0]; + const horizontalOffset = coeffs[1]; + const verticalOffset = coeffs[2]; return m * Math.abs(x - horizontalOffset) + verticalOffset; }, getEquationString: function (coords: Coords) { const coeffs: ReadonlyArray = this.getCoefficients(coords); - const m = coeffs[0], - horizontalOffset = coeffs[1], - verticalOffset = coeffs[2]; + const m = coeffs[0]; + const horizontalOffset = coeffs[1]; + const verticalOffset = coeffs[2]; return ( "y = " + m.toFixed(3) + diff --git a/packages/perseus/src/widgets/orderer/orderer.tsx b/packages/perseus/src/widgets/orderer/orderer.tsx index 7324daa325..972f50fd02 100644 --- a/packages/perseus/src/widgets/orderer/orderer.tsx +++ b/packages/perseus/src/widgets/orderer/orderer.tsx @@ -1,5 +1,5 @@ /* eslint-disable @khanacademy/ts-no-error-suppressions */ -/* eslint-disable @babel/no-invalid-this, @typescript-eslint/no-unused-vars, one-var, react/no-unsafe */ +/* eslint-disable @babel/no-invalid-this, react/no-unsafe */ import {Errors} from "@khanacademy/perseus-core"; import {linterContextDefault} from "@khanacademy/perseus-linter"; import $ from "jquery"; @@ -17,12 +17,7 @@ import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/orderer/ord import {scoreOrderer} from "./score-orderer"; import type {PerseusOrdererWidgetOptions} from "../../perseus-types"; -import type { - PerseusScore, - WidgetExports, - WidgetProps, - Widget, -} from "../../types"; +import type {WidgetExports, WidgetProps, Widget} from "../../types"; import type { PerseusOrdererRubric, PerseusOrdererUserInput, @@ -300,11 +295,6 @@ class Card extends React.Component { } } -const NORMAL = "normal"; -const AUTO = "auto"; -const HORIZONTAL = "horizontal"; -const VERTICAL = "vertical"; - type RenderProps = PerseusOrdererWidgetOptions & { current: any; }; @@ -344,8 +334,8 @@ class Orderer current: [], options: [], correctOptions: [], - height: NORMAL, - layout: HORIZONTAL, + height: "normal", + layout: "horizontal", linterContext: linterContextDefault, }; @@ -521,7 +511,7 @@ class Orderer findCorrectIndex: (arg1: any, arg2: any) => any = (draggable, list) => { // Find the correct index for a card given the current cards. - const isHorizontal = this.props.layout === HORIZONTAL; + const isHorizontal = this.props.layout === "horizontal"; // eslint-disable-next-line react/no-string-refs // @ts-expect-error - TS2769 - No overload matches this call. const $dragList = $(ReactDOM.findDOMNode(this.refs.dragList)); @@ -585,28 +575,24 @@ class Orderer return false; } - const isHorizontal = this.props.layout === HORIZONTAL, - // @ts-expect-error - TS2769 - No overload matches this call. - $draggable = $(ReactDOM.findDOMNode(draggable)), - // eslint-disable-next-line react/no-string-refs - // @ts-expect-error - TS2769 - No overload matches this call. - $bank = $(ReactDOM.findDOMNode(this.refs.bank)), - // @ts-expect-error - TS2339 - Property 'offset' does not exist on type 'JQueryStatic'. - draggableOffset = $draggable.offset(), - // @ts-expect-error - TS2339 - Property 'offset' does not exist on type 'JQueryStatic'. - bankOffset = $bank.offset(), - // @ts-expect-error - TS2339 - Property 'outerHeight' does not exist on type 'JQueryStatic'. - draggableHeight = $draggable.outerHeight(true), - // @ts-expect-error - TS2339 - Property 'outerHeight' does not exist on type 'JQueryStatic'. - bankHeight = $bank.outerHeight(true), - // @ts-expect-error - TS2339 - Property 'outerWidth' does not exist on type 'JQueryStatic'. - bankWidth = $bank.outerWidth(true), - // eslint-disable-next-line react/no-string-refs - dragList = ReactDOM.findDOMNode(this.refs.dragList), - // @ts-expect-error - TS2769 - No overload matches this call. | TS2339 - Property 'width' does not exist on type 'JQueryStatic'. - dragListWidth = $(dragList).width(), - // @ts-expect-error - TS2339 - Property 'outerWidth' does not exist on type 'JQueryStatic'. - draggableWidth = $draggable.outerWidth(true); + const isHorizontal = this.props.layout === "horizontal"; + // @ts-expect-error - TS2769 - No overload matches this call. + const $draggable = $(ReactDOM.findDOMNode(draggable)); + // eslint-disable-next-line react/no-string-refs + // @ts-expect-error - TS2769 - No overload matches this call. + const $bank = $(ReactDOM.findDOMNode(this.refs.bank)); + // @ts-expect-error - TS2339 - Property 'offset' does not exist on type 'JQueryStatic'. + const draggableOffset = $draggable.offset(); + // @ts-expect-error - TS2339 - Property 'offset' does not exist on type 'JQueryStatic'. + const bankOffset = $bank.offset(); + // @ts-expect-error - TS2339 - Property 'outerHeight' does not exist on type 'JQueryStatic'. + const draggableHeight = $draggable.outerHeight(true); + // @ts-expect-error - TS2339 - Property 'outerHeight' does not exist on type 'JQueryStatic'. + const bankHeight = $bank.outerHeight(true); + // @ts-expect-error - TS2339 - Property 'outerWidth' does not exist on type 'JQueryStatic'. + const bankWidth = $bank.outerWidth(true); + // @ts-expect-error - TS2339 - Property 'outerWidth' does not exist on type 'JQueryStatic'. + const draggableWidth = $draggable.outerWidth(true); if (isHorizontal) { return ( diff --git a/packages/simple-markdown/src/index.ts b/packages/simple-markdown/src/index.ts index 18feefe347..e4ad7187b2 100644 --- a/packages/simple-markdown/src/index.ts +++ b/packages/simple-markdown/src/index.ts @@ -1,4 +1,4 @@ -/* eslint-disable prefer-spread, no-regex-spaces, @typescript-eslint/no-unused-vars, guard-for-in, no-console, no-var */ +/* eslint-disable prefer-spread, no-regex-spaces, guard-for-in, no-console, no-var */ /** * Simple-Markdown * =============== @@ -710,7 +710,6 @@ var TABLES = (function () { // predefine regexes so we don't have to create them inside functions // sure, regex literals should be fast, even inside functions, but they // aren't in all browsers. - var TABLE_BLOCK_TRIM = /\n+/g; var TABLE_ROW_SEPARATOR_TRIM = /^ *\| *| *\| *$/g; var TABLE_CELL_END_TRIM = / *$/; var TABLE_RIGHT_ALIGN = /^ *-+: *$/; @@ -931,7 +930,7 @@ var defaultRules: DefaultRules = { // map output over the ast, except group any text // nodes together into a single string output. - for (var i = 0, key = 0; i < arr.length; i++) { + for (var i = 0; i < arr.length; i++) { var node = arr[i]; if (node.type === "text") { node = {type: "text", content: node.content}; @@ -1901,7 +1900,7 @@ var markdownToHtml = function (source: string, state?: State | null): string { // TODO: This needs definition type Props = any; -var ReactMarkdown = function (props): React.ReactElement { +var ReactMarkdown = function (props: Props): React.ReactElement { var divProps: Record = {}; for (var prop in props) { From 64ea2ee86264a20f1d0e34968831945fea8ed36b Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 27 Nov 2024 08:47:44 -0600 Subject: [PATCH 06/10] remove findDOMNode from text-input (#1919) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: `findDOMNode` is deprecated and React whines about it constantly when running tests, so I'm trying to chip away at removing uses of it. Author: handeyeco Reviewers: handeyeco, jeremywiebe Required Reviewers: Approved By: jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1919 --- .changeset/giant-tables-impress.md | 5 +++ .../perseus/src/components/text-input.tsx | 43 ++++++++++--------- 2 files changed, 28 insertions(+), 20 deletions(-) create mode 100644 .changeset/giant-tables-impress.md diff --git a/.changeset/giant-tables-impress.md b/.changeset/giant-tables-impress.md new file mode 100644 index 0000000000..32f5de4204 --- /dev/null +++ b/.changeset/giant-tables-impress.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Remove usage of findDOMNode in text-input component diff --git a/packages/perseus/src/components/text-input.tsx b/packages/perseus/src/components/text-input.tsx index abcb259260..83ad468395 100644 --- a/packages/perseus/src/components/text-input.tsx +++ b/packages/perseus/src/components/text-input.tsx @@ -1,7 +1,7 @@ /* eslint-disable @khanacademy/ts-no-error-suppressions */ +import {Errors, PerseusError} from "@khanacademy/perseus-core"; import {TextField} from "@khanacademy/wonder-blocks-form"; import * as React from "react"; -import ReactDOM from "react-dom"; import type {StyleType} from "@khanacademy/wonder-blocks-core"; @@ -31,6 +31,7 @@ function uniqueIdForInput(prefix = "input-") { } class TextInput extends React.Component { + inputRef = React.createRef(); static defaultProps: DefaultProps = { value: "", disabled: false, @@ -47,45 +48,46 @@ class TextInput extends React.Component { } } + _getInput: () => HTMLInputElement = () => { + if (!this.inputRef.current) { + throw new PerseusError( + "Input ref accessed before set", + Errors.Internal, + ); + } + + return this.inputRef.current; + }; + focus: () => void = () => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'focus' does not exist on type 'Element | Text'. - ReactDOM.findDOMNode(this).focus(); + this._getInput().focus(); }; blur: () => void = () => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'blur' does not exist on type 'Element | Text'. - ReactDOM.findDOMNode(this).blur(); + this._getInput().blur(); }; getValue: () => string | null | undefined = () => { - // @ts-expect-error - TS2339 - Property 'value' does not exist on type 'Element | Text'. - return ReactDOM.findDOMNode(this)?.value; + return this.inputRef.current?.value; }; getStringValue: () => string | null | undefined = () => { - // @ts-expect-error - TS2339 - Property 'value' does not exist on type 'Element | Text'. - return ReactDOM.findDOMNode(this)?.value.toString(); + return this.inputRef.current?.value.toString(); }; setSelectionRange: (arg1: number, arg2: number) => void = ( selectionStart, selectionEnd, ) => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'setSelectionRange' does not exist on type 'Element | Text'. - ReactDOM.findDOMNode(this).setSelectionRange( - selectionStart, - selectionEnd, - ); + this._getInput().setSelectionRange(selectionStart, selectionEnd); }; - getSelectionStart: () => number = () => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'selectionStart' does not exist on type 'Element | Text'. - return ReactDOM.findDOMNode(this).selectionStart; + getSelectionStart: () => number | null = () => { + return this._getInput().selectionStart; }; - getSelectionEnd: () => number = () => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'selectionEnd' does not exist on type 'Element | Text'. - return ReactDOM.findDOMNode(this).selectionEnd; + getSelectionEnd: () => number | null = () => { + return this._getInput().selectionEnd; }; render(): React.ReactNode { @@ -104,6 +106,7 @@ class TextInput extends React.Component { return ( Date: Wed, 27 Nov 2024 08:48:35 -0600 Subject: [PATCH 07/10] remove use of findDOMNode in number-input (#1915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: When running tests, I was tired of seeing: > Warning: findDOMNode is deprecated and will be removed in the next major release. Instead, add a ref directly to the element you want to reference. Learn more about using refs safely here: https://reactjs.org/link/strict-mode-find-node So I thought I'd start chipping away at them. ## Test plan: Nothing should change, just an implementation detail. Author: handeyeco Reviewers: jeremywiebe, handeyeco, nishasy Required Reviewers: Approved By: jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1915 --- .changeset/chilly-carrots-wink.md | 5 ++ .../perseus/src/components/number-input.tsx | 57 ++++++++----------- 2 files changed, 30 insertions(+), 32 deletions(-) create mode 100644 .changeset/chilly-carrots-wink.md diff --git a/.changeset/chilly-carrots-wink.md b/.changeset/chilly-carrots-wink.md new file mode 100644 index 0000000000..eb31515686 --- /dev/null +++ b/.changeset/chilly-carrots-wink.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Remove use of findDOMNode in number-input component diff --git a/packages/perseus/src/components/number-input.tsx b/packages/perseus/src/components/number-input.tsx index 2d7f20efd8..d375a882c4 100644 --- a/packages/perseus/src/components/number-input.tsx +++ b/packages/perseus/src/components/number-input.tsx @@ -1,10 +1,8 @@ -/* eslint-disable @khanacademy/ts-no-error-suppressions */ import {number as knumber} from "@khanacademy/kmath"; +import {Errors, PerseusError} from "@khanacademy/perseus-core"; import classNames from "classnames"; -import $ from "jquery"; import PropTypes from "prop-types"; import * as React from "react"; -import ReactDOM from "react-dom"; import _ from "underscore"; import Util from "../util"; @@ -40,6 +38,7 @@ const getNumericFormat = KhanMath.getNumericFormat; class NumberInput extends React.Component { static contextType = PerseusI18nContext; declare context: React.ContextType; + inputRef = React.createRef(); static propTypes = { value: PropTypes.number, @@ -71,20 +70,27 @@ class NumberInput extends React.Component { } } + _getInput: () => HTMLInputElement = () => { + if (!this.inputRef.current) { + throw new PerseusError( + "Input ref accessed before set", + Errors.Internal, + ); + } + + return this.inputRef.current; + }; + /* Return the current "value" of this input * If empty, it returns the placeholder (if it is a number) or null */ getValue: () => any = () => { - return this.parseInputValue( - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'value' does not exist on type 'Element | Text'. - ReactDOM.findDOMNode(this.refs.input).value, // eslint-disable-line react/no-string-refs - ); + return this.parseInputValue(this._getInput().value); }; /* Return the current string value of this input */ getStringValue: () => string = () => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'value' does not exist on type 'Element | Text'. - return ReactDOM.findDOMNode(this.refs.input).value.toString(); // eslint-disable-line react/no-string-refs + return this._getInput().toString(); }; parseInputValue: (arg1: any) => any = (value) => { @@ -98,36 +104,28 @@ class NumberInput extends React.Component { /* Set text input focus to this input */ focus: () => void = () => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'focus' does not exist on type 'Element | Text'. - ReactDOM.findDOMNode(this.refs.input).focus(); // eslint-disable-line react/no-string-refs + this._getInput().focus(); this._handleFocus(); }; blur: () => void = () => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'blur' does not exist on type 'Element | Text'. - ReactDOM.findDOMNode(this.refs.input).blur(); // eslint-disable-line react/no-string-refs + this._getInput().blur(); this._handleBlur(); }; - setSelectionRange: (arg1: number, arg2: number) => any = ( + setSelectionRange: (arg1: number, arg2: number) => void = ( selectionStart, selectionEnd, ) => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'setSelectionRange' does not exist on type 'Element | Text'. - ReactDOM.findDOMNode(this).setSelectionRange( - selectionStart, - selectionEnd, - ); + this._getInput().setSelectionRange(selectionStart, selectionEnd); }; - getSelectionStart: () => number = () => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'selectionStart' does not exist on type 'Element | Text'. - return ReactDOM.findDOMNode(this).selectionStart; + getSelectionStart: () => number | null = () => { + return this._getInput().selectionStart; }; - getSelectionEnd: () => number = () => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'selectionEnd' does not exist on type 'Element | Text'. - return ReactDOM.findDOMNode(this).selectionEnd; + getSelectionEnd: () => number | null = () => { + return this._getInput().selectionEnd; }; _checkValidity: (arg1: any) => boolean = (value) => { @@ -203,11 +201,7 @@ class NumberInput extends React.Component { }; _setValue: (arg1: number, arg2: MathFormat) => void = (val, format) => { - // eslint-disable-next-line react/no-string-refs - // @ts-expect-error - TS2769 - No overload matches this call. | TS2339 - Property 'val' does not exist on type 'JQueryStatic'. - $(ReactDOM.findDOMNode(this.refs.input)).val( - toNumericString(val, format), - ); + this._getInput().value = toNumericString(val, format); }; render(): React.ReactNode { @@ -237,8 +231,7 @@ class NumberInput extends React.Component { {...restProps} className={classes} type="text" - // eslint-disable-next-line react/no-string-refs - ref="input" + ref={this.inputRef} onChange={this._handleChange} onFocus={this._handleFocus} onBlur={this._handleBlur} From 3e98b7cd300052eeacbe9fcdbd312091c678107b Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 27 Nov 2024 08:49:30 -0600 Subject: [PATCH 08/10] Add tests for propUpgrades functions (#1914) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: Only 3 widgets seem to use the `propUpgrades` mechanism: - Radio - Expression - Measurer None of them seemed to have tests (at least not direct tests), so this adds them. I also took out underscore after I wrote the tests. ## Test plan: This is the test plan Author: handeyeco Reviewers: jeremywiebe, handeyeco Required Reviewers: Approved By: jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1914 --- .changeset/two-feet-care.md | 5 +++ .../widgets/expression/expression.test.tsx | 37 +++++++++++++++- .../src/widgets/measurer/measurer.test.tsx | 44 +++++++++++++++++++ .../perseus/src/widgets/measurer/measurer.tsx | 24 +++++----- .../src/widgets/radio/__tests__/radio.test.ts | 35 ++++++++++++++- packages/perseus/src/widgets/radio/radio.ts | 20 ++++----- 6 files changed, 138 insertions(+), 27 deletions(-) create mode 100644 .changeset/two-feet-care.md create mode 100644 packages/perseus/src/widgets/measurer/measurer.test.tsx diff --git a/.changeset/two-feet-care.md b/.changeset/two-feet-care.md new file mode 100644 index 0000000000..3a774776ad --- /dev/null +++ b/.changeset/two-feet-care.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Add tests for propUpgrades functions (and remove underscore usage) diff --git a/packages/perseus/src/widgets/expression/expression.test.tsx b/packages/perseus/src/widgets/expression/expression.test.tsx index be8d76b63c..2f5ee2a239 100644 --- a/packages/perseus/src/widgets/expression/expression.test.tsx +++ b/packages/perseus/src/widgets/expression/expression.test.tsx @@ -17,7 +17,10 @@ import { expressionItemWithLabels, } from "./expression.testdata"; -import type {PerseusItem} from "../../perseus-types"; +import type { + PerseusExpressionWidgetOptions, + PerseusItem, +} from "../../perseus-types"; import type {UserEvent} from "@testing-library/user-event"; const renderAndAnswer = async ( @@ -523,4 +526,36 @@ describe("Expression Widget", function () { ).toBeNull(); }); }); + + describe("propUpgrades", () => { + it("can upgrade from v0 to v1", () => { + const v0props = { + times: false, + buttonSets: ["basic"], + functions: [], + form: false, + simplify: false, + value: "42", + }; + + const expected: PerseusExpressionWidgetOptions = { + times: false, + buttonSets: ["basic"], + functions: [], + answerForms: [ + { + considered: "correct", + form: false, + simplify: false, + value: "42", + }, + ], + }; + + const result: PerseusExpressionWidgetOptions = + ExpressionWidgetExport.propUpgrades["1"](v0props); + + expect(result).toEqual(expected); + }); + }); }); diff --git a/packages/perseus/src/widgets/measurer/measurer.test.tsx b/packages/perseus/src/widgets/measurer/measurer.test.tsx new file mode 100644 index 0000000000..6a35c0ca4c --- /dev/null +++ b/packages/perseus/src/widgets/measurer/measurer.test.tsx @@ -0,0 +1,44 @@ +import MeasurerWidgetExport from "./measurer"; + +import type {PerseusMeasurerWidgetOptions} from "../../perseus-types"; + +describe("measurer", () => { + describe("propUpgrades", () => { + it("can upgrade from v0 to v1", () => { + const v0props = { + imageUrl: "url", + imageTop: 42, + imageLeft: 42, + showProtractor: false, + showRuler: false, + rulerLabel: "test", + rulerTicks: 4, + rulerPixels: 4, + rulerLength: 4, + box: [4, 4], + static: false, + }; + + const expected: PerseusMeasurerWidgetOptions = { + image: { + url: "url", + top: 42, + left: 42, + }, + showProtractor: false, + showRuler: false, + rulerLabel: "test", + rulerTicks: 4, + rulerPixels: 4, + rulerLength: 4, + box: [4, 4], + static: false, + }; + + const result: PerseusMeasurerWidgetOptions = + MeasurerWidgetExport.propUpgrades["1"](v0props); + + expect(result).toEqual(expected); + }); + }); +}); diff --git a/packages/perseus/src/widgets/measurer/measurer.tsx b/packages/perseus/src/widgets/measurer/measurer.tsx index 4347ec677e..db4b00a07b 100644 --- a/packages/perseus/src/widgets/measurer/measurer.tsx +++ b/packages/perseus/src/widgets/measurer/measurer.tsx @@ -182,19 +182,17 @@ class Measurer extends React.Component implements Widget { } const propUpgrades = { - "1": (v0props: any): any => { - const v1props = _(v0props) - .chain() - .omit("imageUrl", "imageTop", "imageLeft") - .extend({ - image: { - url: v0props.imageUrl, - top: v0props.imageTop, - left: v0props.imageLeft, - }, - }) - .value(); - return v1props; + "1": (v0props: any): PerseusMeasurerWidgetOptions => { + const {imageUrl, imageTop, imageLeft, ...rest} = v0props; + + return { + ...rest, + image: { + url: imageUrl, + top: imageTop, + left: imageLeft, + }, + }; }, } as const; diff --git a/packages/perseus/src/widgets/radio/__tests__/radio.test.ts b/packages/perseus/src/widgets/radio/__tests__/radio.test.ts index 4eeb1de264..593f5a94c4 100644 --- a/packages/perseus/src/widgets/radio/__tests__/radio.test.ts +++ b/packages/perseus/src/widgets/radio/__tests__/radio.test.ts @@ -8,6 +8,7 @@ import * as Dependencies from "../../../dependencies"; import {mockStrings} from "../../../strings"; import {renderQuestion} from "../../__testutils__/renderQuestion"; import PassageWidget from "../../passage"; +import RadioWidgetExport from "../radio"; import scoreRadio from "../score-radio"; import { @@ -17,7 +18,10 @@ import { shuffledNoneQuestion, } from "./radio.testdata"; -import type {PerseusRenderer} from "../../../perseus-types"; +import type { + PerseusRadioWidgetOptions, + PerseusRenderer, +} from "../../../perseus-types"; import type {APIOptions} from "../../../types"; import type {PerseusRadioUserInput} from "../../../validation.types"; import type {UserEvent} from "@testing-library/user-event"; @@ -984,3 +988,32 @@ describe("scoring", () => { expect(renderer).toHaveBeenAnsweredIncorrectly(); }); }); + +describe("propsUpgrade", () => { + it("can upgrade from v0 to v1", () => { + const v0props = { + choices: [{content: "Choice 1"}, {content: "Choice 2"}], + }; + + const expected: PerseusRadioWidgetOptions = { + choices: [{content: "Choice 1"}, {content: "Choice 2"}], + hasNoneOfTheAbove: false, + }; + + const result: PerseusRadioWidgetOptions = + RadioWidgetExport.propUpgrades["1"](v0props); + + expect(result).toEqual(expected); + }); + + it("throws from noneOfTheAbove", () => { + const v0props = { + choices: [{content: "Choice 1"}, {content: "Choice 2"}], + noneOfTheAbove: true, + }; + + expect(() => RadioWidgetExport.propUpgrades["1"](v0props)).toThrow( + "radio widget v0 no longer supports auto noneOfTheAbove", + ); + }); +}); diff --git a/packages/perseus/src/widgets/radio/radio.ts b/packages/perseus/src/widgets/radio/radio.ts index 815320c5e5..528514e7c9 100644 --- a/packages/perseus/src/widgets/radio/radio.ts +++ b/packages/perseus/src/widgets/radio/radio.ts @@ -126,23 +126,19 @@ const transform = ( }; const propUpgrades = { - "1": (v0props: any): any => { - let choices; - let hasNoneOfTheAbove; - - if (!v0props.noneOfTheAbove) { - choices = v0props.choices; - hasNoneOfTheAbove = false; - } else { + "1": (v0props: any): PerseusRadioWidgetOptions => { + const {noneOfTheAbove, ...rest} = v0props; + + if (noneOfTheAbove) { throw new Error( "radio widget v0 no longer supports auto noneOfTheAbove", ); } - return _.extend(_.omit(v0props, "noneOfTheAbove"), { - choices: choices, - hasNoneOfTheAbove: hasNoneOfTheAbove, - }); + return { + ...rest, + hasNoneOfTheAbove: false, + }; }, } as const; From 8ec06f444d8f4559eda5c3dbf189e5183b1c5b42 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Wed, 27 Nov 2024 10:57:37 -0700 Subject: [PATCH 09/10] Inline version in expression widget parser (#1921) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Expression widget imports the KAS parser, which contains generated code with some syntax that `@swc-node/register` (which we use to run TypeScript from the command line) can't parse. The parse error was preventing the exhaustive tests for the parsers (packages/perseus/src/util/parse-perseus-json/exhaustive-test-tool/index.ts) from running. Since we can't change the generated code, the solution is to avoid importing the version number from the Expression widget, and hardcode it instead. I think this is okay, because the migration function that used the version number is called `migrateV0ToV1`, so we actually want to hardcode version 1 rather than taking the current version from the widget. Issue: none ## Test plan: Run the exhaustive test tool according to the instructions in the file. Author: benchristel Reviewers: jeremywiebe Required Reviewers: Approved By: jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1921 --- .changeset/seven-owls-explain.md | 5 +++++ .../parse-perseus-json/perseus-parsers/expression-widget.ts | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 .changeset/seven-owls-explain.md diff --git a/.changeset/seven-owls-explain.md b/.changeset/seven-owls-explain.md new file mode 100644 index 0000000000..6e1d852646 --- /dev/null +++ b/.changeset/seven-owls-explain.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Internal: Inline widget version into Expression widget parser. diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/expression-widget.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/expression-widget.ts index 3d181092fe..9bff1a6a46 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/expression-widget.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/expression-widget.ts @@ -1,4 +1,3 @@ -import ExpressionWidgetModule from "../../../widgets/expression/expression"; import { array, boolean, @@ -93,7 +92,7 @@ function migrateV0ToV1( const {options} = widget; return ctx.success({ ...widget, - version: ExpressionWidgetModule.version, + version: {major: 1, minor: 0}, options: { times: options.times, buttonSets: options.buttonSets, From 584153588be04c6deb7b5d76ed2b258d0f75a3e1 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Wed, 27 Nov 2024 10:42:06 -0800 Subject: [PATCH 10/10] Enable dependabot updates for WB/WS (and group them) (#1863) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: Juan and I were discussing keeping Perseus dependnecies up to date with webapp (especially WB and WS). This is a trial run to see if Dependabot will help us. It should open up a new PR any time WB or WS packages are released. And all WB or WS packages that received an update should be updated together in the same Perseus PR. Issue: "none" ## Test plan: I suspect I'll need to land this and monitor it to test. Github action infra is notoriously difficult to test locally. Author: jeremywiebe Reviewers: jandrade, jeremywiebe, #perseus Required Reviewers: Approved By: jandrade Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1863 --- .changeset/mighty-rules-talk.md | 5 +++++ .eslintrc.js | 5 +++++ .github/dependabot.yml | 12 ++++++++++++ .../components/__stories__/device-framer.stories.tsx | 5 ++--- .../perseus-editor/src/components/widget-editor.tsx | 4 ++-- 5 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 .changeset/mighty-rules-talk.md diff --git a/.changeset/mighty-rules-talk.md b/.changeset/mighty-rules-talk.md new file mode 100644 index 0000000000..7946251c38 --- /dev/null +++ b/.changeset/mighty-rules-talk.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus-editor": patch +--- + +Switch two corner usages of deprecated @khanacademy/wonder-blocks-spacing to @khanacademy/wonder-blocks-tokens diff --git a/.eslintrc.js b/.eslintrc.js index aee5d9fe7d..c7cadf436c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -172,6 +172,11 @@ module.exports = { "no-invalid-this": "off", "@typescript-eslint/no-this-alias": "off", "no-unused-expressions": "off", + "no-restricted-imports": [ + "error", + "@khanacademy/wonder-blocks-color", + "@khanacademy/wonder-blocks-spacing", + ], "object-curly-spacing": "off", semi: "off", diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2be174af9d..126d6a8650 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,3 +10,15 @@ updates: allow: - dependency-name: "@khanacademy/eslint-config" - dependency-name: "@khanacademy/eslint-plugin" + assignees: + - "@Khan/perseus" + + # Grouped updates for Wonder Blocks and Wonder Stuff releases. + # This helps us to stay in sync with the latest releases of these packages. + groups: + wonder-stuff: + patterns: + - "@khanacademy/wonder-stuff-*" + wonder-blocks: + patterns: + - "@khanacademy/wonder-blocks-*" diff --git a/packages/perseus-editor/src/components/__stories__/device-framer.stories.tsx b/packages/perseus-editor/src/components/__stories__/device-framer.stories.tsx index 79ae5e8f18..a6f9f06cfc 100644 --- a/packages/perseus-editor/src/components/__stories__/device-framer.stories.tsx +++ b/packages/perseus-editor/src/components/__stories__/device-framer.stories.tsx @@ -1,5 +1,4 @@ -import Spacing from "@khanacademy/wonder-blocks-spacing"; -import {color} from "@khanacademy/wonder-blocks-tokens"; +import {color, spacing} from "@khanacademy/wonder-blocks-tokens"; import DeviceFramer from "../device-framer"; @@ -21,7 +20,7 @@ const SampleContent = () => { color: color.offWhite, width: "90%", height: "300px", - padding: Spacing.medium_16, + padding: spacing.medium_16, }} > The DeviceFramer controls the size of the content inside the frame. diff --git a/packages/perseus-editor/src/components/widget-editor.tsx b/packages/perseus-editor/src/components/widget-editor.tsx index 5b21deadd2..7c406660a0 100644 --- a/packages/perseus-editor/src/components/widget-editor.tsx +++ b/packages/perseus-editor/src/components/widget-editor.tsx @@ -8,8 +8,8 @@ import { } from "@khanacademy/perseus"; import {useUniqueIdWithMock} from "@khanacademy/wonder-blocks-core"; import {Strut} from "@khanacademy/wonder-blocks-layout"; -import Spacing from "@khanacademy/wonder-blocks-spacing"; import Switch from "@khanacademy/wonder-blocks-switch"; +import {spacing} from "@khanacademy/wonder-blocks-tokens"; import * as React from "react"; import _ from "underscore"; @@ -241,7 +241,7 @@ function LabeledSwitch(props: { return ( <> - + );