diff --git a/.changeset/hot-cougars-laugh.md b/.changeset/hot-cougars-laugh.md new file mode 100644 index 0000000000..979314f7c9 --- /dev/null +++ b/.changeset/hot-cougars-laugh.md @@ -0,0 +1,6 @@ +--- +"@khanacademy/perseus": minor +"@khanacademy/perseus-core": minor +--- + +Enable parsePerseusItem to parse all published content, upgrading old formats to the current one. diff --git a/packages/perseus-core/src/data-schema.ts b/packages/perseus-core/src/data-schema.ts index 62d7162177..b84a34ae4d 100644 --- a/packages/perseus-core/src/data-schema.ts +++ b/packages/perseus-core/src/data-schema.ts @@ -550,11 +550,9 @@ export type GraphRange = [ export type GrapherAnswerTypes = | { type: "absolute_value"; - coords: [ - // The vertex - Coord, // A point along one line of the absolute value "V" lines - Coord, - ]; + // If `coords` is null, the graph will not be gradable. All answers + // will be scored as invalid. + coords: null | [vertex: Coord, secondPoint: Coord]; } | { type: "exponential"; @@ -563,12 +561,16 @@ export type GrapherAnswerTypes = asymptote: [Coord, Coord]; // Two points along the exponential curve. One end of the curve // trends towards the asymptote. - coords: [Coord, Coord]; + // If `coords` is null, the graph will not be gradable. All answers + // will be scored as invalid. + coords: null | [Coord, Coord]; } | { type: "linear"; // Two points along the straight line - coords: [Coord, Coord]; + // If coords is null, the graph will not be gradable. All answers + // will be scored as invalid. + coords: null | [Coord, Coord]; } | { type: "logarithm"; @@ -576,25 +578,29 @@ export type GrapherAnswerTypes = asymptote: [Coord, Coord]; // Two points along the logarithmic curve. One end of the curve // trends towards the asymptote. - coords: [Coord, Coord]; + // If coords is null, the graph will not be gradable. All answers + // will be scored as invalid. + coords: null | [Coord, Coord]; } | { type: "quadratic"; - coords: [ - // The vertex of the parabola - Coord, // A point along the parabola - Coord, - ]; + // If coords is null, the graph will not be gradable. All answers + // will be scored as invalid. + coords: null | [vertex: Coord, secondPoint: Coord]; } | { type: "sinusoid"; // Two points on the same slope in the sinusoid wave line. - coords: [Coord, Coord]; + // If coords is null, the graph will not be gradable. All answers + // will be scored as invalid. + coords: null | [Coord, Coord]; } | { type: "tangent"; // Two points on the same slope in the tangent wave line. - coords: [Coord, Coord]; + // If coords is null, the graph will not be gradable. All answers + // will be scored as invalid. + coords: null | [Coord, Coord]; }; export type PerseusGrapherWidgetOptions = { @@ -1615,9 +1621,6 @@ export type PerseusCSProgramWidgetOptions = { showEditor: boolean; // Whether to show the execute buttons showButtons: boolean; - // TODO(benchristel): width is not used. Delete it? - // The width of the widget - width: number; // The height of the widget height: number; // TODO(benchristel): static is not used. Delete it? @@ -1643,7 +1646,7 @@ export type PerseusIFrameWidgetOptions = { // A URL to display OR a CS Program ID url: string; // Settings that you add here are available to the program as an object returned by Program.settings() - settings: ReadonlyArray; + settings?: ReadonlyArray; // The width of the widget width: number | string; // The height of the widget diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/cs-program-widget.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/cs-program-widget.ts index 2d0eece4ae..7421464ef7 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/cs-program-widget.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/cs-program-widget.ts @@ -22,7 +22,6 @@ export const parseCSProgramWidget: Parser = parseWidget( settings: array(object({name: string, value: string})), showEditor: boolean, showButtons: boolean, - width: number, height: number, static: defaulted(boolean, () => false), }), diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/grapher-widget.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/grapher-widget.ts index 01b5256b03..5f8e0fdedd 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/grapher-widget.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/grapher-widget.ts @@ -11,7 +11,6 @@ import { string, union, } from "../general-purpose-parsers"; -import {defaulted} from "../general-purpose-parsers/defaulted"; import {discriminatedUnionOn} from "../general-purpose-parsers/discriminated-union"; import {parseWidget} from "./widget"; @@ -42,7 +41,7 @@ export const parseGrapherWidget: Parser = parseWidget( "absolute_value", object({ type: constant("absolute_value"), - coords: pairOfPoints, + coords: nullable(pairOfPoints), }), ) .withBranch( @@ -50,21 +49,14 @@ export const parseGrapherWidget: Parser = parseWidget( object({ type: constant("exponential"), asymptote: pairOfPoints, - coords: pairOfPoints, + coords: nullable(pairOfPoints), }), ) .withBranch( "linear", object({ type: constant("linear"), - coords: defaulted( - pairOfPoints, - () => - [ - [-5, 5], - [5, 5], - ] as [[number, number], [number, number]], - ), + coords: nullable(pairOfPoints), }), ) .withBranch( @@ -72,28 +64,28 @@ export const parseGrapherWidget: Parser = parseWidget( object({ type: constant("logarithm"), asymptote: pairOfPoints, - coords: pairOfPoints, + coords: nullable(pairOfPoints), }), ) .withBranch( "quadratic", object({ type: constant("quadratic"), - coords: pairOfPoints, + coords: nullable(pairOfPoints), }), ) .withBranch( "sinusoid", object({ type: constant("sinusoid"), - coords: pairOfPoints, + coords: nullable(pairOfPoints), }), ) .withBranch( "tangent", object({ type: constant("tangent"), - coords: pairOfPoints, + coords: nullable(pairOfPoints), }), ).parser, graph: object({ 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 55625c11f1..909f0c2c5a 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 @@ -19,10 +19,10 @@ export const parseIframeWidget: Parser = parseWidget( constant("iframe"), object({ url: string, - settings: array(object({name: string, value: string})), + settings: optional(array(object({name: string, value: string}))), width: union(number).or(string).parser, height: union(number).or(string).parser, - allowFullScreen: boolean, + allowFullScreen: defaulted(boolean, () => false), allowTopNavigation: optional(boolean), static: defaulted(boolean, () => false), }), diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/interaction-widget.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/interaction-widget.ts index b96c589727..0264398da7 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/interaction-widget.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/interaction-widget.ts @@ -7,9 +7,11 @@ import { object, optional, pair, + pipeParsers, string, union, } from "../general-purpose-parsers"; +import {convert} from "../general-purpose-parsers/convert"; import {defaulted} from "../general-purpose-parsers/defaulted"; import {discriminatedUnionOn} from "../general-purpose-parsers/discriminated-union"; @@ -25,11 +27,12 @@ import type { const pairOfNumbers = pair(number, number); const stringOrEmpty = defaulted(string, () => ""); +const parseKey = pipeParsers(optional(string)).then(convert(String)).parser; + type FunctionElement = Extract; -const parseFunctionType = constant("function"); const parseFunctionElement: Parser = object({ - type: parseFunctionType, - key: string, + type: constant("function"), + key: parseKey, options: object({ value: string, funcName: string, @@ -42,10 +45,9 @@ const parseFunctionElement: Parser = object({ }); type LabelElement = Extract; -const parseLabelType = constant("label"); const parseLabelElement: Parser = object({ - type: parseLabelType, - key: string, + type: constant("label"), + key: parseKey, options: object({ label: string, color: string, @@ -55,10 +57,9 @@ const parseLabelElement: Parser = object({ }); type LineElement = Extract; -const parseLineType = constant("line"); const parseLineElement: Parser = object({ - type: parseLineType, - key: string, + type: constant("line"), + key: parseKey, options: object({ color: string, startX: string, @@ -75,10 +76,9 @@ type MovableLineElement = Extract< PerseusInteractionElement, {type: "movable-line"} >; -const parseMovableLineType = constant("movable-line"); const parseMovableLineElement: Parser = object({ - type: parseMovableLineType, - key: string, + type: constant("movable-line"), + key: parseKey, options: object({ startX: string, startY: string, @@ -100,10 +100,9 @@ type MovablePointElement = Extract< PerseusInteractionElement, {type: "movable-point"} >; -const parseMovablePointType = constant("movable-point"); const parseMovablePointElement: Parser = object({ - type: parseMovablePointType, - key: string, + type: constant("movable-point"), + key: parseKey, options: object({ startX: string, startY: string, @@ -122,10 +121,9 @@ type ParametricElement = Extract< PerseusInteractionElement, {type: "parametric"} >; -const parseParametricType = constant("parametric"); const parseParametricElement: Parser = object({ - type: parseParametricType, - key: string, + type: constant("parametric"), + key: parseKey, options: object({ x: string, y: string, @@ -138,10 +136,9 @@ const parseParametricElement: Parser = object({ }); type PointElement = Extract; -const parsePointType = constant("point"); const parsePointElement: Parser = object({ - type: parsePointType, - key: string, + type: constant("point"), + key: parseKey, options: object({ color: string, coordX: string, @@ -150,10 +147,9 @@ const parsePointElement: Parser = object({ }); type RectangleElement = Extract; -const parseRectangleType = constant("rectangle"); const parseRectangleElement: Parser = object({ - type: parseRectangleType, - key: string, + type: constant("rectangle"), + key: parseKey, options: object({ color: string, coordX: string, diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/interactive-graph-widget.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/interactive-graph-widget.ts index a71209ed37..0de2d6f30c 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/interactive-graph-widget.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/interactive-graph-widget.ts @@ -213,8 +213,8 @@ const parseLockedLineType: Parser = object({ points: pair(parseLockedPointType, parseLockedPointType), color: parseLockedFigureColor, lineStyle: parseLockedLineStyle, - showPoint1: boolean, - showPoint2: boolean, + showPoint1: defaulted(boolean, () => false), + showPoint2: defaulted(boolean, () => false), // TODO(benchristel): default labels to empty array? labels: optional(array(parseLockedLabelType)), ariaLabel: optional(string), @@ -266,13 +266,14 @@ const parseLockedFunctionType: Parser = object({ ariaLabel: optional(string), }); -const parseLockedFigure: Parser = union(parseLockedPointType) - .or(parseLockedLineType) - .or(parseLockedVectorType) - .or(parseLockedEllipseType) - .or(parseLockedPolygonType) - .or(parseLockedFunctionType) - .or(parseLockedLabelType).parser; +const parseLockedFigure: Parser = discriminatedUnionOn("type") + .withBranch("point", parseLockedPointType) + .withBranch("line", parseLockedLineType) + .withBranch("vector", parseLockedVectorType) + .withBranch("ellipse", parseLockedEllipseType) + .withBranch("polygon", parseLockedPolygonType) + .withBranch("function", parseLockedFunctionType) + .withBranch("label", parseLockedLabelType).parser; export const parseInteractiveGraphWidget: Parser = parseWidget( diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/measurer-widget.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/measurer-widget.ts index e3a50e5572..85d2c63900 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/measurer-widget.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/measurer-widget.ts @@ -17,7 +17,14 @@ import type {MeasurerWidget} from "@khanacademy/perseus-core"; export const parseMeasurerWidget: Parser = parseWidget( constant("measurer"), object({ - image: parsePerseusImageBackground, + // The default value for image comes from measurer.tsx. + // See parse-perseus-json/README.md for why we want to duplicate the + // defaults here. + image: defaulted(parsePerseusImageBackground, () => ({ + url: null, + top: 0, + left: 0, + })), showProtractor: boolean, showRuler: boolean, rulerLabel: string, diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/plotter-widget.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/plotter-widget.ts index f56397d51b..24ea3ef0d7 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/plotter-widget.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/plotter-widget.ts @@ -24,9 +24,15 @@ export const parsePlotterWidget: Parser = parseWidget( categories: array(string), type: enumeration(...plotterPlotTypes), maxY: number, - scaleY: number, + // The default value for scaleY comes from plotter.tsx. + // See parse-perseus-json/README.md for why we want to duplicate the + // defaults here. + scaleY: defaulted(number, () => 1), labelInterval: optional(nullable(number)), - snapsPerLine: number, + // The default value for snapsPerLine comes from plotter.tsx. + // See parse-perseus-json/README.md for why we want to duplicate the + // defaults here. + snapsPerLine: defaulted(number, () => 2), starting: array(number), correct: array(number), picUrl: optional(nullable(string)), 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 2bfa98e088..693ce3b280 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 @@ -31,7 +31,62 @@ describe("parseWidgetsMap", () => { const result = parse(widgetsMap, parseWidgetsMap); - expect(result).toEqual(anyFailure); + expect(result).toEqual( + failure( + `At (root).asdf["(widget ID)"] -- expected array of length 2, but got ["asdf"]`, + ), + ); + }); + + it("rejects a widget ID numbered 0", () => { + // Widget IDs with 0 currently cause a full-page crash when the + // exercise is rendered in webapp! + + const widgetsMap: unknown = { + "radio 0": { + type: "radio", + version: {major: 0, minor: 0}, + options: { + choices: [], + noneOfTheAbove: false, + }, + }, + }; + + const result = parse(widgetsMap, parseWidgetsMap); + expect(result).toEqual( + failure( + `At (root)["radio 0"]["(widget ID)"][1] -- expected a string representing a positive integer, but got "0"`, + ), + ); + }); + + it("rejects a widget ID with no number", () => { + const widgetsMap: unknown = { + categorizer: {type: "categorizer"}, + }; + + const result = parse(widgetsMap, parseWidgetsMap); + + expect(result).toEqual( + failure( + `At (root).categorizer["(widget ID)"] -- expected array of length 2, but got ["categorizer"]`, + ), + ); + }); + + it("rejects an unknown widget type", () => { + const widgetsMap: unknown = { + "transmogrifier 1": {type: "transmogrifier"}, + }; + + const result = parse(widgetsMap, parseWidgetsMap); + + expect(result).toEqual( + failure( + `At (root)["transmogrifier 1"] -- expected a valid widget type, but got "transmogrifier"`, + ), + ); }); it("accepts a categorizer widget", () => { @@ -731,20 +786,6 @@ describe("parseWidgetsMap", () => { expect(result).toEqual(success(expected)); }); - it("rejects an unknown widget type", () => { - const widgetsMap: unknown = { - "transmogrifier 1": {type: "transmogrifier"}, - }; - - const result = parse(widgetsMap, parseWidgetsMap); - - expect(result).toEqual( - failure( - `At (root)["transmogrifier 1"] -- expected a valid widget type, but got "transmogrifier"`, - ), - ); - }); - it("accepts a dynamically-registered widget type without checking its options", () => { registerWidget("fake-widget-for-widgets-map-parser-test", { name: "fake-widget-for-widgets-map-parser-test", @@ -763,14 +804,4 @@ describe("parseWidgetsMap", () => { expect(result).toEqual(success(widgetsMap)); }); - - it("rejects a key with no ID", () => { - const widgetsMap: unknown = { - categorizer: {type: "categorizer"}, - }; - - const result = parse(widgetsMap, parseWidgetsMap); - - expect(result).toEqual(anyFailure); - }); }); diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widgets-map.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widgets-map.ts index def3756dbd..565a2d522b 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widgets-map.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widgets-map.ts @@ -69,15 +69,15 @@ const parseWidgetsMapEntry: ( entry: [string, unknown], widgetMap: PerseusWidgetsMap, ctx: ParseContext, -) => ParseResult = ([key, widget], widgetMap, ctx) => { - const keyComponentsResult = parseWidgetMapKeyComponents( - key.split(" "), - ctx, +) => ParseResult = ([id, widget], widgetMap, ctx) => { + const idComponentsResult = parseWidgetIdComponents( + id.split(" "), + ctx.forSubtree("(widget ID)"), ); - if (isFailure(keyComponentsResult)) { - return keyComponentsResult; + if (isFailure(idComponentsResult)) { + return idComponentsResult; } - const [type, id] = keyComponentsResult.value; + const [type, n] = idComponentsResult.value; function parseAndAssign( key: K, @@ -93,107 +93,107 @@ const parseWidgetsMapEntry: ( switch (type) { case "categorizer": - return parseAndAssign(`categorizer ${id}`, parseCategorizerWidget); + return parseAndAssign(`categorizer ${n}`, parseCategorizerWidget); case "cs-program": - return parseAndAssign(`cs-program ${id}`, parseCSProgramWidget); + return parseAndAssign(`cs-program ${n}`, parseCSProgramWidget); case "definition": - return parseAndAssign(`definition ${id}`, parseDefinitionWidget); + return parseAndAssign(`definition ${n}`, parseDefinitionWidget); case "dropdown": - return parseAndAssign(`dropdown ${id}`, parseDropdownWidget); + return parseAndAssign(`dropdown ${n}`, parseDropdownWidget); case "explanation": - return parseAndAssign(`explanation ${id}`, parseExplanationWidget); + return parseAndAssign(`explanation ${n}`, parseExplanationWidget); case "expression": - return parseAndAssign(`expression ${id}`, parseExpressionWidget); + return parseAndAssign(`expression ${n}`, parseExpressionWidget); case "grapher": - return parseAndAssign(`grapher ${id}`, parseGrapherWidget); + return parseAndAssign(`grapher ${n}`, parseGrapherWidget); case "group": - return parseAndAssign(`group ${id}`, parseGroupWidget); + return parseAndAssign(`group ${n}`, parseGroupWidget); case "graded-group": - return parseAndAssign(`graded-group ${id}`, parseGradedGroupWidget); + return parseAndAssign(`graded-group ${n}`, parseGradedGroupWidget); case "graded-group-set": return parseAndAssign( - `graded-group-set ${id}`, + `graded-group-set ${n}`, parseGradedGroupSetWidget, ); case "iframe": - return parseAndAssign(`iframe ${id}`, parseIframeWidget); + return parseAndAssign(`iframe ${n}`, parseIframeWidget); case "image": - return parseAndAssign(`image ${id}`, parseImageWidget); + return parseAndAssign(`image ${n}`, parseImageWidget); case "input-number": - return parseAndAssign(`input-number ${id}`, parseInputNumberWidget); + return parseAndAssign(`input-number ${n}`, parseInputNumberWidget); case "interaction": - return parseAndAssign(`interaction ${id}`, parseInteractionWidget); + return parseAndAssign(`interaction ${n}`, parseInteractionWidget); case "interactive-graph": return parseAndAssign( - `interactive-graph ${id}`, + `interactive-graph ${n}`, parseInteractiveGraphWidget, ); case "label-image": - return parseAndAssign(`label-image ${id}`, parseLabelImageWidget); + return parseAndAssign(`label-image ${n}`, parseLabelImageWidget); case "matcher": - return parseAndAssign(`matcher ${id}`, parseMatcherWidget); + return parseAndAssign(`matcher ${n}`, parseMatcherWidget); case "matrix": - return parseAndAssign(`matrix ${id}`, parseMatrixWidget); + return parseAndAssign(`matrix ${n}`, parseMatrixWidget); case "measurer": - return parseAndAssign(`measurer ${id}`, parseMeasurerWidget); + return parseAndAssign(`measurer ${n}`, parseMeasurerWidget); case "molecule-renderer": return parseAndAssign( - `molecule-renderer ${id}`, + `molecule-renderer ${n}`, parseMoleculeRendererWidget, ); case "number-line": - return parseAndAssign(`number-line ${id}`, parseNumberLineWidget); + return parseAndAssign(`number-line ${n}`, parseNumberLineWidget); case "numeric-input": return parseAndAssign( - `numeric-input ${id}`, + `numeric-input ${n}`, parseNumericInputWidget, ); case "orderer": - return parseAndAssign(`orderer ${id}`, parseOrdererWidget); + return parseAndAssign(`orderer ${n}`, parseOrdererWidget); case "passage": - return parseAndAssign(`passage ${id}`, parsePassageWidget); + return parseAndAssign(`passage ${n}`, parsePassageWidget); case "passage-ref": - return parseAndAssign(`passage-ref ${id}`, parsePassageRefWidget); + return parseAndAssign(`passage-ref ${n}`, parsePassageRefWidget); case "passage-ref-target": // NOTE(benchristel): as of 2024-11-12, passage-ref-target is only // used in test content. See: // https://www.khanacademy.org/devadmin/content/search?query=widget:passage-ref-target - return parseAndAssign(`passage-ref-target ${id}`, any); + return parseAndAssign(`passage-ref-target ${n}`, any); case "phet-simulation": return parseAndAssign( - `phet-simulation ${id}`, + `phet-simulation ${n}`, parsePhetSimulationWidget, ); case "plotter": - return parseAndAssign(`plotter ${id}`, parsePlotterWidget); + return parseAndAssign(`plotter ${n}`, parsePlotterWidget); case "python-program": return parseAndAssign( - `python-program ${id}`, + `python-program ${n}`, parsePythonProgramWidget, ); case "radio": - return parseAndAssign(`radio ${id}`, parseRadioWidget); + return parseAndAssign(`radio ${n}`, parseRadioWidget); case "sorter": - return parseAndAssign(`sorter ${id}`, parseSorterWidget); + return parseAndAssign(`sorter ${n}`, parseSorterWidget); case "table": - return parseAndAssign(`table ${id}`, parseTableWidget); + return parseAndAssign(`table ${n}`, parseTableWidget); case "video": - return parseAndAssign(`video ${id}`, parseVideoWidget); + return parseAndAssign(`video ${n}`, parseVideoWidget); case "sequence": // sequence is a deprecated widget type, and the corresponding // widget component no longer exists. - return parseAndAssign(`sequence ${id}`, parseDeprecatedWidget); + return parseAndAssign(`sequence ${n}`, parseDeprecatedWidget); case "lights-puzzle": - return parseAndAssign(`lights-puzzle ${id}`, parseDeprecatedWidget); + return parseAndAssign(`lights-puzzle ${n}`, parseDeprecatedWidget); case "simulator": - return parseAndAssign(`simulator ${id}`, parseDeprecatedWidget); + return parseAndAssign(`simulator ${n}`, parseDeprecatedWidget); case "transformer": - return parseAndAssign(`transformer ${id}`, parseDeprecatedWidget); + return parseAndAssign(`transformer ${n}`, parseDeprecatedWidget); default: if (getWidget(type)) { // @ts-expect-error - 'type' is not a valid widget type - return parseAndAssign(`${type} ${id}`, any); + return parseAndAssign(`${type} ${n}`, any); } return ctx.failure("a valid widget type", type); } @@ -208,9 +208,12 @@ const parseDeprecatedWidget: Parser = parseWidget( const parseStringToPositiveInt: Parser = (rawValue, ctx) => { if (typeof rawValue !== "string" || !/^[1-9][0-9]*$/.test(rawValue)) { - return ctx.failure("numeric string", rawValue); + return ctx.failure( + "a string representing a positive integer", + rawValue, + ); } return ctx.success(+rawValue); }; -const parseWidgetMapKeyComponents = pair(string, parseStringToPositiveInt); +const parseWidgetIdComponents = pair(string, parseStringToPositiveInt); 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 4f6092c9c4..d0cc4ff18d 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 @@ -272,6 +272,40 @@ exports[`parseAndTypecheckPerseusItem correctly parses data/cs-program-missing-s } `; +exports[`parseAndTypecheckPerseusItem correctly parses data/cs-program-with-null-width.json 1`] = ` +{ + "answer": undefined, + "answerArea": {}, + "hints": [], + "itemDataVersion": undefined, + "question": { + "content": "[[☃ cs-program 1]]", + "images": {}, + "metadata": undefined, + "widgets": { + "cs-program 1": { + "alignment": "block", + "graded": undefined, + "key": undefined, + "options": { + "height": 250, + "programID": "4545417404481536", + "programType": undefined, + "settings": [], + "showButtons": true, + "showEditor": true, + "static": false, + "width": null, + }, + "static": undefined, + "type": "cs-program", + "version": undefined, + }, + }, + }, +} +`; + exports[`parseAndTypecheckPerseusItem correctly parses data/definition-missing-static.json 1`] = ` { "answer": undefined, @@ -1292,16 +1326,7 @@ exports[`parseAndTypecheckPerseusItem correctly parses data/grapher-with-null-co ], "correct": { "asymptote": null, - "coords": [ - [ - -5, - 5, - ], - [ - 5, - 5, - ], - ], + "coords": null, "type": "linear", }, "graph": { @@ -1680,6 +1705,151 @@ In case you would like a fuller experience, here is a taste of a skill you can l } `; +exports[`parseAndTypecheckPerseusItem correctly parses data/iframe-missing-allowFullScreen.json 1`] = ` +{ + "answer": undefined, + "answerArea": { + "calculator": false, + }, + "hints": [ + { + "content": "This is the easy step. Just drag disk 3 over to peg "B". + +[[☃ image 1]]", + "images": {}, + "metadata": undefined, + "replace": undefined, + "widgets": { + "image 1": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "alt": undefined, + "backgroundImage": { + "bottom": undefined, + "height": 215, + "left": undefined, + "scale": undefined, + "top": undefined, + "url": "https://s3.amazonaws.com/ka-cs-algorithms/hanoi_exercise_step2_1.png", + "width": 304, + }, + "box": [ + 304, + 215, + ], + "caption": undefined, + "labels": [], + "range": [ + [ + 0, + 10, + ], + [ + 0, + 10, + ], + ], + "static": undefined, + "title": undefined, + }, + "static": undefined, + "type": "image", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, + ], + "itemDataVersion": { + "major": 0, + "minor": 1, + }, + "question": { + "content": "Congratulations, you have exposed disk 3, and since our goal is move 3 disks to peg "B", that's the disk we want on the bottom of peg "B". Move it to the target peg now. + +[[☃ iframe 1]]", + "images": {}, + "metadata": undefined, + "widgets": { + "iframe 1": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "allowFullScreen": false, + "allowTopNavigation": undefined, + "height": "400", + "settings": [ + { + "name": "step", + "value": "2", + }, + { + "name": "disk1", + "value": "2", + }, + { + "name": "disk2", + "value": "2", + }, + { + "name": "", + "value": "", + }, + ], + "static": false, + "url": "4772835774169088", + "width": 400, + }, + "static": undefined, + "type": "iframe", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, +} +`; + +exports[`parseAndTypecheckPerseusItem correctly parses data/iframe-missing-settings.json 1`] = ` +{ + "answer": undefined, + "answerArea": {}, + "hints": [], + "itemDataVersion": undefined, + "question": { + "content": "[[☃ iframe 1]]", + "images": {}, + "metadata": undefined, + "widgets": { + "iframe 1": { + "alignment": "block", + "graded": undefined, + "key": undefined, + "options": { + "allowFullScreen": false, + "allowTopNavigation": undefined, + "height": "550px", + "settings": undefined, + "static": false, + "url": "https://learnstorm.typeform.com/to/fnQ2tw?", + "width": "100%", + }, + "static": undefined, + "type": "iframe", + "version": undefined, + }, + }, + }, +} +`; + exports[`parseAndTypecheckPerseusItem correctly parses data/iframe-missing-static.json 1`] = ` { "answer": undefined, @@ -2099,153 +2269,1637 @@ exports[`parseAndTypecheckPerseusItem correctly parses data/interaction-element- } `; -exports[`parseAndTypecheckPerseusItem correctly parses data/interactive-graph-backgroundImage-with-empty-string-coordinates.json 1`] = ` +exports[`parseAndTypecheckPerseusItem correctly parses data/interaction-element-missing-key.json 1`] = ` { "answer": undefined, - "answerArea": { - "calculator": false, - "periodicTable": false, - }, - "hints": [ - { - "content": "We can plot the points using the equation to find $d$ for each value of $w$. - -If $w=\\blue 6$, + "answerArea": {}, + "hints": [], + "itemDataVersion": undefined, + "question": { + "content": "# Functions introduction -$\\qquad d=\\blue 6+5\\\\~~~~~~~~~~=\\red{11}.$ +A function is something that maps one value to another. -So we place one point at $(\\blue 6,\\red{11})$.", - "images": {}, - "metadata": undefined, - "replace": undefined, - "widgets": {}, - }, - { - "content": "If $w=\\blue{10}$, +Here is a function that maps an $\\orange\\text{input dot}$ on the top to an $\\blue\\text{output dot}$ on the bottom. Try dragging the $\\orange\\text{input dot}$ on the left and see what $\\blue\\text{output}$ the function maps it to below: -$\\qquad d=\\blue{10}+5=\\red{15}$. +[[☃ interaction 1]] -So the second point is at $(\\blue{10},\\red{15})$.", - "images": {}, - "metadata": undefined, - "replace": undefined, - "widgets": {}, - }, - { - "content": "The graph should look like this: +Not all functions are quite so simple! For example, there is no rule that the $\\blue\\text{output}$ has to increase when the $\\orange\\text{input}$ increases: -![](https://ka-perseus-graphie.s3.amazonaws.com/d29e6802062c091aac9761cf34e41438104bd6a2.png)", - "images": { - "https://ka-perseus-graphie.s3.amazonaws.com/d29e6802062c091aac9761cf34e41438104bd6a2.png": { - "height": 425, - "width": 425, - }, - }, - "metadata": undefined, - "replace": undefined, - "widgets": {}, - }, - ], - "itemDataVersion": { - "major": 0, - "minor": 1, - }, - "question": { - "content": "You are $5$ miles away from your house when you start walking directly away from your house. In the table below, $w$ represents the number of miles you have walked, and $d$ represents your distance from home in miles. +[[☃ interaction 2]] -The relationship between these two variables can be expressed by the following equation: +There is also no rule that a function has to map to a different value for each different input value: -$d=w+5.$ +[[☃ interaction 3]] -**Plot two points on the graph that show your distance from home if you walked $6$ miles and $10$ miles.** +Or that it even has to ever map to a different value at all! -$w$ | $d$ -:-:|:-: -$0$ | $5$ -$1$ | $6$ -$2$ | $7$ -$3$ | $8$ +[[☃ interaction 4]] +But that's sort of unsatisfying! so here's another function that demonstrates all of those concepts: +[[☃ interaction 5]] -[[☃ interactive-graph 1]]", - "images": {}, +Next, we'll look at some other representations of functions!", + "images": { + "https://ka-perseus-graphie.s3.amazonaws.com/b59fc02ca1aae800977b8793ed22f647a1aa75ee.png": { + "height": 150, + "width": 425, + }, + "https://ka-perseus-graphie.s3.amazonaws.com/da8df81c78b22f5c69d477d8eabfb583968eaf84.png": { + "height": 70, + "width": 400, + }, + }, "metadata": undefined, "widgets": { - "interactive-graph 1": { - "alignment": "default", + "interaction 1": { + "alignment": undefined, "graded": true, "key": undefined, "options": { - "backgroundImage": { - "bottom": 0, - "height": 0, - "left": 0, - "scale": 1, - "top": undefined, - "url": null, - "width": 0, - }, - "correct": { - "coord": undefined, - "coords": [ - [ - 6, - 11, + "elements": [ + { + "key": "undefined", + "options": { + "constraint": "snap", + "constraintFn": "-3", + "constraintXMax": "8", + "constraintXMin": "1", + "constraintYMax": "3", + "constraintYMin": "3", + "snap": 1, + "startX": "5", + "startY": "3", + "varSubscript": 0, + }, + "type": "movable-point", + }, + { + "key": "undefined", + "options": { + "color": "#6495ED", + "coordX": "x_0+1", + "coordY": "-3", + }, + "type": "point", + }, + ], + "graph": { + "backgroundImage": { + "bottom": 0, + "height": undefined, + "left": 0, + "scale": 1, + "top": undefined, + "url": null, + "width": undefined, + }, + "box": [ + 400, + 200, + ], + "editableSettings": [ + "canvas", + "graph", + ], + "gridStep": [ + 1, + 3, + ], + "labels": [ + "", + "", + ], + "markings": "graph", + "range": [ + [ + 0, + 10, + ], + [ + -6, + 6, + ], + ], + "rulerLabel": "", + "rulerTicks": 10, + "scale": [ + 40, + 16.666666666666668, + ], + "showProtractor": false, + "showRuler": false, + "snapStep": [ + 0.5, + 1.5, + ], + "tickStep": [ + 1, + 2, + ], + "valid": true, + }, + "static": false, + }, + "static": undefined, + "type": "interaction", + "version": { + "major": 0, + "minor": 0, + }, + }, + "interaction 2": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "elements": [ + { + "key": "undefined", + "options": { + "constraint": "snap", + "constraintFn": "-3", + "constraintXMax": "9", + "constraintXMin": "1", + "constraintYMax": "3", + "constraintYMin": "3", + "snap": 1, + "startX": "5", + "startY": "3", + "varSubscript": 0, + }, + "type": "movable-point", + }, + { + "key": "undefined", + "options": { + "color": "#6495ED", + "coordX": "10-x_0", + "coordY": "-3", + }, + "type": "point", + }, + ], + "graph": { + "backgroundImage": { + "bottom": 0, + "height": undefined, + "left": 0, + "scale": 1, + "top": undefined, + "url": null, + "width": undefined, + }, + "box": [ + 400, + 200, + ], + "editableSettings": [ + "canvas", + "graph", + ], + "gridStep": [ + 1, + 3, + ], + "labels": [ + "", + "", + ], + "markings": "graph", + "range": [ + [ + 0, + 10, + ], + [ + -6, + 6, + ], + ], + "rulerLabel": "", + "rulerTicks": 10, + "scale": [ + 40, + 16.666666666666668, + ], + "showProtractor": false, + "showRuler": false, + "snapStep": [ + 0.5, + 1.5, + ], + "tickStep": [ + 1, + 2, + ], + "valid": true, + }, + "static": false, + }, + "static": undefined, + "type": "interaction", + "version": { + "major": 0, + "minor": 0, + }, + }, + "interaction 3": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "elements": [ + { + "key": "undefined", + "options": { + "constraint": "snap", + "constraintFn": "-3", + "constraintXMax": "9", + "constraintXMin": "1", + "constraintYMax": "3", + "constraintYMin": "3", + "snap": 1, + "startX": "5", + "startY": "3", + "varSubscript": 0, + }, + "type": "movable-point", + }, + { + "key": "undefined", + "options": { + "color": "#6495ED", + "coordX": "\\sin\\left(x_0\\cdot\\frac{\\pi}{2}\\right)+5", + "coordY": "-3", + }, + "type": "point", + }, + ], + "graph": { + "backgroundImage": { + "bottom": 0, + "height": undefined, + "left": 0, + "scale": 1, + "top": undefined, + "url": null, + "width": undefined, + }, + "box": [ + 400, + 200, + ], + "editableSettings": [ + "canvas", + "graph", + ], + "gridStep": [ + 1, + 3, + ], + "labels": [ + "", + "", + ], + "markings": "graph", + "range": [ + [ + 0, + 10, + ], + [ + -6, + 6, + ], + ], + "rulerLabel": "", + "rulerTicks": 10, + "scale": [ + 40, + 16.666666666666668, + ], + "showProtractor": false, + "showRuler": false, + "snapStep": [ + 0.5, + 1.5, + ], + "tickStep": [ + 1, + 2, + ], + "valid": true, + }, + "static": false, + }, + "static": undefined, + "type": "interaction", + "version": { + "major": 0, + "minor": 0, + }, + }, + "interaction 4": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "elements": [ + { + "key": "undefined", + "options": { + "constraint": "snap", + "constraintFn": "-3", + "constraintXMax": "9", + "constraintXMin": "1", + "constraintYMax": "3", + "constraintYMin": "3", + "snap": 1, + "startX": "5", + "startY": "3", + "varSubscript": 0, + }, + "type": "movable-point", + }, + { + "key": "undefined", + "options": { + "color": "#6495ED", + "coordX": "4", + "coordY": "-3", + }, + "type": "point", + }, + ], + "graph": { + "backgroundImage": { + "bottom": 0, + "height": undefined, + "left": 0, + "scale": 1, + "top": undefined, + "url": null, + "width": undefined, + }, + "box": [ + 400, + 200, + ], + "editableSettings": [ + "canvas", + "graph", + ], + "gridStep": [ + 1, + 3, + ], + "labels": [ + "", + "", + ], + "markings": "graph", + "range": [ + [ + 0, + 10, + ], + [ + -6, + 6, + ], + ], + "rulerLabel": "", + "rulerTicks": 10, + "scale": [ + 40, + 16.666666666666668, + ], + "showProtractor": false, + "showRuler": false, + "snapStep": [ + 0.5, + 1.5, + ], + "tickStep": [ + 1, + 2, + ], + "valid": true, + }, + "static": false, + }, + "static": undefined, + "type": "interaction", + "version": { + "major": 0, + "minor": 0, + }, + }, + "interaction 5": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "elements": [ + { + "key": "undefined", + "options": { + "constraint": "snap", + "constraintFn": "-3", + "constraintXMax": "9", + "constraintXMin": "1", + "constraintYMax": "3", + "constraintYMin": "3", + "snap": 1, + "startX": "5", + "startY": "3", + "varSubscript": 0, + }, + "type": "movable-point", + }, + { + "key": "undefined", + "options": { + "color": "#6495ED", + "coordX": "5-\\left|x_0-5\\right|", + "coordY": "-3", + }, + "type": "point", + }, + ], + "graph": { + "backgroundImage": { + "bottom": 0, + "height": undefined, + "left": 0, + "scale": 1, + "top": undefined, + "url": null, + "width": undefined, + }, + "box": [ + 400, + 200, + ], + "editableSettings": [ + "canvas", + "graph", + ], + "gridStep": [ + 1, + 3, + ], + "labels": [ + "", + "", + ], + "markings": "graph", + "range": [ + [ + 0, + 10, + ], + [ + -6, + 6, + ], + ], + "rulerLabel": "", + "rulerTicks": 10, + "scale": [ + 40, + 16.666666666666668, + ], + "showProtractor": false, + "showRuler": false, + "snapStep": [ + 0.5, + 1.5, + ], + "tickStep": [ + 1, + 2, + ], + "valid": true, + }, + "static": false, + }, + "static": undefined, + "type": "interaction", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, +} +`; + +exports[`parseAndTypecheckPerseusItem correctly parses data/interactive-graph-backgroundImage-with-empty-string-coordinates.json 1`] = ` +{ + "answer": undefined, + "answerArea": { + "calculator": false, + "periodicTable": false, + }, + "hints": [ + { + "content": "We can plot the points using the equation to find $d$ for each value of $w$. + +If $w=\\blue 6$, + +$\\qquad d=\\blue 6+5\\\\~~~~~~~~~~=\\red{11}.$ + +So we place one point at $(\\blue 6,\\red{11})$.", + "images": {}, + "metadata": undefined, + "replace": undefined, + "widgets": {}, + }, + { + "content": "If $w=\\blue{10}$, + +$\\qquad d=\\blue{10}+5=\\red{15}$. + +So the second point is at $(\\blue{10},\\red{15})$.", + "images": {}, + "metadata": undefined, + "replace": undefined, + "widgets": {}, + }, + { + "content": "The graph should look like this: + +![](https://ka-perseus-graphie.s3.amazonaws.com/d29e6802062c091aac9761cf34e41438104bd6a2.png)", + "images": { + "https://ka-perseus-graphie.s3.amazonaws.com/d29e6802062c091aac9761cf34e41438104bd6a2.png": { + "height": 425, + "width": 425, + }, + }, + "metadata": undefined, + "replace": undefined, + "widgets": {}, + }, + ], + "itemDataVersion": { + "major": 0, + "minor": 1, + }, + "question": { + "content": "You are $5$ miles away from your house when you start walking directly away from your house. In the table below, $w$ represents the number of miles you have walked, and $d$ represents your distance from home in miles. + +The relationship between these two variables can be expressed by the following equation: + +$d=w+5.$ + +**Plot two points on the graph that show your distance from home if you walked $6$ miles and $10$ miles.** + +$w$ | $d$ +:-:|:-: +$0$ | $5$ +$1$ | $6$ +$2$ | $7$ +$3$ | $8$ + + + +[[☃ interactive-graph 1]]", + "images": {}, + "metadata": undefined, + "widgets": { + "interactive-graph 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "backgroundImage": { + "bottom": 0, + "height": 0, + "left": 0, + "scale": 1, + "top": undefined, + "url": null, + "width": 0, + }, + "correct": { + "coord": undefined, + "coords": [ + [ + 6, + 11, + ], + [ + 10, + 15, + ], + ], + "numPoints": 2, + "startCoords": undefined, + "type": "point", + }, + "fullGraphAriaDescription": undefined, + "fullGraphLabel": undefined, + "graph": { + "coord": undefined, + "coords": undefined, + "numPoints": 2, + "startCoords": undefined, + "type": "point", + }, + "gridStep": [ + 1, + 1, + ], + "labels": [ + "w", + "d", + ], + "lockedFigures": undefined, + "markings": "graph", + "range": [ + [ + -1, + 18, + ], + [ + -1, + 18, + ], + ], + "rulerLabel": "", + "rulerTicks": 10, + "showProtractor": false, + "showRuler": false, + "showTooltips": undefined, + "snapStep": [ + 0.5, + 0.5, + ], + "step": [ + 1, + 1, + ], + }, + "static": undefined, + "type": "interactive-graph", + "version": { + "major": 0, + "minor": 0, + }, + }, + }, + }, +} +`; + +exports[`parseAndTypecheckPerseusItem correctly parses data/interactive-graph-locked-line-missing-showPoint1.json 1`] = ` +{ + "answer": undefined, + "answerArea": {}, + "hints": [], + "itemDataVersion": undefined, + "question": { + "content": "Custom Axis Labels: +[[☃ interactive-graph 1]] + +Large $y$-range, origin near bottom left: +[[☃ interactive-graph 2]] + +Large $x$-range, origin near left side: +[[☃ interactive-graph 3]] + +Fractional axis labels: +[[☃ interactive-graph 4]] + +Gridlines every two ticks: +[[☃ interactive-graph 5]] + +Gridlines every half tick: +[[☃ interactive-graph 6]] + +Nonsquare grid: +[[☃ interactive-graph 7]] + +Locked figures: +[[☃ interactive-graph 8]] +", + "images": {}, + "metadata": undefined, + "widgets": { + "interactive-graph 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "backgroundImage": { + "bottom": undefined, + "height": undefined, + "left": undefined, + "scale": undefined, + "top": undefined, + "url": null, + "width": undefined, + }, + "correct": { + "coord": undefined, + "coords": [ + [ + [ + -5, + 5, + ], + [ + 5, + 5, + ], + ], + [ + [ + -5, + 3, + ], + [ + 5, + 3, + ], + ], + [ + [ + -5, + 1, + ], + [ + 5, + 1, + ], + ], + [ + [ + -5, + -1, + ], + [ + 5, + -1, + ], + ], + [ + [ + -5, + -3, + ], + [ + 5, + -3, + ], + ], + [ + [ + -5, + -5, + ], + [ + 5, + -5, + ], + ], + ], + "numSegments": 6, + "startCoords": undefined, + "type": "segment", + }, + "fullGraphAriaDescription": undefined, + "fullGraphLabel": undefined, + "graph": { + "coord": undefined, + "coords": undefined, + "numSegments": 6, + "startCoords": undefined, + "type": "segment", + }, + "gridStep": [ + 1, + 1, + ], + "labels": [ + "\\text{Re}", + "\\text{Im}", + ], + "lockedFigures": undefined, + "markings": "graph", + "range": [ + [ + -10, + 10, + ], + [ + -10, + 10, + ], + ], + "rulerLabel": undefined, + "rulerTicks": undefined, + "showProtractor": false, + "showRuler": undefined, + "showTooltips": false, + "snapStep": [ + 0.5, + 0.5, + ], + "step": [ + 1, + 1, + ], + }, + "static": false, + "type": "interactive-graph", + "version": { + "major": 0, + "minor": 0, + }, + }, + "interactive-graph 2": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "backgroundImage": { + "bottom": undefined, + "height": undefined, + "left": undefined, + "scale": undefined, + "top": undefined, + "url": null, + "width": undefined, + }, + "correct": { + "coord": undefined, + "coords": [ + [ + [ + 1.5, + 70, + ], + [ + 5.5, + 70, + ], + ], + ], + "numSegments": undefined, + "startCoords": undefined, + "type": "segment", + }, + "fullGraphAriaDescription": undefined, + "fullGraphLabel": undefined, + "graph": { + "coord": undefined, + "coords": undefined, + "numSegments": undefined, + "startCoords": undefined, + "type": "segment", + }, + "gridStep": [ + 1, + 10, + ], + "labels": [ + "x", + "y", + ], + "lockedFigures": undefined, + "markings": "graph", + "range": [ + [ + -0.7, + 8, + ], + [ + -10, + 100, + ], + ], + "rulerLabel": undefined, + "rulerTicks": undefined, + "showProtractor": false, + "showRuler": undefined, + "showTooltips": false, + "snapStep": [ + 0.5, + 5, + ], + "step": [ + 1, + 10, + ], + }, + "static": false, + "type": "interactive-graph", + "version": { + "major": 0, + "minor": 0, + }, + }, + "interactive-graph 3": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "backgroundImage": { + "bottom": undefined, + "height": 0, + "left": undefined, + "scale": undefined, + "top": undefined, + "url": null, + "width": 0, + }, + "correct": { + "coord": undefined, + "coords": undefined, + "numSegments": undefined, + "startCoords": undefined, + "type": "segment", + }, + "fullGraphAriaDescription": undefined, + "fullGraphLabel": undefined, + "graph": { + "coord": undefined, + "coords": undefined, + "numSegments": undefined, + "startCoords": undefined, + "type": "segment", + }, + "gridStep": [ + 5, + 1, + ], + "labels": [ + "x", + "y", + ], + "lockedFigures": undefined, + "markings": "graph", + "range": [ + [ + -10, + 100, + ], + [ + -10, + 10, + ], + ], + "rulerLabel": undefined, + "rulerTicks": undefined, + "showProtractor": false, + "showRuler": undefined, + "showTooltips": false, + "snapStep": [ + 2.5, + 0.5, + ], + "step": [ + 20, + 1, + ], + }, + "static": false, + "type": "interactive-graph", + "version": { + "major": 0, + "minor": 0, + }, + }, + "interactive-graph 4": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "backgroundImage": { + "bottom": undefined, + "height": undefined, + "left": undefined, + "scale": undefined, + "top": undefined, + "url": null, + "width": undefined, + }, + "correct": { + "coord": undefined, + "coords": undefined, + "numSegments": undefined, + "startCoords": undefined, + "type": "segment", + }, + "fullGraphAriaDescription": undefined, + "fullGraphLabel": undefined, + "graph": { + "coord": undefined, + "coords": undefined, + "numSegments": undefined, + "startCoords": undefined, + "type": "segment", + }, + "gridStep": [ + 0.5, + 0.5, + ], + "labels": [ + "x", + "y", + ], + "lockedFigures": undefined, + "markings": "graph", + "range": [ + [ + -3, + 3, + ], + [ + -3, + 3, + ], + ], + "rulerLabel": undefined, + "rulerTicks": undefined, + "showProtractor": false, + "showRuler": undefined, + "showTooltips": false, + "snapStep": [ + 0.25, + 0.25, + ], + "step": [ + 0.5, + 0.5, + ], + }, + "static": false, + "type": "interactive-graph", + "version": { + "major": 0, + "minor": 0, + }, + }, + "interactive-graph 5": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "backgroundImage": { + "bottom": undefined, + "height": undefined, + "left": undefined, + "scale": undefined, + "top": undefined, + "url": null, + "width": undefined, + }, + "correct": { + "coord": undefined, + "coords": undefined, + "numSegments": undefined, + "startCoords": undefined, + "type": "segment", + }, + "fullGraphAriaDescription": undefined, + "fullGraphLabel": undefined, + "graph": { + "coord": undefined, + "coords": undefined, + "numSegments": undefined, + "startCoords": undefined, + "type": "segment", + }, + "gridStep": [ + 2, + 2, + ], + "labels": [ + "x", + "y", + ], + "lockedFigures": undefined, + "markings": "graph", + "range": [ + [ + -10, + 10, + ], + [ + -10, + 10, + ], + ], + "rulerLabel": undefined, + "rulerTicks": undefined, + "showProtractor": false, + "showRuler": undefined, + "showTooltips": false, + "snapStep": [ + 1, + 1, + ], + "step": [ + 1, + 1, + ], + }, + "static": false, + "type": "interactive-graph", + "version": { + "major": 0, + "minor": 0, + }, + }, + "interactive-graph 6": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "backgroundImage": { + "bottom": undefined, + "height": undefined, + "left": undefined, + "scale": undefined, + "top": undefined, + "url": null, + "width": undefined, + }, + "correct": { + "coord": undefined, + "coords": undefined, + "numSegments": undefined, + "startCoords": undefined, + "type": "segment", + }, + "fullGraphAriaDescription": undefined, + "fullGraphLabel": undefined, + "graph": { + "coord": undefined, + "coords": undefined, + "numSegments": undefined, + "startCoords": undefined, + "type": "segment", + }, + "gridStep": [ + 0.5, + 0.5, + ], + "labels": [ + "x", + "y", + ], + "lockedFigures": undefined, + "markings": "graph", + "range": [ + [ + -5, + 5, + ], + [ + -5, + 5, + ], + ], + "rulerLabel": undefined, + "rulerTicks": undefined, + "showProtractor": false, + "showRuler": undefined, + "showTooltips": false, + "snapStep": [ + 0.25, + 0.25, + ], + "step": [ + 1, + 1, + ], + }, + "static": false, + "type": "interactive-graph", + "version": { + "major": 0, + "minor": 0, + }, + }, + "interactive-graph 7": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "backgroundImage": { + "bottom": undefined, + "height": undefined, + "left": undefined, + "scale": undefined, + "top": undefined, + "url": null, + "width": undefined, + }, + "correct": { + "coord": undefined, + "coords": undefined, + "numSegments": undefined, + "startCoords": undefined, + "type": "segment", + }, + "fullGraphAriaDescription": undefined, + "fullGraphLabel": undefined, + "graph": { + "coord": undefined, + "coords": undefined, + "numSegments": undefined, + "startCoords": undefined, + "type": "segment", + }, + "gridStep": [ + 2, + 0.5, + ], + "labels": [ + "x", + "y", + ], + "lockedFigures": undefined, + "markings": "graph", + "range": [ + [ + -5, + 5, + ], + [ + -5, + 5, + ], + ], + "rulerLabel": undefined, + "rulerTicks": undefined, + "showProtractor": false, + "showRuler": undefined, + "showTooltips": false, + "snapStep": [ + 1, + 0.25, + ], + "step": [ + 1, + 1, + ], + }, + "static": false, + "type": "interactive-graph", + "version": { + "major": 0, + "minor": 0, + }, + }, + "interactive-graph 8": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "backgroundImage": { + "bottom": undefined, + "height": undefined, + "left": undefined, + "scale": undefined, + "top": undefined, + "url": null, + "width": undefined, + }, + "correct": { + "coord": undefined, + "coords": [ + [ + [ + -5, + -5, + ], + [ + 5, + 5, + ], + ], + ], + "hasBeenInteractedWith": true, + "markings": "graph", + "numSegments": undefined, + "range": [ + [ + -10, + 10, + ], + [ + -10, + 10, + ], + ], + "snapStep": [ + 0.5, + 0.5, + ], + "startCoords": undefined, + "type": "segment", + }, + "fullGraphAriaDescription": undefined, + "fullGraphLabel": undefined, + "graph": { + "coord": undefined, + "coords": undefined, + "numSegments": undefined, + "startCoords": undefined, + "type": "segment", + }, + "gridStep": [ + 1, + 1, + ], + "labels": [ + "x", + "y", + ], + "lockedFigures": [ + { + "ariaLabel": undefined, + "color": "green", + "coord": [ + -1, + 5, + ], + "filled": true, + "labels": undefined, + "type": "point", + }, + { + "ariaLabel": undefined, + "color": "grayH", + "coord": [ + 1, + 5, + ], + "filled": false, + "labels": undefined, + "type": "point", + }, + { + "ariaLabel": undefined, + "color": "grayH", + "kind": "line", + "labels": undefined, + "lineStyle": "solid", + "points": [ + { + "ariaLabel": undefined, + "color": "grayH", + "coord": [ + 0, + 1, + ], + "filled": true, + "labels": undefined, + "type": "point", + }, + { + "ariaLabel": undefined, + "color": "grayH", + "coord": [ + 5, + 2, + ], + "filled": true, + "labels": undefined, + "type": "point", + }, + ], + "showEndPoint": false, + "showPoint1": false, + "showPoint2": false, + "showStartPoint": false, + "type": "line", + }, + { + "ariaLabel": undefined, + "color": "grayH", + "kind": "line", + "labels": undefined, + "lineStyle": "dashed", + "points": [ + { + "ariaLabel": undefined, + "color": "grayH", + "coord": [ + 0, + 0, + ], + "filled": true, + "labels": undefined, + "type": "point", + }, + { + "ariaLabel": undefined, + "color": "grayH", + "coord": [ + 5, + 1, + ], + "filled": false, + "labels": undefined, + "type": "point", + }, + ], + "showEndPoint": true, + "showPoint1": false, + "showPoint2": false, + "showStartPoint": true, + "type": "line", + }, + { + "ariaLabel": undefined, + "color": "pink", + "kind": "ray", + "labels": undefined, + "lineStyle": "solid", + "points": [ + { + "ariaLabel": undefined, + "color": "pink", + "coord": [ + 0, + -1, + ], + "filled": true, + "labels": undefined, + "type": "point", + }, + { + "ariaLabel": undefined, + "color": "pink", + "coord": [ + 5, + 0, + ], + "filled": true, + "labels": undefined, + "type": "point", + }, + ], + "showEndPoint": false, + "showPoint1": false, + "showPoint2": false, + "showStartPoint": false, + "type": "line", + }, + { + "ariaLabel": undefined, + "color": "pink", + "kind": "ray", + "labels": undefined, + "lineStyle": "dashed", + "points": [ + { + "ariaLabel": undefined, + "color": "purple", + "coord": [ + 0, + -2, + ], + "filled": true, + "labels": undefined, + "type": "point", + }, + { + "ariaLabel": undefined, + "color": "pink", + "coord": [ + 5, + -1, + ], + "filled": false, + "labels": undefined, + "type": "point", + }, ], - [ - 10, - 15, + "showEndPoint": true, + "showPoint1": false, + "showPoint2": false, + "showStartPoint": true, + "type": "line", + }, + { + "ariaLabel": undefined, + "color": "red", + "kind": "segment", + "labels": undefined, + "lineStyle": "solid", + "points": [ + { + "ariaLabel": undefined, + "color": "red", + "coord": [ + 0, + -3, + ], + "filled": true, + "labels": undefined, + "type": "point", + }, + { + "ariaLabel": undefined, + "color": "red", + "coord": [ + 5, + -2, + ], + "filled": true, + "labels": undefined, + "type": "point", + }, ], - ], - "numPoints": 2, - "startCoords": undefined, - "type": "point", - }, - "fullGraphAriaDescription": undefined, - "fullGraphLabel": undefined, - "graph": { - "coord": undefined, - "coords": undefined, - "numPoints": 2, - "startCoords": undefined, - "type": "point", - }, - "gridStep": [ - 1, - 1, - ], - "labels": [ - "w", - "d", + "showEndPoint": false, + "showPoint1": false, + "showPoint2": false, + "showStartPoint": false, + "type": "line", + }, + { + "ariaLabel": undefined, + "color": "red", + "kind": "segment", + "labels": undefined, + "lineStyle": "dashed", + "points": [ + { + "ariaLabel": undefined, + "color": "green", + "coord": [ + 0, + -4, + ], + "filled": true, + "labels": undefined, + "type": "point", + }, + { + "ariaLabel": undefined, + "color": "red", + "coord": [ + 5, + -3, + ], + "filled": false, + "labels": undefined, + "type": "point", + }, + ], + "showEndPoint": true, + "showPoint1": false, + "showPoint2": false, + "showStartPoint": true, + "type": "line", + }, + { + "color": "blue", + "coord": [ + -6, + 0, + ], + "size": "medium", + "text": "\\frac{1}{4}?", + "type": "label", + }, ], - "lockedFigures": undefined, "markings": "graph", "range": [ [ - -1, - 18, + -10, + 10, ], [ - -1, - 18, + -10, + 10, ], ], - "rulerLabel": "", - "rulerTicks": 10, + "rulerLabel": undefined, + "rulerTicks": undefined, "showProtractor": false, - "showRuler": false, - "showTooltips": undefined, + "showRuler": undefined, + "showTooltips": false, "snapStep": [ 0.5, 0.5, ], "step": [ - 1, - 1, + 2, + 2, ], }, - "static": undefined, + "static": false, "type": "interactive-graph", "version": { "major": 0, @@ -4017,6 +5671,119 @@ $\\left[\\begin{array}{c} } `; +exports[`parseAndTypecheckPerseusItem correctly parses data/measurer-missing-image.json 1`] = ` +{ + "answer": undefined, + "answerArea": { + "calculator": false, + }, + "hints": [ + { + "content": "crwdns2931741:0crwdne2931741:0", + "images": {}, + "metadata": undefined, + "replace": undefined, + "widgets": {}, + }, + { + "content": "crwdns2931695:0crwdne2931695:0", + "images": {}, + "metadata": undefined, + "replace": undefined, + "widgets": {}, + }, + { + "content": "crwdns2931679:0crwdne2931679:0", + "images": {}, + "metadata": undefined, + "replace": undefined, + "widgets": {}, + }, + ], + "itemDataVersion": undefined, + "question": { + "content": "crwdns3125767:0crwdne3125767:0", + "images": {}, + "metadata": undefined, + "widgets": { + "dropdown 1": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "ariaLabel": undefined, + "choices": [ + { + "content": "crwdns2301760:0crwdne2301760:0", + "correct": false, + }, + { + "content": "crwdns3766725:0crwdne3766725:0", + "correct": false, + }, + { + "content": "crwdns3395333:0crwdne3395333:0", + "correct": true, + }, + { + "content": "crwdns3395334:0crwdne3395334:0", + "correct": false, + }, + { + "content": "crwdns3445395:0crwdne3445395:0", + "correct": false, + }, + { + "content": "crwdns3395337:0crwdne3395337:0", + "correct": false, + }, + { + "content": "crwdns3395340:0crwdne3395340:0", + "correct": false, + }, + ], + "placeholder": "", + "static": false, + "visibleLabel": undefined, + }, + "static": undefined, + "type": "dropdown", + "version": undefined, + }, + "measurer 1": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "box": [ + 480, + 480, + ], + "image": { + "left": 0, + "top": 0, + "url": null, + }, + "imageLeft": 0, + "imageTop": 0, + "imageUrl": "crwdns6514084:0crwdne6514084:0", + "rulerLabel": "", + "rulerLength": 10, + "rulerPixels": 40, + "rulerTicks": 10, + "showProtractor": true, + "showRuler": false, + "static": false, + }, + "static": undefined, + "type": "measurer", + "version": undefined, + }, + }, + }, +} +`; + exports[`parseAndTypecheckPerseusItem correctly parses data/measurer-missing-static.json 1`] = ` { "answer": undefined, @@ -6996,6 +8763,132 @@ Anton Peffenhauser, *Foot-Combat Armor of Prince-Elector Christian I of Saxony ( } `; +exports[`parseAndTypecheckPerseusItem correctly parses data/plotter-missing-scaleY-and-snapsPerLine.json 1`] = ` +{ + "answer": undefined, + "answerArea": { + "calculator": false, + }, + "hints": [ + { + "content": "Barn | Antal mål +- | :-: +Calista | $\\blue2$ +William |$\\red3$ +Michaela | $\\green5$ +James | $\\gray2$ + +$$ + +$\\green5 - \\red3= \\purple{2}$", + "images": {}, + "metadata": undefined, + "replace": undefined, + "widgets": {}, + }, + { + "content": "Michaela gjorde $\\purple{2}$ korgar mer än William. ", + "images": {}, + "metadata": undefined, + "replace": undefined, + "widgets": {}, + }, + ], + "itemDataVersion": { + "major": 0, + "minor": 1, + }, + "question": { + "content": "En familj spelar basket. Pictogrammet visar hur många mål varje barn gjorde. + +**Michaela gjorde [[☃ input-number 1]] fler mål än William.** + +![](https://ka-perseus-graphie.s3.amazonaws.com/01794c4768ba6b824954277b869aaaefd551a0e5.png) + + +![](https://ka-perseus-images.s3.amazonaws.com/2875f6cdd7dea3db2fef714b1225366c7250c49d.png)", + "images": { + "https://ka-perseus-graphie.s3.amazonaws.com/01794c4768ba6b824954277b869aaaefd551a0e5.png": { + "height": 37, + "width": 120, + }, + "https://ka-perseus-images.s3.amazonaws.com/2875f6cdd7dea3db2fef714b1225366c7250c49d.png": { + "height": 336, + "width": 474, + }, + }, + "metadata": undefined, + "widgets": { + "input-number 1": { + "alignment": undefined, + "graded": true, + "key": undefined, + "options": { + "answerType": "number", + "customKeypad": undefined, + "inexact": false, + "maxError": 0.1, + "rightAlign": undefined, + "simplify": "required", + "size": "normal", + "value": 2, + }, + "static": undefined, + "type": "input-number", + "version": { + "major": 0, + "minor": 0, + }, + }, + "plotter 1": { + "alignment": undefined, + "graded": undefined, + "key": undefined, + "options": { + "categories": [ + "Calista", + "WIlliam", + "Michaela", + "James", + ], + "correct": [ + 1, + 1, + 1, + 1, + ], + "labelInterval": undefined, + "labels": [ + "Child", + "Baskets", + ], + "maxY": 5, + "picBoxHeight": undefined, + "picSize": undefined, + "picUrl": "http://i.imgur.com/B8mGnxB.png", + "plotDimensions": [ + 380, + 300, + ], + "scaleY": 1, + "snapsPerLine": 2, + "starting": [ + 1, + 1, + 1, + 1, + ], + "type": "pic", + }, + "static": undefined, + "type": "plotter", + "version": undefined, + }, + }, + }, +} +`; + exports[`parseAndTypecheckPerseusItem correctly parses data/plotter-with-undefined-plotDimensions.json 1`] = ` { "answer": undefined, diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/data/cs-program-with-null-width.json b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/cs-program-with-null-width.json new file mode 100644 index 0000000000..f587f8ebd9 --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/cs-program-with-null-width.json @@ -0,0 +1,20 @@ +{ + "question": { + "content": "[[☃ cs-program 1]]", + "images": {}, + "widgets": { + "cs-program 1": { + "type": "cs-program", + "options": { + "settings": [], + "height": 250, + "width": null, + "programID": "4545417404481536", + "showButtons": true, + "showEditor": true + }, + "alignment": "block" + } + } + } +} diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/data/iframe-missing-allowFullScreen.json b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/iframe-missing-allowFullScreen.json new file mode 100644 index 0000000000..ddc5e6cf7e --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/iframe-missing-allowFullScreen.json @@ -0,0 +1,90 @@ +{ + "answerArea": { + "calculator": false, + "options": { + "content": "", + "images": {}, + "widgets": {} + }, + "type": "multiple" + }, + "hints": [ + { + "content": "This is the easy step. Just drag disk 3 over to peg \"B\".\n\n[[☃ image 1]]", + "images": {}, + "widgets": { + "image 1": { + "graded": true, + "options": { + "backgroundImage": { + "height": 215, + "url": "https://s3.amazonaws.com/ka-cs-algorithms/hanoi_exercise_step2_1.png", + "width": 304 + }, + "box": [ + 304, + 215 + ], + "labels": [], + "range": [ + [ + 0, + 10 + ], + [ + 0, + 10 + ] + ] + }, + "type": "image", + "version": { + "major": 0, + "minor": 0 + } + } + } + } + ], + "itemDataVersion": { + "major": 0, + "minor": 1 + }, + "question": { + "content": "Congratulations, you have exposed disk 3, and since our goal is move 3 disks to peg \"B\", that's the disk we want on the bottom of peg \"B\". Move it to the target peg now.\n\n[[☃ iframe 1]]", + "images": {}, + "widgets": { + "iframe 1": { + "graded": true, + "options": { + "height": "400", + "settings": [ + { + "name": "step", + "value": "2" + }, + { + "name": "disk1", + "value": "2" + }, + { + "name": "disk2", + "value": "2" + }, + { + "name": "", + "value": "" + } + ], + "url": "4772835774169088", + "width": 400 + }, + "type": "iframe", + "version": { + "major": 0, + "minor": 0 + } + } + } + } +} diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/data/iframe-missing-settings.json b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/iframe-missing-settings.json new file mode 100644 index 0000000000..cdb1f2c86e --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/iframe-missing-settings.json @@ -0,0 +1,17 @@ +{ + "question": { + "content": "[[☃ iframe 1]]", + "images": {}, + "widgets": { + "iframe 1": { + "alignment": "block", + "options": { + "height": "550px", + "url": "https://learnstorm.typeform.com/to/fnQ2tw?", + "width": "100%" + }, + "type": "iframe" + } + } + } +} diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/data/interaction-element-missing-key.json b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/interaction-element-missing-key.json new file mode 100644 index 0000000000..efd2eb02f5 --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/interaction-element-missing-key.json @@ -0,0 +1,452 @@ +{ + "question": { + "content": "# Functions introduction\n\nA function is something that maps one value to another.\n\nHere is a function that maps an $\\orange\\text{input dot}$ on the top to an $\\blue\\text{output dot}$ on the bottom. Try dragging the $\\orange\\text{input dot}$ on the left and see what $\\blue\\text{output}$ the function maps it to below:\n\n[[☃ interaction 1]]\n\nNot all functions are quite so simple! For example, there is no rule that the $\\blue\\text{output}$ has to increase when the $\\orange\\text{input}$ increases:\n\n[[☃ interaction 2]]\n\nThere is also no rule that a function has to map to a different value for each different input value:\n\n[[☃ interaction 3]]\n\nOr that it even has to ever map to a different value at all!\n\n[[☃ interaction 4]]\n\nBut that's sort of unsatisfying! so here's another function that demonstrates all of those concepts:\n\n[[☃ interaction 5]]\n\nNext, we'll look at some other representations of functions!", + "images": { + "https://ka-perseus-graphie.s3.amazonaws.com/da8df81c78b22f5c69d477d8eabfb583968eaf84.png": { + "width": 400, + "height": 70 + }, + "https://ka-perseus-graphie.s3.amazonaws.com/b59fc02ca1aae800977b8793ed22f647a1aa75ee.png": { + "width": 425, + "height": 150 + } + }, + "widgets": { + "interaction 1": { + "type": "interaction", + "graded": true, + "options": { + "graph": { + "editableSettings": [ + "canvas", + "graph" + ], + "box": [ + 400, + 200 + ], + "labels": [ + "", + "" + ], + "range": [ + [ + 0, + 10 + ], + [ + -6, + 6 + ] + ], + "gridStep": [ + 1, + 3 + ], + "markings": "graph", + "snapStep": [ + 0.5, + 1.5 + ], + "valid": true, + "backgroundImage": { + "url": null, + "scale": 1, + "bottom": 0, + "left": 0 + }, + "showProtractor": false, + "showRuler": false, + "rulerLabel": "", + "rulerTicks": 10, + "tickStep": [ + 1, + 2 + ], + "scale": [ + 40, + 16.666666666666668 + ] + }, + "elements": [ + { + "type": "movable-point", + "options": { + "startX": "5", + "startY": "3", + "constraint": "snap", + "snap": 1, + "constraintFn": "-3", + "constraintXMin": "1", + "constraintXMax": "8", + "constraintYMin": "3", + "constraintYMax": "3", + "varSubscript": 0 + } + }, + { + "type": "point", + "options": { + "coordX": "x_0+1", + "coordY": "-3", + "color": "#6495ED" + } + } + ] + }, + "version": { + "major": 0, + "minor": 0 + } + }, + "interaction 2": { + "type": "interaction", + "graded": true, + "options": { + "graph": { + "editableSettings": [ + "canvas", + "graph" + ], + "box": [ + 400, + 200 + ], + "labels": [ + "", + "" + ], + "range": [ + [ + 0, + 10 + ], + [ + -6, + 6 + ] + ], + "gridStep": [ + 1, + 3 + ], + "markings": "graph", + "snapStep": [ + 0.5, + 1.5 + ], + "valid": true, + "backgroundImage": { + "url": null, + "scale": 1, + "bottom": 0, + "left": 0 + }, + "showProtractor": false, + "showRuler": false, + "rulerLabel": "", + "rulerTicks": 10, + "tickStep": [ + 1, + 2 + ], + "scale": [ + 40, + 16.666666666666668 + ] + }, + "elements": [ + { + "type": "movable-point", + "options": { + "startX": "5", + "startY": "3", + "constraint": "snap", + "snap": 1, + "constraintFn": "-3", + "constraintXMin": "1", + "constraintXMax": "9", + "constraintYMin": "3", + "constraintYMax": "3", + "varSubscript": 0 + } + }, + { + "type": "point", + "options": { + "coordX": "10-x_0", + "coordY": "-3", + "color": "#6495ED" + } + } + ] + }, + "version": { + "major": 0, + "minor": 0 + } + }, + "interaction 3": { + "type": "interaction", + "graded": true, + "options": { + "graph": { + "editableSettings": [ + "canvas", + "graph" + ], + "box": [ + 400, + 200 + ], + "labels": [ + "", + "" + ], + "range": [ + [ + 0, + 10 + ], + [ + -6, + 6 + ] + ], + "gridStep": [ + 1, + 3 + ], + "markings": "graph", + "snapStep": [ + 0.5, + 1.5 + ], + "valid": true, + "backgroundImage": { + "url": null, + "scale": 1, + "bottom": 0, + "left": 0 + }, + "showProtractor": false, + "showRuler": false, + "rulerLabel": "", + "rulerTicks": 10, + "tickStep": [ + 1, + 2 + ], + "scale": [ + 40, + 16.666666666666668 + ] + }, + "elements": [ + { + "type": "movable-point", + "options": { + "startX": "5", + "startY": "3", + "constraint": "snap", + "snap": 1, + "constraintFn": "-3", + "constraintXMin": "1", + "constraintXMax": "9", + "constraintYMin": "3", + "constraintYMax": "3", + "varSubscript": 0 + } + }, + { + "type": "point", + "options": { + "coordX": "\\sin\\left(x_0\\cdot\\frac{\\pi}{2}\\right)+5", + "coordY": "-3", + "color": "#6495ED" + } + } + ] + }, + "version": { + "major": 0, + "minor": 0 + } + }, + "interaction 4": { + "type": "interaction", + "graded": true, + "options": { + "graph": { + "editableSettings": [ + "canvas", + "graph" + ], + "box": [ + 400, + 200 + ], + "labels": [ + "", + "" + ], + "range": [ + [ + 0, + 10 + ], + [ + -6, + 6 + ] + ], + "gridStep": [ + 1, + 3 + ], + "markings": "graph", + "snapStep": [ + 0.5, + 1.5 + ], + "valid": true, + "backgroundImage": { + "url": null, + "scale": 1, + "bottom": 0, + "left": 0 + }, + "showProtractor": false, + "showRuler": false, + "rulerLabel": "", + "rulerTicks": 10, + "tickStep": [ + 1, + 2 + ], + "scale": [ + 40, + 16.666666666666668 + ] + }, + "elements": [ + { + "type": "movable-point", + "options": { + "startX": "5", + "startY": "3", + "constraint": "snap", + "snap": 1, + "constraintFn": "-3", + "constraintXMin": "1", + "constraintXMax": "9", + "constraintYMin": "3", + "constraintYMax": "3", + "varSubscript": 0 + } + }, + { + "type": "point", + "options": { + "coordX": "4", + "coordY": "-3", + "color": "#6495ED" + } + } + ] + }, + "version": { + "major": 0, + "minor": 0 + } + }, + "interaction 5": { + "type": "interaction", + "graded": true, + "options": { + "graph": { + "editableSettings": [ + "canvas", + "graph" + ], + "box": [ + 400, + 200 + ], + "labels": [ + "", + "" + ], + "range": [ + [ + 0, + 10 + ], + [ + -6, + 6 + ] + ], + "gridStep": [ + 1, + 3 + ], + "markings": "graph", + "snapStep": [ + 0.5, + 1.5 + ], + "valid": true, + "backgroundImage": { + "url": null, + "scale": 1, + "bottom": 0, + "left": 0 + }, + "showProtractor": false, + "showRuler": false, + "rulerLabel": "", + "rulerTicks": 10, + "tickStep": [ + 1, + 2 + ], + "scale": [ + 40, + 16.666666666666668 + ] + }, + "elements": [ + { + "type": "movable-point", + "options": { + "startX": "5", + "startY": "3", + "constraint": "snap", + "snap": 1, + "constraintFn": "-3", + "constraintXMin": "1", + "constraintXMax": "9", + "constraintYMin": "3", + "constraintYMax": "3", + "varSubscript": 0 + } + }, + { + "type": "point", + "options": { + "coordX": "5-\\left|x_0-5\\right|", + "coordY": "-3", + "color": "#6495ED" + } + } + ] + }, + "version": { + "major": 0, + "minor": 0 + } + } + } + } +} diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/data/interactive-graph-locked-line-missing-showPoint1.json b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/interactive-graph-locked-line-missing-showPoint1.json new file mode 100644 index 0000000000..89b8bb015c --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/interactive-graph-locked-line-missing-showPoint1.json @@ -0,0 +1,712 @@ +{ + "question": { + "content": "Custom Axis Labels:\n[[☃ interactive-graph 1]]\n\nLarge $y$-range, origin near bottom left:\n[[☃ interactive-graph 2]]\n\nLarge $x$-range, origin near left side:\n[[☃ interactive-graph 3]]\n\nFractional axis labels:\n[[☃ interactive-graph 4]]\n\nGridlines every two ticks:\n[[☃ interactive-graph 5]]\n\nGridlines every half tick:\n[[☃ interactive-graph 6]]\n\nNonsquare grid:\n[[☃ interactive-graph 7]]\n\nLocked figures:\n[[☃ interactive-graph 8]]\n", + "images": {}, + "widgets": { + "interactive-graph 1": { + "type": "interactive-graph", + "alignment": "default", + "static": false, + "graded": true, + "options": { + "step": [ + 1, + 1 + ], + "backgroundImage": { + "url": null + }, + "markings": "graph", + "labels": [ + "\\text{Re}", + "\\text{Im}" + ], + "showProtractor": false, + "showTooltips": false, + "range": [ + [ + -10, + 10 + ], + [ + -10, + 10 + ] + ], + "gridStep": [ + 1, + 1 + ], + "snapStep": [ + 0.5, + 0.5 + ], + "graph": { + "type": "segment", + "numSegments": 6 + }, + "correct": { + "type": "segment", + "numSegments": 6, + "coords": [ + [ + [ + -5, + 5 + ], + [ + 5, + 5 + ] + ], + [ + [ + -5, + 3 + ], + [ + 5, + 3 + ] + ], + [ + [ + -5, + 1 + ], + [ + 5, + 1 + ] + ], + [ + [ + -5, + -1 + ], + [ + 5, + -1 + ] + ], + [ + [ + -5, + -3 + ], + [ + 5, + -3 + ] + ], + [ + [ + -5, + -5 + ], + [ + 5, + -5 + ] + ] + ] + } + }, + "version": { + "major": 0, + "minor": 0 + } + }, + "interactive-graph 2": { + "type": "interactive-graph", + "alignment": "default", + "static": false, + "graded": true, + "options": { + "step": [ + 1, + 10 + ], + "backgroundImage": { + "url": null + }, + "markings": "graph", + "labels": [ + "x", + "y" + ], + "showProtractor": false, + "showTooltips": false, + "range": [ + [ + -0.7, + 8 + ], + [ + -10, + 100 + ] + ], + "gridStep": [ + 1, + 10 + ], + "snapStep": [ + 0.5, + 5 + ], + "graph": { + "type": "segment" + }, + "correct": { + "type": "segment", + "coords": [ + [ + [ + 1.5, + 70 + ], + [ + 5.5, + 70 + ] + ] + ] + } + }, + "version": { + "major": 0, + "minor": 0 + } + }, + "interactive-graph 3": { + "type": "interactive-graph", + "alignment": "default", + "static": false, + "graded": true, + "options": { + "step": [ + 20, + 1 + ], + "backgroundImage": { + "url": null, + "width": 0, + "height": 0 + }, + "markings": "graph", + "labels": [ + "x", + "y" + ], + "showProtractor": false, + "showTooltips": false, + "range": [ + [ + -10, + 100 + ], + [ + -10, + 10 + ] + ], + "gridStep": [ + 5, + 1 + ], + "snapStep": [ + 2.5, + 0.5 + ], + "graph": { + "type": "segment" + }, + "correct": { + "type": "segment" + } + }, + "version": { + "major": 0, + "minor": 0 + } + }, + "interactive-graph 4": { + "type": "interactive-graph", + "alignment": "default", + "static": false, + "graded": true, + "options": { + "step": [ + 0.5, + 0.5 + ], + "backgroundImage": { + "url": null + }, + "markings": "graph", + "labels": [ + "x", + "y" + ], + "showProtractor": false, + "showTooltips": false, + "range": [ + [ + -3, + 3 + ], + [ + -3, + 3 + ] + ], + "gridStep": [ + 0.5, + 0.5 + ], + "snapStep": [ + 0.25, + 0.25 + ], + "graph": { + "type": "segment" + }, + "correct": { + "type": "segment" + } + }, + "version": { + "major": 0, + "minor": 0 + } + }, + "interactive-graph 5": { + "type": "interactive-graph", + "alignment": "default", + "static": false, + "graded": true, + "options": { + "step": [ + 1, + 1 + ], + "backgroundImage": { + "url": null + }, + "markings": "graph", + "labels": [ + "x", + "y" + ], + "showProtractor": false, + "showTooltips": false, + "range": [ + [ + -10, + 10 + ], + [ + -10, + 10 + ] + ], + "gridStep": [ + 2, + 2 + ], + "snapStep": [ + 1, + 1 + ], + "graph": { + "type": "segment" + }, + "correct": { + "type": "segment" + } + }, + "version": { + "major": 0, + "minor": 0 + } + }, + "interactive-graph 6": { + "type": "interactive-graph", + "alignment": "default", + "static": false, + "graded": true, + "options": { + "step": [ + 1, + 1 + ], + "backgroundImage": { + "url": null + }, + "markings": "graph", + "labels": [ + "x", + "y" + ], + "showProtractor": false, + "showTooltips": false, + "range": [ + [ + -5, + 5 + ], + [ + -5, + 5 + ] + ], + "gridStep": [ + 0.5, + 0.5 + ], + "snapStep": [ + 0.25, + 0.25 + ], + "graph": { + "type": "segment" + }, + "correct": { + "type": "segment" + } + }, + "version": { + "major": 0, + "minor": 0 + } + }, + "interactive-graph 7": { + "type": "interactive-graph", + "alignment": "default", + "static": false, + "graded": true, + "options": { + "step": [ + 1, + 1 + ], + "backgroundImage": { + "url": null + }, + "markings": "graph", + "labels": [ + "x", + "y" + ], + "showProtractor": false, + "showTooltips": false, + "range": [ + [ + -5, + 5 + ], + [ + -5, + 5 + ] + ], + "gridStep": [ + 2, + 0.5 + ], + "snapStep": [ + 1, + 0.25 + ], + "graph": { + "type": "segment" + }, + "correct": { + "type": "segment" + } + }, + "version": { + "major": 0, + "minor": 0 + } + }, + "interactive-graph 8": { + "type": "interactive-graph", + "alignment": "default", + "static": false, + "graded": true, + "options": { + "step": [ + 2, + 2 + ], + "backgroundImage": { + "url": null + }, + "markings": "graph", + "labels": [ + "x", + "y" + ], + "showProtractor": false, + "showTooltips": false, + "range": [ + [ + -10, + 10 + ], + [ + -10, + 10 + ] + ], + "gridStep": [ + 1, + 1 + ], + "snapStep": [ + 0.5, + 0.5 + ], + "lockedFigures": [ + { + "type": "point", + "coord": [ + -1, + 5 + ], + "color": "green", + "filled": true + }, + { + "type": "point", + "coord": [ + 1, + 5 + ], + "color": "grayH", + "filled": false + }, + { + "type": "line", + "kind": "line", + "points": [ + { + "type": "point", + "coord": [ + 0, + 1 + ], + "color": "grayH", + "filled": true + }, + { + "type": "point", + "coord": [ + 5, + 2 + ], + "color": "grayH", + "filled": true + } + ], + "color": "grayH", + "lineStyle": "solid", + "showStartPoint": false, + "showEndPoint": false + }, + { + "type": "line", + "kind": "line", + "points": [ + { + "type": "point", + "coord": [ + 0, + 0 + ], + "color": "grayH", + "filled": true + }, + { + "type": "point", + "coord": [ + 5, + 1 + ], + "color": "grayH", + "filled": false + } + ], + "color": "grayH", + "lineStyle": "dashed", + "showStartPoint": true, + "showEndPoint": true + }, + { + "type": "line", + "kind": "ray", + "points": [ + { + "type": "point", + "coord": [ + 0, + -1 + ], + "color": "pink", + "filled": true + }, + { + "type": "point", + "coord": [ + 5, + 0 + ], + "color": "pink", + "filled": true + } + ], + "color": "pink", + "lineStyle": "solid", + "showStartPoint": false, + "showEndPoint": false + }, + { + "type": "line", + "kind": "ray", + "points": [ + { + "type": "point", + "coord": [ + 0, + -2 + ], + "color": "purple", + "filled": true + }, + { + "type": "point", + "coord": [ + 5, + -1 + ], + "color": "pink", + "filled": false + } + ], + "color": "pink", + "lineStyle": "dashed", + "showStartPoint": true, + "showEndPoint": true + }, + { + "type": "line", + "kind": "segment", + "points": [ + { + "type": "point", + "coord": [ + 0, + -3 + ], + "color": "red", + "filled": true + }, + { + "type": "point", + "coord": [ + 5, + -2 + ], + "color": "red", + "filled": true + } + ], + "color": "red", + "lineStyle": "solid", + "showStartPoint": false, + "showEndPoint": false + }, + { + "type": "line", + "kind": "segment", + "points": [ + { + "type": "point", + "coord": [ + 0, + -4 + ], + "color": "green", + "filled": true + }, + { + "type": "point", + "coord": [ + 5, + -3 + ], + "color": "red", + "filled": false + } + ], + "color": "red", + "lineStyle": "dashed", + "showStartPoint": true, + "showEndPoint": true + }, + { + "type": "label", + "coord": [ + -6, + 0 + ], + "text": "\\frac{1}{4}?", + "color": "blue", + "size": "medium" + } + ], + "graph": { + "type": "segment" + }, + "correct": { + "type": "segment", + "hasBeenInteractedWith": true, + "range": [ + [ + -10, + 10 + ], + [ + -10, + 10 + ] + ], + "snapStep": [ + 0.5, + 0.5 + ], + "markings": "graph", + "coords": [ + [ + [ + -5, + -5 + ], + [ + 5, + 5 + ] + ] + ] + } + }, + "version": { + "major": 0, + "minor": 0 + } + } + } + } +} diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/data/measurer-missing-image.json b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/measurer-missing-image.json new file mode 100644 index 0000000000..9fd018cae8 --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/measurer-missing-image.json @@ -0,0 +1,89 @@ +{ + "answerArea": { + "calculator": false, + "options": { + "content": "", + "images": {}, + "widgets": {} + }, + "type": "multiple" + }, + "hints": [ + { + "content": "crwdns2931741:0crwdne2931741:0", + "images": {}, + "widgets": {} + }, + { + "content": "crwdns2931695:0crwdne2931695:0", + "images": {}, + "widgets": {} + }, + { + "content": "crwdns2931679:0crwdne2931679:0", + "images": {}, + "widgets": {} + } + ], + "question": { + "content": "crwdns3125767:0crwdne3125767:0", + "images": {}, + "widgets": { + "dropdown 1": { + "graded": true, + "options": { + "choices": [ + { + "content": "crwdns2301760:0crwdne2301760:0", + "correct": false + }, + { + "content": "crwdns3766725:0crwdne3766725:0", + "correct": false + }, + { + "content": "crwdns3395333:0crwdne3395333:0", + "correct": true + }, + { + "content": "crwdns3395334:0crwdne3395334:0", + "correct": false + }, + { + "content": "crwdns3445395:0crwdne3445395:0", + "correct": false + }, + { + "content": "crwdns3395337:0crwdne3395337:0", + "correct": false + }, + { + "content": "crwdns3395340:0crwdne3395340:0", + "correct": false + } + ] + }, + "type": "dropdown" + }, + "measurer 1": { + "graded": true, + "options": { + "box": [ + 480, + 480 + ], + "imageLeft": 0, + "imageTop": 0, + "imageUrl": "crwdns6514084:0crwdne6514084:0", + "rulerLabel": "", + "rulerLength": 10, + "rulerPixels": 40, + "rulerTicks": 10, + "showProtractor": true, + "showRuler": false + }, + "type": "measurer" + } + } + } +} diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/data/plotter-missing-scaleY-and-snapsPerLine.json b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/plotter-missing-scaleY-and-snapsPerLine.json new file mode 100644 index 0000000000..92102561ab --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/plotter-missing-scaleY-and-snapsPerLine.json @@ -0,0 +1,88 @@ +{ + "answerArea": { + "calculator": false, + "options": { + "content": "", + "images": {}, + "widgets": {} + }, + "type": "multiple" + }, + "hints": [ + { + "content": "Barn | Antal mål\n- | :-: \nCalista | $\\blue2$ \nWilliam |$\\red3$ \nMichaela | $\\green5$ \nJames | $\\gray2$\n\n$$\n\n$\\green5 - \\red3= \\purple{2}$", + "images": {}, + "widgets": {} + }, + { + "content": "Michaela gjorde $\\purple{2}$ korgar mer än William. ", + "images": {}, + "widgets": {} + } + ], + "itemDataVersion": { + "major": 0, + "minor": 1 + }, + "question": { + "content": "En familj spelar basket. Pictogrammet visar hur många mål varje barn gjorde. \n\n**Michaela gjorde [[☃ input-number 1]] fler mål än William.**\n\n![](https://ka-perseus-graphie.s3.amazonaws.com/01794c4768ba6b824954277b869aaaefd551a0e5.png)\n\n\n![](https://ka-perseus-images.s3.amazonaws.com/2875f6cdd7dea3db2fef714b1225366c7250c49d.png)", + "images": { + "https://ka-perseus-graphie.s3.amazonaws.com/01794c4768ba6b824954277b869aaaefd551a0e5.png": { + "height": 37, + "width": 120 + }, + "https://ka-perseus-images.s3.amazonaws.com/2875f6cdd7dea3db2fef714b1225366c7250c49d.png": { + "height": 336, + "width": 474 + } + }, + "widgets": { + "input-number 1": { + "graded": true, + "options": { + "answerType": "number", + "inexact": false, + "maxError": 0.1, + "simplify": "required", + "size": "normal", + "value": 2 + }, + "type": "input-number", + "version": { + "major": 0, + "minor": 0 + } + }, + "plotter 1": { + "options": { + "categories": [ + "Calista", + "WIlliam", + "Michaela", + "James" + ], + "correct": [ + 1, + 1, + 1, + 1 + ], + "labels": [ + "Child", + "Baskets" + ], + "maxY": 5, + "picUrl": "http://i.imgur.com/B8mGnxB.png", + "starting": [ + 1, + 1, + 1, + 1 + ], + "type": "pic" + }, + "type": "plotter" + } + } + } +} diff --git a/packages/perseus/src/widget-ai-utils/cs-program/cs-program-ai-utils.test.ts b/packages/perseus/src/widget-ai-utils/cs-program/cs-program-ai-utils.test.ts index 16dc8062f5..132d26fbe7 100644 --- a/packages/perseus/src/widget-ai-utils/cs-program/cs-program-ai-utils.test.ts +++ b/packages/perseus/src/widget-ai-utils/cs-program/cs-program-ai-utils.test.ts @@ -19,7 +19,6 @@ const question1: PerseusRenderer = { {name: "", value: ""}, ], height: 540, - width: 640, programID: "6293105639817216", static: false, showButtons: false, diff --git a/packages/perseus/src/widgets/cs-program/cs-program.testdata.ts b/packages/perseus/src/widgets/cs-program/cs-program.testdata.ts index b647d43fdd..4f75277594 100644 --- a/packages/perseus/src/widgets/cs-program/cs-program.testdata.ts +++ b/packages/perseus/src/widgets/cs-program/cs-program.testdata.ts @@ -15,7 +15,6 @@ export const question1: PerseusRenderer = { {name: "", value: ""}, ], height: 540, - width: 640, programID: "6293105639817216", static: false, showButtons: false, diff --git a/packages/perseus/src/widgets/grapher/score-grapher.test.ts b/packages/perseus/src/widgets/grapher/score-grapher.test.ts index b4d94b7ae7..632e9b3ae2 100644 --- a/packages/perseus/src/widgets/grapher/score-grapher.test.ts +++ b/packages/perseus/src/widgets/grapher/score-grapher.test.ts @@ -53,9 +53,6 @@ describe("scoreGrapher", () => { const userInput: PerseusGrapherUserInput = { type: "exponential", asymptote, - // TODO: either the types or logic is wrong, - // but the existing scoring function checks for null coords - // @ts-expect-error - TS(2322) - Type 'null' is not assignable to type 'readonly Coord[]'. coords: null, }; @@ -103,6 +100,37 @@ describe("scoreGrapher", () => { expect(result).toHaveInvalidInput(); }); + it("is invalid when rubric has null coords", () => { + // The rubric.correct.coords are null in some cases in legacy data. + // Before this test was added and made to pass, the scoring code would + // throw an exception if the coords were null. From a learner's + // perspective, they'd click the "check answer" button and nothing + // would visibly happen. Returning "invalid" is slightly nicer, and has + // a similar effect (blocking learner progress). + + // Arrange + const userInput: PerseusGrapherUserInput = { + type: "linear", + coords: [ + [-10, -10], + [10, 10], + ], + }; + + const rubric: PerseusGrapherRubric = { + correct: { + type: "linear", + coords: null, + }, + }; + + // Act + const result = scoreGrapher(userInput, rubric); + + // Assert + expect(result).toHaveInvalidInput(); + }); + it("can be answered correctly", () => { const coords: [Coord, Coord] = [ [-10, -10], diff --git a/packages/perseus/src/widgets/grapher/score-grapher.ts b/packages/perseus/src/widgets/grapher/score-grapher.ts index 87736f0fe0..4344550247 100644 --- a/packages/perseus/src/widgets/grapher/score-grapher.ts +++ b/packages/perseus/src/widgets/grapher/score-grapher.ts @@ -12,6 +12,9 @@ import type {GrapherAnswerTypes} from "@khanacademy/perseus-core"; function getCoefficientsByType( data: GrapherAnswerTypes, ): ReadonlyArray | undefined { + if (data.coords == null) { + return undefined; + } if (data.type === "exponential" || data.type === "logarithm") { const grader = functionForType(data.type); return grader.getCoefficients(data.coords, data.asymptote);