diff --git a/.changeset/poor-numbers-reflect.md b/.changeset/poor-numbers-reflect.md deleted file mode 100644 index 4830ff7a54..0000000000 --- a/.changeset/poor-numbers-reflect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus": patch ---- - -[Numeric Input] - BUGFIX - Adjust color contrast of tooltip text diff --git a/.eslintrc.js b/.eslintrc.js index f8ebfe48d2..64c2e1a093 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable import/no-commonjs */ const fs = require("fs"); const path = require("path"); @@ -139,7 +139,7 @@ module.exports = { { files: ["config/**", "utils/**", "testing/*"], rules: { - "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/no-require-imports": "off", "import/no-commonjs": "off", "import/no-extraneous-dependencies": "off", "import/no-relative-packages": "off", @@ -166,6 +166,18 @@ module.exports = { ], }, }, + { + /** + * .typetest.ts files are used to do "type testing" :mindblown: + * It is common practice in these files to declare variables and + * expressions that are never used. + */ + files: ["*.typetest.ts"], + rules: { + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-unused-expressions": "off", + }, + }, ], rules: { "max-lines": "off", diff --git a/babel.config.js b/babel.config.js index d848874b57..8dfb135f20 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,2 +1,3 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ // eslint-disable-next-line import/no-commonjs module.exports = require("./config/build/babel.config"); diff --git a/config/test/custom-matchers.ts b/config/test/custom-matchers.ts index 5906790cb8..afddd66c24 100644 --- a/config/test/custom-matchers.ts +++ b/config/test/custom-matchers.ts @@ -19,56 +19,17 @@ declare global { } } -type PerseusRenderer = { - guessAndScore: () => [Array, PerseusScore]; -}; - -type Answerable = PerseusRenderer | PerseusScore; - -function isRenderer(obj: Answerable): obj is PerseusRenderer { - // @ts-expect-error - TS(2339) - TS is annoying - return obj?.guessAndScore !== undefined; -} - -function check(answerable: Answerable) { - let widgetState: string = ""; - let score: PerseusScore; - - if (isRenderer(answerable)) { - const result = answerable.guessAndScore(); - widgetState = JSON.stringify(result[0]); - score = result[1]; - } else { - score = answerable; - } - - return {widgetState, score}; -} - -function maybeAddState(message: string, widgetState: string): string { - if (!widgetState) { - return message; - } - - return message + `; widget state: ${widgetState}`; -} - expect.extend({ toHaveBeenAnsweredCorrectly( - answerable: Answerable, + score: PerseusScore, options: { shouldHavePoints: boolean; }, ) { const shouldHavePoints = options?.shouldHavePoints ?? true; - const {widgetState, score} = check(answerable); if (score.type === "invalid") { - const errMessage = maybeAddState( - `Invalid answer: ${score.message || "(no message)"}`, - widgetState, - ); - + const errMessage = `Invalid answer: ${score.message || "(no message)"}`; return { pass: false, message: () => errMessage, @@ -83,10 +44,7 @@ expect.extend({ } if (score.earned !== score.total) { - const errMessage = maybeAddState( - "Problem was answered incorrectly", - widgetState, - ); + const errMessage = "Problem was answered incorrectly"; return { pass: false, @@ -95,20 +53,14 @@ expect.extend({ } if (shouldHavePoints && score.total < 1) { - const errMessage = maybeAddState( - "Score did not have any points", - widgetState, - ); + const errMessage = "Score did not have any points"; return { pass: false, message: () => errMessage, }; } else if (!shouldHavePoints && score.total > 0) { - const errMessage = maybeAddState( - "Score had points when it shouldn't have", - widgetState, - ); + const errMessage = "Score had points when it shouldn't have"; return { pass: false, @@ -119,14 +71,9 @@ expect.extend({ return {pass: true, message: () => ""}; }, - toHaveInvalidInput(answerable: Answerable, message: string | null) { - const {widgetState, score} = check(answerable); - + toHaveInvalidInput(score: PerseusScore, message: string | null) { if (score.type !== "invalid") { - const errMessage = maybeAddState( - `Answer state is not invalid. Score: ${JSON.stringify(score)}`, - widgetState, - ); + const errMessage = `Answer state is not invalid. Score: ${JSON.stringify(score)}`; return { pass: false, @@ -135,12 +82,9 @@ expect.extend({ } if (message && (!score.message || !score.message.includes(message))) { - const errMessage = maybeAddState( - `Message shown for invalid input did not include "${message}": ${ - score.message || "(no message)" - }. Score: ${JSON.stringify(score)}`, - widgetState, - ); + const errMessage = `Message shown for invalid input did not include "${message}": ${ + score.message || "(no message)" + }. Score: ${JSON.stringify(score)}`; return { pass: false, @@ -151,14 +95,9 @@ expect.extend({ return {pass: true, message: () => ""}; }, - toHaveBeenAnsweredIncorrectly(answerable: Answerable) { - const {widgetState, score} = check(answerable); - + toHaveBeenAnsweredIncorrectly(score: PerseusScore) { if (score.type === "invalid") { - const errMessage = maybeAddState( - `Invalid answer: ${score.message || "(no message)"}`, - widgetState, - ); + const errMessage = `Invalid answer: ${score.message || "(no message)"}`; return { pass: false, @@ -179,10 +118,7 @@ expect.extend({ if (score.earned !== 0) { return { pass: false, - message: () => - `Problem was answered correctly. Widget state: ${JSON.stringify( - widgetState, - )}`, + message: () => `Problem was answered correctly.`, }; } diff --git a/dev/CHANGELOG.md b/dev/CHANGELOG.md index 5619529864..d80730a1b3 100644 --- a/dev/CHANGELOG.md +++ b/dev/CHANGELOG.md @@ -1,5 +1,18 @@ # @khanacademy/perseus-dev-ui +## 4.0.4 + +### Patch Changes + +- Updated dependencies [[`f9906728c`](https://github.com/Khan/perseus/commit/f9906728c1a8f09c91c11d824718b8a06d6a7609), [`341d316aa`](https://github.com/Khan/perseus/commit/341d316aa8727ebb9e7fde28fc4e2d8779aa3e82)]: + - @khanacademy/perseus-core@2.0.0 + - @khanacademy/kas@0.4.3 + - @khanacademy/kmath@0.1.17 + - @khanacademy/math-input@21.1.7 + - @khanacademy/perseus-linter@1.2.5 + - @khanacademy/pure-markdown@0.3.14 + - @khanacademy/simple-markdown@0.13.7 + ## 4.0.3 ### Patch Changes diff --git a/dev/package.json b/dev/package.json index b8ec59888d..b7d94bb9e5 100644 --- a/dev/package.json +++ b/dev/package.json @@ -3,7 +3,7 @@ "description": "Perseus dev UI", "author": "Khan Academy", "license": "MIT", - "version": "4.0.3", + "version": "4.0.4", "private": true, "repository": { "type": "git", @@ -14,23 +14,23 @@ "dev": "vite" }, "dependencies": { - "@khanacademy/kas": "^0.4.2", - "@khanacademy/kmath": "^0.1.16", - "@khanacademy/math-input": "^21.1.6", - "@khanacademy/perseus-core": "1.5.3", - "@khanacademy/perseus-linter": "^1.2.4", - "@khanacademy/pure-markdown": "^0.3.13", - "@khanacademy/simple-markdown": "^0.13.6", - "@khanacademy/wonder-blocks-banner": "3.1.14", - "@khanacademy/wonder-blocks-icon": "4.2.0", - "@khanacademy/wonder-blocks-icon-button": "5.6.2", - "@khanacademy/wonder-blocks-link": "6.1.10", - "@khanacademy/wonder-blocks-search-field": "2.3.6", - "@khanacademy/wonder-blocks-timing": "5.0.2", - "@khanacademy/wonder-blocks-tokens": "2.1.0", - "@khanacademy/wonder-blocks-toolbar": "4.0.0", - "@khanacademy/wonder-blocks-tooltip": "2.5.4", - "@khanacademy/wonder-stuff-core": "1.5.4", + "@khanacademy/kas": "^0.4.3", + "@khanacademy/kmath": "^0.1.17", + "@khanacademy/math-input": "^21.1.7", + "@khanacademy/perseus-core": "2.0.0", + "@khanacademy/perseus-linter": "^1.2.5", + "@khanacademy/pure-markdown": "^0.3.14", + "@khanacademy/simple-markdown": "^0.13.7", + "@khanacademy/wonder-blocks-banner": "3.1.7", + "@khanacademy/wonder-blocks-icon": "4.1.3", + "@khanacademy/wonder-blocks-icon-button": "5.3.4", + "@khanacademy/wonder-blocks-link": "6.1.6", + "@khanacademy/wonder-blocks-search-field": "2.2.23", + "@khanacademy/wonder-blocks-timing": "5.0.1", + "@khanacademy/wonder-blocks-tokens": "2.0.0", + "@khanacademy/wonder-blocks-toolbar": "3.0.35", + "@khanacademy/wonder-blocks-tooltip": "2.4.0", + "@khanacademy/wonder-stuff-core": "1.5.2", "@phosphor-icons/core": "^2.0.2" }, "devDependencies": { diff --git a/docs/architecture.md b/docs/architecture.md index 9224910412..b57ea3ab44 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -55,17 +55,6 @@ with a short description and the main Perseus data type it accepts. article, but the scoring is not available outside of the renderer. - - MultiItemRenderer - { _multi: any } - - The `MultiItemRenderer` is a more advanced renderer. It accepts an - object representing a tree of `PerseusRenderer` objects. A `Shape` - object defines the structure of the tree and you must provide a - "callback" object of the same structure which defines how and where - to render each `PerseusRenderer` that appears in the tree. - - Renderer PerseusRenderer diff --git a/jest.config.js b/jest.config.js index b3486f1ea4..a4b55eba52 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,2 +1,3 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable import/no-commonjs */ module.exports = require("./config/test/test.config"); diff --git a/package.json b/package.json index 89ec395396..6b42321113 100644 --- a/package.json +++ b/package.json @@ -61,8 +61,8 @@ "@types/react": "~18.2.64", "@types/react-dom": "~18.2.19", "@types/underscore": "^1.11.4", - "@typescript-eslint/eslint-plugin": "^7.3.1", - "@typescript-eslint/parser": "^7.3.1", + "@typescript-eslint/eslint-plugin": "^8.18.0", + "@typescript-eslint/parser": "^8.18.0", "ancesdir": "^3.1.0", "babel-jest": "^29.7.0", "babel-loader": "^9.1.3", @@ -73,7 +73,7 @@ "cypress-jest-adapter": "^0.1.1", "cypress-real-events": "^1.12.0", "cypress-wait-until": "^3.0.1", - "eslint": "^8.57.0", + "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-typescript": "^3.5.5", @@ -81,7 +81,7 @@ "eslint-plugin-disable": "^2.0.3", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jest": "27.9.0", + "eslint-plugin-jest": "28.9.0", "eslint-plugin-jsdoc": "^48.2.1", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-monorepo": "^0.3.2", diff --git a/packages/kas/.eslintrc.js b/packages/kas/.eslintrc.js index 09c8638f13..6c26fa1595 100644 --- a/packages/kas/.eslintrc.js +++ b/packages/kas/.eslintrc.js @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable import/no-commonjs */ const path = require("path"); diff --git a/packages/kas/CHANGELOG.md b/packages/kas/CHANGELOG.md index 2510c98ba5..d686d05f1e 100644 --- a/packages/kas/CHANGELOG.md +++ b/packages/kas/CHANGELOG.md @@ -1,5 +1,14 @@ # @khanacademy/kas +## 0.4.3 + +### Patch Changes + +- [#1971](https://github.com/Khan/perseus/pull/1971) [`341d316aa`](https://github.com/Khan/perseus/commit/341d316aa8727ebb9e7fde28fc4e2d8779aa3e82) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Move to using optional chaining in a few places to resolve new lint rule violations. + +- Updated dependencies [[`f9906728c`](https://github.com/Khan/perseus/commit/f9906728c1a8f09c91c11d824718b8a06d6a7609), [`341d316aa`](https://github.com/Khan/perseus/commit/341d316aa8727ebb9e7fde28fc4e2d8779aa3e82)]: + - @khanacademy/perseus-core@2.0.0 + ## 0.4.2 ### Patch Changes diff --git a/packages/kas/package.json b/packages/kas/package.json index 238809566c..ea34a093d4 100644 --- a/packages/kas/package.json +++ b/packages/kas/package.json @@ -3,7 +3,7 @@ "description": "A lightweight JavaScript CAS for comparing expressions and equations.", "author": "Khan Academy", "license": "MIT", - "version": "0.4.2", + "version": "0.4.3", "publishConfig": { "access": "public" }, @@ -26,7 +26,7 @@ "test": "bash -c 'yarn --silent --cwd \"../..\" test ${@:0} $($([[ ${@: -1} = -* ]] || [[ ${@: -1} = bash ]]) && echo $PWD)'" }, "dependencies": { - "@khanacademy/perseus-core": "1.5.3" + "@khanacademy/perseus-core": "2.0.0" }, "devDependencies": { "jison": "0.4.15", diff --git a/packages/kas/src/nodes.ts b/packages/kas/src/nodes.ts index 9d33c1fe05..12a6d5a4d0 100644 --- a/packages/kas/src/nodes.ts +++ b/packages/kas/src/nodes.ts @@ -181,7 +181,7 @@ abstract class Expr { // @ts-expect-error: TypeScript doesn't want to unify // `Function` with the `compile`'s return type. return new Function("vars", "return " + code + ";"); - } catch (e) { + } catch { throw new Error("Function did not compile: " + code); } } diff --git a/packages/kas/src/parser-generator.ts b/packages/kas/src/parser-generator.ts index 2559c6f45c..40066dc921 100755 --- a/packages/kas/src/parser-generator.ts +++ b/packages/kas/src/parser-generator.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable import/no-commonjs */ /* eslint-disable prettier/prettier */ diff --git a/packages/keypad-context/.eslintrc.js b/packages/keypad-context/.eslintrc.js index 09c8638f13..6c26fa1595 100644 --- a/packages/keypad-context/.eslintrc.js +++ b/packages/keypad-context/.eslintrc.js @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable import/no-commonjs */ const path = require("path"); diff --git a/packages/keypad-context/CHANGELOG.md b/packages/keypad-context/CHANGELOG.md index e14fda793f..91bdc313e3 100644 --- a/packages/keypad-context/CHANGELOG.md +++ b/packages/keypad-context/CHANGELOG.md @@ -1,5 +1,14 @@ # @khanacademy/keypad-context +## 1.0.5 + +### Patch Changes + +- [#1971](https://github.com/Khan/perseus/pull/1971) [`341d316aa`](https://github.com/Khan/perseus/commit/341d316aa8727ebb9e7fde28fc4e2d8779aa3e82) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Move to using optional chaining in a few places to resolve new lint rule violations. + +- Updated dependencies [[`f9906728c`](https://github.com/Khan/perseus/commit/f9906728c1a8f09c91c11d824718b8a06d6a7609), [`341d316aa`](https://github.com/Khan/perseus/commit/341d316aa8727ebb9e7fde28fc4e2d8779aa3e82)]: + - @khanacademy/perseus-core@2.0.0 + ## 1.0.4 ### Patch Changes diff --git a/packages/keypad-context/package.json b/packages/keypad-context/package.json index b912fed47e..435a4143af 100644 --- a/packages/keypad-context/package.json +++ b/packages/keypad-context/package.json @@ -3,7 +3,7 @@ "description": "Perseus keypad context", "author": "Khan Academy", "license": "MIT", - "version": "1.0.4", + "version": "1.0.5", "publishConfig": { "access": "public" }, @@ -25,7 +25,7 @@ "test": "bash -c 'yarn --silent --cwd \"../..\" test ${@:0} $($([[ ${@: -1} = -* ]] || [[ ${@: -1} = bash ]]) && echo $PWD)'" }, "dependencies": { - "@khanacademy/perseus-core": "1.5.3" + "@khanacademy/perseus-core": "2.0.0" }, "devDependencies": { "react": "^18.2.0" diff --git a/packages/kmath/.eslintrc.js b/packages/kmath/.eslintrc.js index 09c8638f13..6c26fa1595 100644 --- a/packages/kmath/.eslintrc.js +++ b/packages/kmath/.eslintrc.js @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable import/no-commonjs */ const path = require("path"); diff --git a/packages/kmath/CHANGELOG.md b/packages/kmath/CHANGELOG.md index 7ca3e8f206..3983b1a984 100644 --- a/packages/kmath/CHANGELOG.md +++ b/packages/kmath/CHANGELOG.md @@ -1,5 +1,14 @@ # @khanacademy/kmath +## 0.1.17 + +### Patch Changes + +- [#1971](https://github.com/Khan/perseus/pull/1971) [`341d316aa`](https://github.com/Khan/perseus/commit/341d316aa8727ebb9e7fde28fc4e2d8779aa3e82) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Move to using optional chaining in a few places to resolve new lint rule violations. + +- Updated dependencies [[`f9906728c`](https://github.com/Khan/perseus/commit/f9906728c1a8f09c91c11d824718b8a06d6a7609), [`341d316aa`](https://github.com/Khan/perseus/commit/341d316aa8727ebb9e7fde28fc4e2d8779aa3e82)]: + - @khanacademy/perseus-core@2.0.0 + ## 0.1.16 ### Patch Changes diff --git a/packages/kmath/package.json b/packages/kmath/package.json index 7771b68c8f..5b9b040910 100644 --- a/packages/kmath/package.json +++ b/packages/kmath/package.json @@ -3,7 +3,7 @@ "description": "Khan Academy's Javascript Numeric Math Utilities", "author": "Khan Academy", "license": "MIT", - "version": "0.1.16", + "version": "0.1.17", "publishConfig": { "access": "public" }, @@ -24,7 +24,7 @@ "test": "bash -c 'yarn --silent --cwd \"../..\" test ${@:0} $($([[ ${@: -1} = -* ]] || [[ ${@: -1} = bash ]]) && echo $PWD)'" }, "dependencies": { - "@khanacademy/perseus-core": "1.5.3" + "@khanacademy/perseus-core": "2.0.0" }, "devDependencies": { "perseus-build-settings": "^0.4.1", diff --git a/packages/math-input/.eslintrc.js b/packages/math-input/.eslintrc.js index 687012161a..c859a84c08 100644 --- a/packages/math-input/.eslintrc.js +++ b/packages/math-input/.eslintrc.js @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable import/no-commonjs */ const path = require("path"); diff --git a/packages/math-input/CHANGELOG.md b/packages/math-input/CHANGELOG.md index 157421927c..4c0c10151c 100644 --- a/packages/math-input/CHANGELOG.md +++ b/packages/math-input/CHANGELOG.md @@ -1,5 +1,15 @@ # @khanacademy/math-input +## 21.1.7 + +### Patch Changes + +- [#1971](https://github.com/Khan/perseus/pull/1971) [`341d316aa`](https://github.com/Khan/perseus/commit/341d316aa8727ebb9e7fde28fc4e2d8779aa3e82) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Move to using optional chaining in a few places to resolve new lint rule violations. + +- Updated dependencies [[`f9906728c`](https://github.com/Khan/perseus/commit/f9906728c1a8f09c91c11d824718b8a06d6a7609), [`341d316aa`](https://github.com/Khan/perseus/commit/341d316aa8727ebb9e7fde28fc4e2d8779aa3e82)]: + - @khanacademy/perseus-core@2.0.0 + - @khanacademy/keypad-context@1.0.5 + ## 21.1.6 ### Patch Changes diff --git a/packages/math-input/package.json b/packages/math-input/package.json index 9d2958f533..7b72dd1f32 100644 --- a/packages/math-input/package.json +++ b/packages/math-input/package.json @@ -3,7 +3,7 @@ "description": "Khan Academy's new expression editor for the mobile web.", "author": "Khan Academy", "license": "MIT", - "version": "21.1.6", + "version": "21.1.7", "publishConfig": { "access": "public" }, @@ -38,8 +38,8 @@ ], "scripts": {}, "dependencies": { - "@khanacademy/keypad-context": "^1.0.4", - "@khanacademy/perseus-core": "1.5.3", + "@khanacademy/keypad-context": "^1.0.5", + "@khanacademy/perseus-core": "2.0.0", "mathquill": "https://github.com/Khan/mathquill/releases/download/v1.0.0/mathquill-v1.0.0.tgz" }, "devDependencies": { diff --git a/packages/math-input/src/components/input/math-input.tsx b/packages/math-input/src/components/input/math-input.tsx index 8114cd5fc0..2a9f5cfb52 100644 --- a/packages/math-input/src/components/input/math-input.tsx +++ b/packages/math-input/src/components/input/math-input.tsx @@ -108,8 +108,7 @@ class MathInput extends React.Component { // this `MathInput` component in an intermediary component // that translates accesses on the keypad into vanilla props, // to make this input keypad-agnostic. - this.props.keypadElement && - this.props.keypadElement.setCursor(cursor); + this.props.keypadElement?.setCursor(cursor); }, }, ); @@ -228,7 +227,7 @@ class MathInput extends React.Component { // in which case we don't want to dismiss the keypad on check. if (!isWithinKeypadBounds(x, y)) { this.blur(); - this.props.onBlur && this.props.onBlur(); + this.props.onBlur?.(); } } } @@ -613,10 +612,9 @@ class MathInput extends React.Component { cursor.insAtLeftEnd(this.mathField.mathField.controller().root); } // In that event, we need to update the cursor context ourselves. - this.props.keypadElement && - this.props.keypadElement.setCursor({ - context: this.mathField.contextForCursor(), - }); + this.props.keypadElement?.setCursor({ + context: this.mathField.contextForCursor(), + }); }; handleTouchStart = ( diff --git a/packages/math-input/src/components/input/mathquill-instance.ts b/packages/math-input/src/components/input/mathquill-instance.ts index c90f4203a0..43e6d1ff7b 100644 --- a/packages/math-input/src/components/input/mathquill-instance.ts +++ b/packages/math-input/src/components/input/mathquill-instance.ts @@ -132,6 +132,7 @@ export function createMathField( // HTTP request to fetch non-english speech rules, and cannot be easily // mocked in consuming packages now that we do not bundle source code. // When it eventually times out, it will cause arbitrary test failures. + // eslint-disable-next-line @typescript-eslint/no-unused-expressions !inJest && SpeechRuleEngine.setup(locale).then((SRE) => mathField.setMathspeakOverride(SRE.texToSpeech), diff --git a/packages/math-input/src/enums.ts b/packages/math-input/src/enums.ts index e9c32a12c1..73b73b4e03 100644 --- a/packages/math-input/src/enums.ts +++ b/packages/math-input/src/enums.ts @@ -7,6 +7,7 @@ export enum KeypadType { EXPRESSION = "EXPRESSION", } +// eslint-disable-next-line @typescript-eslint/no-unused-vars const KeyTypes = [ "EMPTY", // For numerals, variables, and any other characters that themselves diff --git a/packages/perseus-core/.eslintrc.js b/packages/perseus-core/.eslintrc.js index 09c8638f13..6c26fa1595 100644 --- a/packages/perseus-core/.eslintrc.js +++ b/packages/perseus-core/.eslintrc.js @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable import/no-commonjs */ const path = require("path"); diff --git a/packages/perseus-core/CHANGELOG.md b/packages/perseus-core/CHANGELOG.md index 256d57f13f..c07316cc35 100644 --- a/packages/perseus-core/CHANGELOG.md +++ b/packages/perseus-core/CHANGELOG.md @@ -1,5 +1,15 @@ # @khanacademy/perseus-core +## 2.0.0 + +### Major Changes + +- [#1973](https://github.com/Khan/perseus/pull/1973) [`f9906728c`](https://github.com/Khan/perseus/commit/f9906728c1a8f09c91c11d824718b8a06d6a7609) Thanks [@handeyeco](https://github.com/handeyeco)! - Remove `scoreInput` from ServerItemRenderer + +### Patch Changes + +- [#1971](https://github.com/Khan/perseus/pull/1971) [`341d316aa`](https://github.com/Khan/perseus/commit/341d316aa8727ebb9e7fde28fc4e2d8779aa3e82) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Move to using optional chaining in a few places to resolve new lint rule violations. + ## 1.5.3 ### Patch Changes diff --git a/packages/perseus-core/package.json b/packages/perseus-core/package.json index efda63cd93..91219870f4 100644 --- a/packages/perseus-core/package.json +++ b/packages/perseus-core/package.json @@ -3,7 +3,7 @@ "description": "Shared Perseus infrastructure", "author": "Khan Academy", "license": "MIT", - "version": "1.5.3", + "version": "2.0.0", "publishConfig": { "access": "public" }, diff --git a/packages/perseus-core/src/types.ts b/packages/perseus-core/src/types.ts index f9b93fd731..2e71b875bd 100644 --- a/packages/perseus-core/src/types.ts +++ b/packages/perseus-core/src/types.ts @@ -14,7 +14,6 @@ type State = any; export interface RendererInterface { getSerializedState(): State; restoreSerializedState(state: State, callback?: () => void): void; - scoreInput(): KEScore; blur(): void; focus(): boolean | null | undefined; props: any; diff --git a/packages/perseus-editor/.eslintrc.js b/packages/perseus-editor/.eslintrc.js index 09c8638f13..6c26fa1595 100644 --- a/packages/perseus-editor/.eslintrc.js +++ b/packages/perseus-editor/.eslintrc.js @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable import/no-commonjs */ const path = require("path"); diff --git a/packages/perseus-editor/CHANGELOG.md b/packages/perseus-editor/CHANGELOG.md index 91941befa8..0baf71f92b 100644 --- a/packages/perseus-editor/CHANGELOG.md +++ b/packages/perseus-editor/CHANGELOG.md @@ -1,5 +1,58 @@ # @khanacademy/perseus-editor +## 16.0.3 + +### Patch Changes + +- Updated dependencies [[`cc9d3a4bc`](https://github.com/Khan/perseus/commit/cc9d3a4bc8866bbdaa09e2bf3fdb321fb9a125c1), [`2748a1ff8`](https://github.com/Khan/perseus/commit/2748a1ff82b5e04b12f6b740b29b5e01a0561882)]: + - @khanacademy/perseus@48.1.0 + +## 16.0.2 + +### Patch Changes + +- [#1985](https://github.com/Khan/perseus/pull/1985) [`701895c01`](https://github.com/Khan/perseus/commit/701895c01bd2792a4dccc6ce0d5edc23f7483db3) Thanks [@nishasy](https://github.com/nishasy)! - [Locked Figure Labels] Update placeholder to include TeX \$s. Start new visible labels with text "label". + +* [#1971](https://github.com/Khan/perseus/pull/1971) [`341d316aa`](https://github.com/Khan/perseus/commit/341d316aa8727ebb9e7fde28fc4e2d8779aa3e82) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Move to using optional chaining in a few places to resolve new lint rule violations. + +- [#1984](https://github.com/Khan/perseus/pull/1984) [`990a3170c`](https://github.com/Khan/perseus/commit/990a3170c2111ddc2c6e6f8f6420a4908cc71c82) Thanks [@catandthemachines](https://github.com/catandthemachines)! - Fixed crashing behavior that occurs in when selecting "unlimited" in polygon option of interactive graph editor. + +- Updated dependencies [[`f9906728c`](https://github.com/Khan/perseus/commit/f9906728c1a8f09c91c11d824718b8a06d6a7609), [`f9906728c`](https://github.com/Khan/perseus/commit/f9906728c1a8f09c91c11d824718b8a06d6a7609), [`341d316aa`](https://github.com/Khan/perseus/commit/341d316aa8727ebb9e7fde28fc4e2d8779aa3e82), [`990a3170c`](https://github.com/Khan/perseus/commit/990a3170c2111ddc2c6e6f8f6420a4908cc71c82)]: + - @khanacademy/perseus@48.0.0 + - @khanacademy/perseus-core@2.0.0 + - @khanacademy/kas@0.4.3 + - @khanacademy/keypad-context@1.0.5 + - @khanacademy/kmath@0.1.17 + - @khanacademy/math-input@21.1.7 + - @khanacademy/pure-markdown@0.3.14 + +## 16.0.1 + +### Patch Changes + +- Updated dependencies [[`335615bab`](https://github.com/Khan/perseus/commit/335615bab18685aa6331c7628c1225bdecc54aab), [`acd8bd566`](https://github.com/Khan/perseus/commit/acd8bd56664c6a0849bf3d532be8978115a97dfd)]: + - @khanacademy/perseus@47.0.1 + +## 16.0.0 + +### Major Changes + +- [#1955](https://github.com/Khan/perseus/pull/1955) [`e7b4db0bf`](https://github.com/Khan/perseus/commit/e7b4db0bf193241a36508804dd6e58c729f0a3db) Thanks [@handeyeco](https://github.com/handeyeco)! - Remove support for MultiRenderer + +### Patch Changes + +- Updated dependencies [[`e7b4db0bf`](https://github.com/Khan/perseus/commit/e7b4db0bf193241a36508804dd6e58c729f0a3db)]: + - @khanacademy/perseus@47.0.0 + +## 15.1.4 + +### Patch Changes + +- [#1976](https://github.com/Khan/perseus/pull/1976) [`2ad163b5e`](https://github.com/Khan/perseus/commit/2ad163b5ea20d40fb9f0edf30e03cd54ecf9bf31) Thanks [@nishasy](https://github.com/nishasy)! - [Locked Figures Aria] Update the auto-generated text to spell out commas + +- Updated dependencies [[`e22a931d9`](https://github.com/Khan/perseus/commit/e22a931d987291258b66f2c80db3536970a4555d)]: + - @khanacademy/perseus@46.0.1 + ## 15.1.3 ### Patch Changes diff --git a/packages/perseus-editor/package.json b/packages/perseus-editor/package.json index c7fa87a124..bca51f7e2d 100644 --- a/packages/perseus-editor/package.json +++ b/packages/perseus-editor/package.json @@ -3,7 +3,7 @@ "description": "Perseus editors", "author": "Khan Academy", "license": "MIT", - "version": "15.1.3", + "version": "16.0.3", "publishConfig": { "access": "public" }, @@ -34,32 +34,32 @@ "test": "bash -c 'yarn --silent --cwd \"../..\" test ${@:0} $($([[ ${@: -1} = -* ]] || [[ ${@: -1} = bash ]]) && echo $PWD)'" }, "dependencies": { - "@khanacademy/kas": "^0.4.2", - "@khanacademy/keypad-context": "^1.0.4", - "@khanacademy/kmath": "^0.1.16", - "@khanacademy/math-input": "^21.1.6", - "@khanacademy/perseus": "^46.0.0", - "@khanacademy/perseus-core": "1.5.3", - "@khanacademy/pure-markdown": "^0.3.13", + "@khanacademy/kas": "^0.4.3", + "@khanacademy/keypad-context": "^1.0.5", + "@khanacademy/kmath": "^0.1.17", + "@khanacademy/math-input": "^21.1.7", + "@khanacademy/perseus": "^48.1.0", + "@khanacademy/perseus-core": "2.0.0", + "@khanacademy/pure-markdown": "^0.3.14", "mafs": "^0.19.0" }, "devDependencies": { - "@khanacademy/perseus-linter": "^1.2.4", - "@khanacademy/wonder-blocks-accordion": "1.3.10", - "@khanacademy/wonder-blocks-banner": "3.1.14", - "@khanacademy/wonder-blocks-button": "6.3.12", - "@khanacademy/wonder-blocks-clickable": "4.2.9", - "@khanacademy/wonder-blocks-core": "7.0.1", - "@khanacademy/wonder-blocks-dropdown": "5.8.1", - "@khanacademy/wonder-blocks-form": "4.11.0", - "@khanacademy/wonder-blocks-icon": "4.2.0", - "@khanacademy/wonder-blocks-icon-button": "5.6.2", - "@khanacademy/wonder-blocks-switch": "1.1.24", - "@khanacademy/wonder-blocks-timing": "5.0.2", - "@khanacademy/wonder-blocks-tokens": "2.1.0", - "@khanacademy/wonder-blocks-tooltip": "2.5.4", - "@khanacademy/wonder-blocks-typography": "2.1.16", - "@khanacademy/wonder-stuff-core": "1.5.4", + "@khanacademy/perseus-linter": "^1.2.5", + "@khanacademy/wonder-blocks-accordion": "1.3.6", + "@khanacademy/wonder-blocks-banner": "3.1.7", + "@khanacademy/wonder-blocks-button": "6.3.8", + "@khanacademy/wonder-blocks-clickable": "4.2.6", + "@khanacademy/wonder-blocks-core": "6.4.3", + "@khanacademy/wonder-blocks-dropdown": "5.4.5", + "@khanacademy/wonder-blocks-form": "4.9.0", + "@khanacademy/wonder-blocks-icon": "4.1.3", + "@khanacademy/wonder-blocks-icon-button": "5.3.4", + "@khanacademy/wonder-blocks-switch": "1.1.20", + "@khanacademy/wonder-blocks-timing": "5.0.1", + "@khanacademy/wonder-blocks-tokens": "2.0.0", + "@khanacademy/wonder-blocks-tooltip": "2.4.0", + "@khanacademy/wonder-blocks-typography": "2.1.14", + "@khanacademy/wonder-stuff-core": "1.5.2", "@phosphor-icons/core": "^2.0.2", "aphrodite": "^1.2.5", "classnames": "1.1.4", @@ -73,21 +73,21 @@ "underscore": "^1.4.4" }, "peerDependencies": { - "@khanacademy/wonder-blocks-accordion": "1.3.10", - "@khanacademy/wonder-blocks-banner": "3.1.14", - "@khanacademy/wonder-blocks-button": "6.3.12", - "@khanacademy/wonder-blocks-clickable": "4.2.9", - "@khanacademy/wonder-blocks-core": "7.0.1", - "@khanacademy/wonder-blocks-dropdown": "5.8.1", - "@khanacademy/wonder-blocks-form": "4.11.0", - "@khanacademy/wonder-blocks-icon": "4.2.0", - "@khanacademy/wonder-blocks-icon-button": "5.6.2", - "@khanacademy/wonder-blocks-switch": "1.1.24", - "@khanacademy/wonder-blocks-timing": "5.0.2", - "@khanacademy/wonder-blocks-tokens": "2.1.0", - "@khanacademy/wonder-blocks-tooltip": "2.5.4", - "@khanacademy/wonder-blocks-typography": "2.1.16", - "@khanacademy/wonder-stuff-core": "1.5.4", + "@khanacademy/wonder-blocks-accordion": "1.3.6", + "@khanacademy/wonder-blocks-banner": "3.1.7", + "@khanacademy/wonder-blocks-button": "6.3.8", + "@khanacademy/wonder-blocks-clickable": "4.2.6", + "@khanacademy/wonder-blocks-core": "6.4.3", + "@khanacademy/wonder-blocks-dropdown": "5.4.5", + "@khanacademy/wonder-blocks-form": "4.9.0", + "@khanacademy/wonder-blocks-icon": "4.1.3", + "@khanacademy/wonder-blocks-icon-button": "5.3.4", + "@khanacademy/wonder-blocks-switch": "1.1.20", + "@khanacademy/wonder-blocks-timing": "5.0.1", + "@khanacademy/wonder-blocks-tokens": "2.0.0", + "@khanacademy/wonder-blocks-tooltip": "2.4.0", + "@khanacademy/wonder-blocks-typography": "2.1.14", + "@khanacademy/wonder-stuff-core": "1.5.2", "@phosphor-icons/core": "^2.0.2", "aphrodite": "^1.2.5", "classnames": "1.1.4", diff --git a/packages/perseus-editor/src/__stories__/flags-for-api-options.ts b/packages/perseus-editor/src/__stories__/flags-for-api-options.ts index c67bf16c7a..ab9acbb7fd 100644 --- a/packages/perseus-editor/src/__stories__/flags-for-api-options.ts +++ b/packages/perseus-editor/src/__stories__/flags-for-api-options.ts @@ -8,10 +8,12 @@ export const flags = { quadratic: true, sinusoid: true, polygon: true, + "unlimited-polygon": true, linear: true, "linear-system": true, ray: true, point: true, + "unlimited-point": true, none: true, // Locked figures flags diff --git a/packages/perseus-editor/src/__tests__/i18n.test.ts b/packages/perseus-editor/src/__tests__/i18n.test.ts deleted file mode 100644 index d909731be4..0000000000 --- a/packages/perseus-editor/src/__tests__/i18n.test.ts +++ /dev/null @@ -1,501 +0,0 @@ -import {Dependencies} from "@khanacademy/perseus"; -import _ from "underscore"; - -import {testDependencies} from "../../../../testing/test-dependencies"; -import i18n from "../i18n"; -import {registerAllWidgetsAndEditorsForTesting} from "../util/register-all-widgets-and-editors-for-testing"; - -const exerciseImagesEverywhere = { - question: { - content: - "![question-a](question-a)\n\n![question-b](question-b)\n\n[[☃ categorizer 1]]\n\n[[☃ group 1]]\n\n[[☃ image 1]]\n\n[[☃ matcher 1]]\n\n[[☃ matrix 1]]\n\n[[☃ orderer 1]]\n\n[[☃ passage 1]]\n\n[[☃ radio 1]]\n\n[[☃ sorter 1]]\n\n[[☃ table 1]]\n\n[[☃ grapher 1]]\n\n[[☃ interactive-graph 1]]\n\n[[☃ measurer 1]]\n\n[[☃ plotter 1]]", - images: {}, - widgets: { - "categorizer 1": { - type: "categorizer", - graded: true, - options: { - items: [ - "![category-item-a](category-item-a)", - "![category-item-b](category-item-b)", - ], - categories: [ - "![category-category-a](category-category-a)", - "![category-category-b](category-category-b)", - ], - values: [], - randomizeItems: false, - }, - version: {major: 0, minor: 0}, - }, - "group 1": { - type: "group", - graded: true, - options: { - content: - "![group-a](group-a)\n\n![group-b](group-b)\n\n[[☃ image 1]]", - images: {}, - widgets: { - "image 1": { - type: "image", - graded: true, - options: { - title: "![group-image-title-a](group-image-title-a) ![group-image-title-b](group-image-title-b)", - range: [ - [0, 10], - [0, 10], - ], - box: [0, 0], - backgroundImage: { - url: "group-image-a", - width: 0, - height: 0, - }, - labels: [], - alt: "", - caption: - "![group-image-caption-a](group-image-caption-a) ![group-image-caption-b](group-image-caption-b)", - }, - version: {major: 0, minor: 0}, - }, - }, - }, - version: {major: 0, minor: 0}, - }, - "image 1": { - type: "image", - graded: true, - options: { - title: "![image-title-a](image-title-a) ![image-title-b](image-title-b)", - range: [ - [0, 10], - [0, 10], - ], - box: [0, 0], - backgroundImage: {url: "image-a", width: 0, height: 0}, - labels: [], - alt: "", - caption: - "![image-caption-a](image-caption-a) ![image-caption-b](image-caption-b)", - }, - version: {major: 0, minor: 0}, - }, - "matcher 1": { - type: "matcher", - graded: true, - options: { - left: [ - "![matcher-left-a](matcher-left-a)", - "![matcher-left-b](matcher-left-b)", - ], - right: [ - "![matcher-right-a](matcher-right-a)", - "![matcher-right-b](matcher-right-b)", - ], - labels: [ - "![matcher-label-a](matcher-label-a)", - "![matcher-label-b](matcher-label-b)", - ], - orderMatters: false, - padding: true, - }, - version: {major: 0, minor: 0}, - }, - "matrix 1": { - type: "matrix", - graded: true, - options: { - matrixBoardSize: [3, 3], - answers: [[]], - prefix: "![matrix-prefix-a](matrix-prefix-a) ![matrix-prefix-b](matrix-prefix-b)", - suffix: "![matrix-suffix-a](matrix-suffix-a) ![matrix-suffix-b](matrix-suffix-b)", - cursorPosition: [0, 0], - }, - version: {major: 0, minor: 0}, - }, - "orderer 1": { - type: "orderer", - graded: true, - options: { - options: [ - {content: "![orderer-correct-a](orderer-correct-a)"}, - {content: "![orderer-correct-b](orderer-correct-b)"}, - {content: "![orderer-other-a](orderer-other-a)"}, - {content: "![orderer-other-b](orderer-other-b)"}, - ], - correctOptions: [ - {content: "![orderer-correct-a](orderer-correct-a)"}, - {content: "![orderer-correct-b](orderer-correct-b)"}, - ], - otherOptions: [ - {content: "![orderer-other-a](orderer-other-a)"}, - {content: "![orderer-other-b](orderer-other-b)"}, - ], - height: "normal", - layout: "horizontal", - }, - version: {major: 0, minor: 0}, - }, - "passage 1": { - type: "passage", - graded: true, - options: { - passageTitle: - "![passage-title-a](passage-title-a) ![passage-title-b](passage-title-b)", - passageText: "", - footnotes: "", - showLineNumbers: true, - }, - version: {major: 0, minor: 0}, - }, - "radio 1": { - type: "radio", - graded: true, - options: { - choices: [ - { - content: - "![radio-choice1-a](radio-choice1-a) ![radio-choice1-b](radio-choice1-b)", - }, - { - content: - "![radio-choice2-a](radio-choice2-a) ![radio-choice2-b](radio-choice2-b)", - }, - ], - randomize: false, - multipleSelect: false, - displayCount: null, - hasNoneOfTheAbove: false, - onePerLine: true, - deselectEnabled: false, - }, - version: {major: 1, minor: 0}, - }, - "sorter 1": { - type: "sorter", - graded: true, - options: { - correct: [ - "![sorter-correct-a](sorter-correct-a)", - "![sorter-correct-b](sorter-correct-b)", - "![sorter-correct-c](sorter-correct-c)", - ], - layout: "horizontal", - padding: true, - }, - version: {major: 0, minor: 0}, - }, - "table 1": { - type: "table", - graded: true, - options: { - headers: [ - "![table-header-a](table-header-a)", - "![table-header-b](table-header-b)", - "![table-header-c](table-header-c)", - ], - rows: 4, - columns: 3, - answers: [ - ["", "", ""], - ["", "", ""], - ["", "", ""], - ["", "", ""], - ], - }, - version: {major: 0, minor: 0}, - }, - "grapher 1": { - type: "grapher", - graded: true, - options: { - correct: {type: "linear", coords: null, asymptote: null}, - availableTypes: ["linear"], - graph: { - editableSettings: ["graph", "snap", "image"], - range: [ - [-10, 10], - [-10, 10], - ], - labels: ["x", "y"], - step: [1, 1], - gridStep: [1, 1], - snapStep: [1, 1], - valid: true, - backgroundImage: { - url: "grapher-a", - width: 32, - height: 32, - }, - markings: "graph", - rulerLabel: "", - rulerTicks: 10, - showProtractor: false, - showRuler: false, - }, - }, - version: {major: 0, minor: 0}, - }, - "interactive-graph 1": { - type: "interactive-graph", - graded: true, - options: { - step: [1, 1], - backgroundImage: { - url: "interactive-graph-a", - width: 32, - height: 32, - }, - markings: "graph", - labels: ["x", "y"], - showProtractor: false, - showRuler: false, - rulerLabel: "", - rulerTicks: 10, - range: [ - [-10, 10], - [-10, 10], - ], - gridStep: [1, 1], - snapStep: [0.5, 0.5], - graph: {type: "linear"}, - correct: {type: "linear", coords: null}, - }, - version: {major: 0, minor: 0}, - }, - "measurer 1": { - type: "measurer", - graded: true, - options: { - box: [480, 480], - image: {url: "measurer-a"}, - showProtractor: true, - showRuler: false, - rulerLabel: "", - rulerTicks: 10, - rulerPixels: 40, - rulerLength: 10, - }, - version: {major: 1, minor: 0}, - }, - "plotter 1": { - type: "plotter", - graded: true, - options: { - correct: [1, 1], - starting: [1, 1], - type: "pic", - labels: ["", ""], - categories: ["a", "a"], - scaleY: 1, - maxY: 10, - snapsPerLine: 2, - labelInterval: 1, - picUrl: "plotter-a", - }, - version: {major: 0, minor: 0}, - }, - }, - }, - answerArea: {calculator: false}, - itemDataVersion: {major: 0, minor: 1}, - hints: [ - { - content: - "![hint1-a](hint1-a)\n\n![hint1-b](hint1-b)\n\n[[☃ image 1]]", - images: {}, - widgets: { - "image 1": { - type: "image", - graded: true, - options: { - title: "![hint1-image-title-a](hint1-image-title-a) ![hint1-image-title-b](hint1-image-title-b)", - range: [ - [0, 10], - [0, 10], - ], - box: [0, 0], - backgroundImage: { - url: "hint1-image-a", - width: 0, - height: 0, - }, - labels: [], - alt: "", - caption: - "![hint1-image-caption-a](hint1-image-caption-a) ![hint1-image-caption-b](hint1-image-caption-b)", - }, - version: {major: 0, minor: 0}, - }, - }, - }, - { - content: - "![hint2-a](hint2-a)\n\n![hint2-b](hint2-b)\n\n[[☃ image 1]]", - images: {}, - widgets: { - "image 1": { - type: "image", - graded: true, - options: { - title: "![hint2-image-title-a](hint2-image-title-a) ![hint2-image-title-b](hint2-image-title-b)", - range: [ - [0, 10], - [0, 10], - ], - box: [0, 0], - backgroundImage: { - url: "hint2-image-a", - width: 0, - height: 0, - }, - labels: [], - alt: "", - caption: - "![hint2-image-caption-a](hint2-image-caption-a) ![hint2-image-caption-b](hint2-image-caption-b)", - }, - version: {major: 0, minor: 0}, - }, - }, - }, - ], -} as const; - -// Article perseus format is very similar to exercises and should parse the -// same way, but should just be a list of renders. So we reuse the test data -// from exerciseImagesEverywhere, but include the question and hints as if they -// were paragraphs in the article. -const articleImagesEverywhere = [exerciseImagesEverywhere.question].concat( - // @ts-expect-error - TS2769 - No overload matches this call. - exerciseImagesEverywhere.hints, -); - -// All of the images that are in the `exerciseImagesEverywhere` item. Also the -// same list of images that are in the `articleImagesEverywhere` item. -const allImages = [ - "question-a", - "question-b", - "category-category-a", - "category-category-b", - "category-item-a", - "category-item-b", - "group-a", - "group-b", - "group-image-title-a", - "group-image-title-b", - "group-image-a", - "group-image-caption-a", - "group-image-caption-b", - "image-title-a", - "image-title-b", - "image-a", - "image-caption-a", - "image-caption-b", - "matcher-label-a", - "matcher-label-b", - "matcher-left-a", - "matcher-left-b", - "matcher-right-a", - "matcher-right-b", - "matrix-prefix-a", - "matrix-prefix-b", - "matrix-suffix-a", - "matrix-suffix-b", - "orderer-correct-a", - "orderer-correct-b", - "orderer-other-a", - "orderer-other-b", - "passage-title-a", - "passage-title-b", - "radio-choice1-a", - "radio-choice1-b", - "radio-choice2-a", - "radio-choice2-b", - "sorter-correct-a", - "sorter-correct-b", - "sorter-correct-c", - "table-header-a", - "table-header-b", - "table-header-c", - "grapher-a", - "interactive-graph-a", - "measurer-a", - "plotter-a", - "hint1-a", - "hint1-a", - "hint1-image-title-a", - "hint1-image-title-b", - "hint1-image-a", - "hint1-image-caption-a", - "hint1-image-caption-b", - "hint2-a", - "hint2-a", - "hint2-image-title-a", - "hint2-image-title-b", - "hint2-image-a", - "hint2-image-caption-a", - "hint2-image-caption-b", -]; - -describe("i18n", () => { - beforeAll(() => { - registerAllWidgetsAndEditorsForTesting(); - }); - - beforeEach(() => { - jest.spyOn(Dependencies, "getDependencies").mockReturnValue( - testDependencies, - ); - }); - - describe("Exercise image finding", () => { - it("should find all of the images in items", () => { - const foundImages = i18n.findImagesInItemData( - exerciseImagesEverywhere, - ); - - expect(foundImages.length <= allImages.length).toBeTruthy(); - - _.each(allImages, (image) => { - expect(foundImages.indexOf(image) !== -1).toBeTruthy(); - }); - }); - }); - - describe("Article image finding", () => { - it("should find all of the images in items", () => { - const foundImages = i18n.findImagesInArticles( - articleImagesEverywhere, - ); - - expect(foundImages.length <= allImages.length).toBeTruthy(); - - _.each(allImages, (image) => { - expect(foundImages.indexOf(image) !== -1).toBeTruthy(); - }); - }); - }); - - describe("Multi-item image finding", () => { - it("should find all of the images in each leaf node", () => { - const foundImages = i18n.findImagesInItemData({ - _multi: { - question: { - __type: "content", - ...exerciseImagesEverywhere.question, - }, - hints: exerciseImagesEverywhere.hints.map((hint) => ({ - __type: "hint", - ...hint, - })), - }, - }); - - expect(foundImages.length <= allImages.length).toBeTruthy(); - - _.each(allImages, (image) => { - expect(foundImages.indexOf(image) !== -1).toBeTruthy(); - }); - }); - }); -}); diff --git a/packages/perseus-editor/src/components/form-wrapped-text-field.tsx b/packages/perseus-editor/src/components/form-wrapped-text-field.tsx index 5e0db3337b..302c83a790 100644 --- a/packages/perseus-editor/src/components/form-wrapped-text-field.tsx +++ b/packages/perseus-editor/src/components/form-wrapped-text-field.tsx @@ -86,14 +86,14 @@ class FormWrappedTextField extends React.Component { handleBlur = (e: React.ChangeEvent) => { const {onBlur} = this.props; - onBlur && onBlur(e); + onBlur?.(e); this.setState({focused: false}); }; handleFocus = (e: React.ChangeEvent) => { const {onFocus} = this.props; - onFocus && onFocus(e); + onFocus?.(e); this.setState({focused: true}); }; diff --git a/packages/perseus-editor/src/components/graph-settings.tsx b/packages/perseus-editor/src/components/graph-settings.tsx index 483033dba5..27bc39778b 100644 --- a/packages/perseus-editor/src/components/graph-settings.tsx +++ b/packages/perseus-editor/src/components/graph-settings.tsx @@ -92,8 +92,6 @@ const GraphSettings = createReactClass({ }, UNSAFE_componentWillReceiveProps: function (nextProps) { - // Make sure that state updates when switching - // between different items in a multi-item editor. if ( !_.isEqual(this.props.labels, nextProps.labels) || !_.isEqual(this.props.gridStep, nextProps.gridStep) || diff --git a/packages/perseus-editor/src/components/json-editor.tsx b/packages/perseus-editor/src/components/json-editor.tsx index 3c1f3186b9..1b2a0576fc 100644 --- a/packages/perseus-editor/src/components/json-editor.tsx +++ b/packages/perseus-editor/src/components/json-editor.tsx @@ -62,7 +62,7 @@ const JsonEditor: any = createReactClass({ this.props.onChange(json); }, ); - } catch (ex: any) { + } catch { this.setState({ currentValue: nextString, valid: false, @@ -93,7 +93,7 @@ const JsonEditor: any = createReactClass({ this.props.onChange(json); }, ); - } catch (ex: any) { + } catch { this.setState({ currentValue: JSON.stringify(this.props.value, null, 4), valid: true, diff --git a/packages/perseus-editor/src/diffs/__stories__/structured-item-diff.stories.tsx b/packages/perseus-editor/src/diffs/__stories__/structured-item-diff.stories.tsx deleted file mode 100644 index eaa4d79b78..0000000000 --- a/packages/perseus-editor/src/diffs/__stories__/structured-item-diff.stories.tsx +++ /dev/null @@ -1,855 +0,0 @@ -import * as React from "react"; - -import StructuredItemDiff from "../structured-item-diff"; - -import Wrapper from "./perseus-diff-wrapper"; - -import("../../styles/perseus-editor.less"); - -type StoryArgs = Record; - -type Story = { - title: string; - decorators: ReadonlyArray< - (StoryComponent: typeof React.Component) => React.ReactElement - >; -}; - -export default { - title: "PerseusEditor/Diffs/Structured Item Diff", - decorators: [ - (StoryComponent) => ( - - - - ), - ], -} as Story; - -const tags = { - a: "a tag", - b: "b tag", - c: "c tag", -} as const; - -export const ContentAdded = (args: StoryArgs): React.ReactElement => { - const props = { - before: { - _multi: { - directions: { - __type: "content", - content: "", - images: {}, - widgets: {}, - }, - passage: { - type: "content", - content: "", - images: {}, - widgets: {}, - }, - overview: { - type: "content", - content: "", - images: {}, - widgets: {}, - }, - hints: [], - questions: [], - }, - }, - after: { - _multi: { - directions: { - type: "content", - content: "directions", - images: {}, - widgets: {}, - }, - passage: { - type: "content", - content: "passage", - images: {}, - widgets: {}, - }, - overview: { - type: "content", - content: "overview", - images: {}, - widgets: {}, - }, - hints: [ - { - type: "hint", - content: "hint 1", - images: {}, - widgets: {}, - }, - ], - questions: [ - { - hints: [ - { - type: "hint", - content: "question hint 1", - images: {}, - widgets: {}, - }, - ], - keepInMind: { - type: "content", - content: "keep in mind", - images: {}, - widgets: {}, - }, - overview: { - type: "content", - content: "overview", - images: {}, - widgets: {}, - }, - question: { - type: "content", - content: "question", - images: {}, - widgets: {}, - }, - tags: ["a", "b"], - }, - ], - }, - }, - shape: { - type: "object", - shape: { - directions: { - type: "content", - }, - overview: { - type: "content", - }, - passage: { - type: "content", - }, - hints: { - type: "array", - elementShape: { - type: "hint", - }, - }, - questions: { - type: "array", - elementShape: { - type: "object", - shape: { - tags: {type: "tags"}, - question: { - type: "content", - }, - overview: { - type: "content", - }, - keepInMind: { - type: "content", - }, - hints: { - type: "array", - elementShape: { - type: "hint", - }, - }, - }, - }, - }, - }, - }, - tags: { - idToName: (id) => tags[id], - nameToId: (name) => name[0], - names: ["a tag", "b tag", "c tag"], - }, - } as const; - - return ; -}; - -// second instance -export const ContentAddedRemovedAndChanged = ( - args: StoryArgs, -): React.ReactElement => { - const props = { - before: { - _multi: { - directions: { - __type: "content", - content: "directions", - images: {}, - widgets: {}, - }, - passage: { - type: "content", - content: "passage", - images: {}, - widgets: {}, - }, - overview: { - type: "content", - content: "overview", - images: {}, - widgets: {}, - }, - hints: [ - { - type: "hint", - content: "hint 1", - images: {}, - widgets: {}, - }, - { - type: "hint", - content: "hint 2", - images: {}, - widgets: {}, - }, - ], - questions: [ - { - hints: [ - { - type: "hint", - content: "question hint 1", - images: {}, - widgets: {}, - }, - ], - keepInMind: { - type: "content", - content: "keep in mind", - images: {}, - widgets: {}, - }, - overview: { - type: "content", - content: "overview", - images: {}, - widgets: {}, - }, - question: { - type: "content", - content: "question", - images: {}, - widgets: {}, - }, - tags: ["a", "b"], - }, - ], - }, - }, - after: { - _multi: { - directions: { - type: "content", - content: "directions", - images: {}, - widgets: {}, - }, - passage: { - type: "content", - content: "passage", - images: {}, - widgets: {}, - }, - overview: { - type: "content", - content: "overview", - images: {}, - widgets: {}, - }, - hints: [ - { - type: "hint", - content: "hint 1", - images: {}, - widgets: {}, - }, - ], - questions: [ - { - hints: [], - keepInMind: { - type: "content", - content: "keep in mind", - images: {}, - widgets: {}, - }, - overview: { - type: "content", - content: "overview", - images: {}, - widgets: {}, - }, - question: { - type: "content", - content: "question edited", - images: {}, - widgets: {}, - }, - tags: ["a", "c"], - }, - { - hints: [ - { - type: "hint", - content: "question hint 1", - images: {}, - widgets: {}, - }, - ], - keepInMind: { - type: "content", - content: "keep in mind", - images: {}, - widgets: {}, - }, - overview: { - type: "content", - content: "overview", - images: {}, - widgets: {}, - }, - question: { - type: "content", - content: "question", - images: {}, - widgets: {}, - }, - tags: ["a", "b"], - }, - ], - }, - }, - shape: { - type: "object", - shape: { - directions: { - type: "content", - }, - overview: { - type: "content", - }, - passage: { - type: "content", - }, - hints: { - type: "array", - elementShape: { - type: "hint", - }, - }, - questions: { - type: "array", - elementShape: { - type: "object", - shape: { - tags: {type: "tags"}, - question: { - type: "content", - }, - overview: { - type: "content", - }, - keepInMind: { - type: "content", - }, - hints: { - type: "array", - elementShape: { - type: "hint", - }, - }, - }, - }, - }, - }, - }, - tags: { - idToName: (id) => tags[id], - nameToId: (name) => name[0], - names: ["a tag", "b tag", "c tag"], - }, - } as const; - return ; -}; - -// third instance -export const MiscContentChanges = (args: StoryArgs): React.ReactElement => { - const props = { - before: { - _multi: { - directions: { - __type: "content", - content: "directions", - images: {}, - widgets: {}, - }, - passage: { - type: "content", - content: "passage", - images: {}, - widgets: {}, - }, - overview: { - type: "content", - content: "overview", - images: {}, - widgets: {}, - }, - hints: [ - { - type: "hint", - content: "hint 1", - images: {}, - widgets: {}, - }, - { - type: "hint", - content: "hint 2", - images: {}, - widgets: {}, - }, - { - type: "hint", - content: "hint 3", - images: {}, - widgets: {}, - }, - ], - questions: [ - { - hints: [ - { - type: "hint", - content: "question hint 1", - images: {}, - widgets: {}, - }, - { - type: "hint", - content: "question hint 2", - images: {}, - widgets: {}, - }, - { - type: "hint", - content: "question hint 3", - images: {}, - widgets: {}, - }, - ], - keepInMind: { - type: "content", - content: "keep in mind", - images: {}, - widgets: {}, - }, - overview: { - type: "content", - content: "overview", - images: {}, - widgets: {}, - }, - question: { - type: "content", - content: "question edited", - images: {}, - widgets: {}, - }, - tags: ["a", "c"], - }, - { - hints: [ - { - type: "hint", - content: "question hint 1", - images: {}, - widgets: {}, - }, - ], - keepInMind: { - type: "content", - content: "keep in mind", - images: {}, - widgets: {}, - }, - overview: { - type: "content", - content: "overview", - images: {}, - widgets: {}, - }, - question: { - type: "content", - content: "question", - images: {}, - widgets: {}, - }, - tags: ["a", "b"], - }, - { - hints: [ - { - type: "hint", - content: "question hint 1", - images: {}, - widgets: {}, - }, - ], - keepInMind: { - type: "content", - content: "keep in mind", - images: {}, - widgets: {}, - }, - overview: { - type: "content", - content: "overview", - images: {}, - widgets: {}, - }, - question: { - type: "content", - content: "question", - images: {}, - widgets: {}, - }, - tags: ["a", "b"], - }, - ], - }, - }, - after: { - _multi: { - directions: { - __type: "content", - content: "directions", - images: {}, - widgets: {}, - }, - passage: { - type: "content", - content: "passage", - images: {}, - widgets: {}, - }, - overview: { - type: "content", - content: "overview", - images: {}, - widgets: {}, - }, - hints: [ - { - type: "hint", - content: "hint 1", - images: {}, - widgets: {}, - }, - { - type: "hint", - content: "hint 2", - images: {}, - widgets: {}, - }, - ], - questions: [ - { - hints: [ - { - type: "hint", - content: "question hint 1 edited", - images: {}, - widgets: {}, - }, - ], - keepInMind: { - type: "content", - content: "keep in mind", - images: {}, - widgets: {}, - }, - overview: { - type: "content", - content: "overview", - images: {}, - widgets: {}, - }, - question: { - type: "content", - content: "question edited", - images: {}, - widgets: {}, - }, - tags: ["a", "c"], - }, - { - hints: [ - { - type: "hint", - content: "question hint 1", - images: {}, - widgets: {}, - }, - ], - keepInMind: { - type: "content", - content: "keep in mind", - images: {}, - widgets: {}, - }, - overview: { - type: "content", - content: "overview", - images: {}, - widgets: {}, - }, - question: { - type: "content", - content: "question", - images: {}, - widgets: {}, - }, - tags: ["a", "b"], - }, - ], - }, - }, - shape: { - type: "object", - shape: { - directions: { - type: "content", - }, - overview: { - type: "content", - }, - passage: { - type: "content", - }, - hints: { - type: "array", - elementShape: { - type: "hint", - }, - }, - questions: { - type: "array", - elementShape: { - type: "object", - shape: { - tags: {type: "tags"}, - question: { - type: "content", - }, - overview: { - type: "content", - }, - keepInMind: { - type: "content", - }, - hints: { - type: "array", - elementShape: { - type: "hint", - }, - }, - }, - }, - }, - }, - }, - tags: { - idToName: (id) => tags[id], - nameToId: (name) => name[0], - names: ["a tag", "b tag", "c tag"], - }, - } as const; - - return ; -}; - -// fourth -export const ContentRemoved = (args: StoryArgs): React.ReactElement => { - const props = { - before: { - _multi: { - directions: { - __type: "content", - content: "directions", - images: {}, - widgets: {}, - }, - passage: { - type: "content", - content: "passage", - images: {}, - widgets: {}, - }, - overview: { - type: "content", - content: "overview", - images: {}, - widgets: {}, - }, - hints: [ - { - type: "hint", - content: "hint 1", - images: {}, - widgets: {}, - }, - { - type: "hint", - content: "hint 2", - images: {}, - widgets: {}, - }, - ], - questions: [ - { - hints: [ - { - type: "hint", - content: "question hint 1 edited", - images: {}, - widgets: {}, - }, - ], - keepInMind: { - type: "content", - content: "keep in mind", - images: {}, - widgets: {}, - }, - overview: { - type: "content", - content: "overview", - images: {}, - widgets: {}, - }, - question: { - type: "content", - content: "question edited", - images: {}, - widgets: {}, - }, - tags: ["a", "c"], - }, - { - hints: [ - { - type: "hint", - content: "question hint 1", - images: {}, - widgets: {}, - }, - ], - keepInMind: { - type: "content", - content: "keep in mind", - images: {}, - widgets: {}, - }, - overview: { - type: "content", - content: "overview", - images: {}, - widgets: {}, - }, - question: { - type: "content", - content: "question", - images: {}, - widgets: {}, - }, - tags: ["a", "b"], - }, - ], - }, - }, - after: { - _multi: { - directions: { - __type: "content", - content: "", - images: {}, - widgets: {}, - }, - passage: { - type: "content", - content: "", - images: {}, - widgets: {}, - }, - overview: { - type: "content", - content: "", - images: {}, - widgets: {}, - }, - hints: [], - questions: [], - }, - }, - shape: { - type: "object", - shape: { - directions: { - type: "content", - }, - overview: { - type: "content", - }, - passage: { - type: "content", - }, - hints: { - type: "array", - elementShape: { - type: "hint", - }, - }, - questions: { - type: "array", - elementShape: { - type: "object", - shape: { - tags: {type: "tags"}, - question: { - type: "content", - }, - overview: { - type: "content", - }, - keepInMind: { - type: "content", - }, - hints: { - type: "array", - elementShape: { - type: "hint", - }, - }, - }, - }, - }, - }, - }, - tags: { - idToName: (id) => tags[id], - nameToId: (name) => name[0], - names: ["a tag", "b tag", "c tag"], - }, - } as const; - - return ; -}; diff --git a/packages/perseus-editor/src/diffs/__tests__/structured-item-diff.test.ts b/packages/perseus-editor/src/diffs/__tests__/structured-item-diff.test.ts deleted file mode 100644 index 37d29ecac0..0000000000 --- a/packages/perseus-editor/src/diffs/__tests__/structured-item-diff.test.ts +++ /dev/null @@ -1,433 +0,0 @@ -import {buildEmptyItemTreeForShape, shapes} from "@khanacademy/perseus"; - -import StructuredItemDiff from "../structured-item-diff"; - -import type {Path} from "@khanacademy/perseus"; - -const gtpPassageShape = shapes.shape({ - directions: shapes.content, - passage: shapes.content, - overview: shapes.content, - hints: shapes.hints, - - questions: shapes.arrayOf( - shapes.shape({ - tags: shapes.tags, - question: shapes.content, - keepInMind: shapes.content, - overview: shapes.content, - hints: shapes.hints, - }), - ), -}); - -const gtpSingleQuestionShape = shapes.shape({ - blurb: shapes.content, - question: shapes.content, - hints: shapes.hints, -}); - -const emptyContent = buildEmptyItemTreeForShape(shapes.content); -const emptyHint = buildEmptyItemTreeForShape(shapes.hint); - -describe("StructuredItemDiff", function () { - /** - * Testing adding all empty content items, - * and then testing removing by reversing the order. - */ - it("test adding empty items, removing empty items", function () { - const beforeList = [ - [emptyContent, ["directions"]], - [emptyContent, ["passage"]], - [emptyContent, ["overview"]], - ]; - - const afterList = [ - [emptyContent, ["directions"]], - [emptyContent, ["passage"]], - [emptyContent, ["overview"]], - [emptyHint, ["hints", 0]], - [emptyHint, ["hints", 1]], - [emptyContent, ["questions", 0, "tags"]], - [emptyContent, ["questions", 0, "question"]], - [emptyContent, ["questions", 0, "keepInMind"]], - [emptyContent, ["questions", 0, "overview"]], - [emptyContent, ["questions", 1, "tags"]], - [emptyContent, ["questions", 1, "question"]], - [emptyContent, ["questions", 1, "keepInMind"]], - [emptyContent, ["questions", 1, "overview"]], - [emptyHint, ["questions", 1, "hints", 0]], - [emptyHint, ["questions", 1, "hints", 1]], - ]; - - const addingResult: Array = []; - StructuredItemDiff.generateCompletePathsList( - // @ts-expect-error - TS2345 - Argument of type 'any[][]' is not assignable to parameter of type 'ItemList[]'. - beforeList.slice(), - afterList.slice(), - addingResult, - gtpPassageShape, - [], - ); - - const removingResult = []; - StructuredItemDiff.generateCompletePathsList( - // @ts-expect-error - TS2345 - Argument of type 'any[][]' is not assignable to parameter of type 'ItemList[]'. - afterList, - beforeList, - removingResult, - gtpPassageShape, - [], - ); - - expect(addingResult).toEqual([ - ["directions"], - ["passage"], - ["overview"], - ["hints", 0], - ["hints", 1], - ["questions", 0, "tags"], - ["questions", 0, "question"], - ["questions", 0, "keepInMind"], - ["questions", 0, "overview"], - ["questions", 1, "tags"], - ["questions", 1, "question"], - ["questions", 1, "keepInMind"], - ["questions", 1, "overview"], - ["questions", 1, "hints", 0], - ["questions", 1, "hints", 1], - ]); - - expect(removingResult).toEqual([ - ["directions"], - ["passage"], - ["overview"], - ["hints", 0], - ["hints", 1], - ["questions", 0, "tags"], - ["questions", 0, "question"], - ["questions", 0, "keepInMind"], - ["questions", 0, "overview"], - ["questions", 1, "tags"], - ["questions", 1, "question"], - ["questions", 1, "keepInMind"], - ["questions", 1, "overview"], - ["questions", 1, "hints", 0], - ["questions", 1, "hints", 1], - ]); - }); - - /** - * Testing editing content of items, - * including top level items, as well as hints, - * and items within questions. - */ - it("test editing items", function () { - const passage = buildEmptyItemTreeForShape(shapes.content); - passage.content = "passage"; - const firstQuestion = buildEmptyItemTreeForShape(shapes.content); - firstQuestion.content = "question 1"; - const firstHint = buildEmptyItemTreeForShape(shapes.hint); - firstHint.content = "hint 1"; - const secondQuestionFirstHint = buildEmptyItemTreeForShape(shapes.hint); - secondQuestionFirstHint.content = "question 1 hint 1"; - const beforeList = [ - [emptyContent, ["directions"]], - [passage, ["passage"]], - [emptyContent, ["overview"]], - [firstHint, ["hints", 0]], - [emptyHint, ["hints", 1]], - [emptyContent, ["questions", 0, "tags"]], - [firstQuestion, ["questions", 0, "question"]], - [emptyContent, ["questions", 0, "keepInMind"]], - [emptyContent, ["questions", 0, "overview"]], - [emptyContent, ["questions", 1, "tags"]], - [emptyContent, ["questions", 1, "question"]], - [emptyContent, ["questions", 1, "keepInMind"]], - [emptyContent, ["questions", 1, "overview"]], - [secondQuestionFirstHint, ["questions", 1, "hints", 0]], - [emptyHint, ["questions", 1, "hints", 1]], - ]; - - const passageEdited = buildEmptyItemTreeForShape(shapes.content); - passageEdited.content = "passage edited"; - const firstQuestionEdited = buildEmptyItemTreeForShape(shapes.content); - firstQuestionEdited.content = "question edited 1"; - const firstHintEdited = buildEmptyItemTreeForShape(shapes.hint); - firstHintEdited.content = "edited hint 1"; - const secondQuestionFirstHintEdited = buildEmptyItemTreeForShape( - shapes.hint, - ); - secondQuestionFirstHintEdited.content = "question 1 hint 1 edited"; - const afterList = [ - [emptyContent, ["directions"]], - [passageEdited, ["passage"]], - [emptyContent, ["overview"]], - [firstHintEdited, ["hints", 0]], - [emptyHint, ["hints", 1]], - [emptyContent, ["questions", 0, "tags"]], - [firstQuestionEdited, ["questions", 0, "question"]], - [emptyContent, ["questions", 0, "keepInMind"]], - [emptyContent, ["questions", 0, "overview"]], - [emptyContent, ["questions", 1, "tags"]], - [emptyContent, ["questions", 1, "question"]], - [emptyContent, ["questions", 1, "keepInMind"]], - [emptyContent, ["questions", 1, "overview"]], - [secondQuestionFirstHintEdited, ["questions", 1, "hints", 0]], - [emptyHint, ["questions", 1, "hints", 1]], - ]; - - const firstResult: Array = []; - StructuredItemDiff.generateCompletePathsList( - // @ts-expect-error - TS2345 - Argument of type 'any[][]' is not assignable to parameter of type 'ItemList[]'. - beforeList.slice(), - afterList.slice(), - firstResult, - gtpPassageShape, - [], - ); - - const secondResult = []; - StructuredItemDiff.generateCompletePathsList( - // @ts-expect-error - TS2345 - Argument of type 'any[][]' is not assignable to parameter of type 'ItemList[]'. - afterList, - beforeList, - secondResult, - gtpPassageShape, - [], - ); - - expect(firstResult).toEqual([ - ["directions"], - ["passage"], - ["overview"], - ["hints", 0], - ["hints", 1], - ["questions", 0, "tags"], - ["questions", 0, "question"], - ["questions", 0, "keepInMind"], - ["questions", 0, "overview"], - ["questions", 1, "tags"], - ["questions", 1, "question"], - ["questions", 1, "keepInMind"], - ["questions", 1, "overview"], - ["questions", 1, "hints", 0], - ["questions", 1, "hints", 1], - ]); - - expect(secondResult).toEqual([ - ["directions"], - ["passage"], - ["overview"], - ["hints", 0], - ["hints", 1], - ["questions", 0, "tags"], - ["questions", 0, "question"], - ["questions", 0, "keepInMind"], - ["questions", 0, "overview"], - ["questions", 1, "tags"], - ["questions", 1, "question"], - ["questions", 1, "keepInMind"], - ["questions", 1, "overview"], - ["questions", 1, "hints", 0], - ["questions", 1, "hints", 1], - ]); - }); - - /** - * Testing these cases: - * (1) adding/removing overall hints - * (2) adding/removing question hints of a middle question - * (question is surrounded by other questions) - * (3) adding/removing question hints of the last question - * (4) adding/removing questions entirely - * - * This test is done with empty items, since we've tested for edits, - * and generateCompletePathLists() doesn't look at the contents. - */ - it("testing adding and removing in special cases", function () { - const beforeList = [ - [emptyContent, ["directions"]], - [emptyContent, ["passage"]], - [emptyContent, ["overview"]], - [emptyContent, ["questions", 0, "tags"]], - [emptyContent, ["questions", 0, "question"]], - [emptyContent, ["questions", 0, "keepInMind"]], - [emptyContent, ["questions", 0, "overview"]], - [emptyContent, ["questions", 1, "tags"]], - [emptyContent, ["questions", 1, "question"]], - [emptyContent, ["questions", 1, "keepInMind"]], - [emptyContent, ["questions", 1, "overview"]], - [emptyHint, ["questions", 1, "hints", 0]], - [emptyHint, ["questions", 1, "hints", 1]], - [emptyContent, ["questions", 2, "tags"]], - [emptyContent, ["questions", 2, "question"]], - [emptyContent, ["questions", 2, "keepInMind"]], - [emptyContent, ["questions", 2, "overview"]], - [emptyHint, ["questions", 2, "hints", 0]], - [emptyHint, ["questions", 2, "hints", 1]], - [emptyContent, ["questions", 3, "tags"]], - [emptyContent, ["questions", 3, "question"]], - [emptyContent, ["questions", 3, "keepInMind"]], - [emptyContent, ["questions", 3, "overview"]], - ]; - - const afterList = [ - [emptyContent, ["directions"]], - [emptyContent, ["passage"]], - [emptyContent, ["overview"]], - [emptyHint, ["hints", 0]], // add - [emptyHint, ["hints", 1]], // add - [emptyContent, ["questions", 0, "tags"]], - [emptyContent, ["questions", 0, "question"]], - [emptyContent, ["questions", 0, "keepInMind"]], - [emptyContent, ["questions", 0, "overview"]], - [emptyHint, ["questions", 0, "hints", 0]], // add - [emptyHint, ["questions", 0, "hints", 1]], // add - [emptyContent, ["questions", 1, "tags"]], - [emptyContent, ["questions", 1, "question"]], - [emptyContent, ["questions", 1, "keepInMind"]], - [emptyContent, ["questions", 1, "overview"]], - [emptyHint, ["questions", 1, "hints", 0]], - // remove second hint of question 1 - [emptyContent, ["questions", 2, "tags"]], - [emptyContent, ["questions", 2, "question"]], - [emptyContent, ["questions", 2, "keepInMind"]], - [emptyContent, ["questions", 2, "overview"]], - [emptyHint, ["questions", 2, "hints", 0]], - [emptyHint, ["questions", 2, "hints", 1]], - // removed question 2, added two hints to question 3 - ]; - - const firstResult: Array = []; - StructuredItemDiff.generateCompletePathsList( - // @ts-expect-error - TS2345 - Argument of type 'any[][]' is not assignable to parameter of type 'ItemList[]'. - beforeList.slice(), - afterList.slice(), - firstResult, - gtpPassageShape, - [], - ); - - const secondResult: Array = []; - StructuredItemDiff.generateCompletePathsList( - // @ts-expect-error - TS2345 - Argument of type 'any[][]' is not assignable to parameter of type 'ItemList[]'. - afterList, - beforeList, - secondResult, - gtpPassageShape, - [], - ); - - expect(firstResult).toEqual([ - ["directions"], - ["passage"], - ["overview"], - ["hints", 0], - ["hints", 1], - ["questions", 0, "tags"], - ["questions", 0, "question"], - ["questions", 0, "keepInMind"], - ["questions", 0, "overview"], - ["questions", 0, "hints", 0], - ["questions", 0, "hints", 1], - ["questions", 1, "tags"], - ["questions", 1, "question"], - ["questions", 1, "keepInMind"], - ["questions", 1, "overview"], - ["questions", 1, "hints", 0], - ["questions", 1, "hints", 1], - ["questions", 2, "tags"], - ["questions", 2, "question"], - ["questions", 2, "keepInMind"], - ["questions", 2, "overview"], - ["questions", 2, "hints", 0], - ["questions", 2, "hints", 1], - ["questions", 3, "tags"], - ["questions", 3, "question"], - ["questions", 3, "keepInMind"], - ["questions", 3, "overview"], - ]); - - expect(secondResult).toEqual([ - ["directions"], - ["passage"], - ["overview"], - ["hints", 0], - ["hints", 1], - ["questions", 0, "tags"], - ["questions", 0, "question"], - ["questions", 0, "keepInMind"], - ["questions", 0, "overview"], - ["questions", 0, "hints", 0], - ["questions", 0, "hints", 1], - ["questions", 1, "tags"], - ["questions", 1, "question"], - ["questions", 1, "keepInMind"], - ["questions", 1, "overview"], - ["questions", 1, "hints", 0], - ["questions", 1, "hints", 1], - ["questions", 2, "tags"], - ["questions", 2, "question"], - ["questions", 2, "keepInMind"], - ["questions", 2, "overview"], - ["questions", 2, "hints", 0], - ["questions", 2, "hints", 1], - ["questions", 3, "tags"], - ["questions", 3, "question"], - ["questions", 3, "keepInMind"], - ["questions", 3, "overview"], - ]); - }); - - /** - * Testing single question layout shape. - */ - it("testing single question layout", function () { - const beforeList = [ - [emptyContent, ["blurb"]], - [emptyContent, ["question"]], - ]; - - const afterList = [ - [emptyContent, ["blurb"]], - [emptyContent, ["question"]], - [emptyHint, ["hints", 0]], - [emptyHint, ["hints", 1]], - ]; - - const firstResult = []; - StructuredItemDiff.generateCompletePathsList( - // @ts-expect-error - TS2345 - Argument of type 'any[][]' is not assignable to parameter of type 'ItemList[]'. - beforeList.slice(), - afterList.slice(), - firstResult, - gtpSingleQuestionShape, - [], - ); - - const secondResult = []; - StructuredItemDiff.generateCompletePathsList( - // @ts-expect-error - TS2345 - Argument of type 'any[][]' is not assignable to parameter of type 'ItemList[]'. - afterList, - beforeList, - secondResult, - gtpSingleQuestionShape, - [], - ); - - expect(firstResult).toEqual([ - ["blurb"], - ["question"], - ["hints", 0], - ["hints", 1], - ]); - - expect(secondResult).toEqual([ - ["blurb"], - ["question"], - ["hints", 0], - ["hints", 1], - ]); - }); -}); diff --git a/packages/perseus-editor/src/diffs/structured-item-diff.tsx b/packages/perseus-editor/src/diffs/structured-item-diff.tsx deleted file mode 100644 index 43cccc1298..0000000000 --- a/packages/perseus-editor/src/diffs/structured-item-diff.tsx +++ /dev/null @@ -1,275 +0,0 @@ -/* eslint-disable @khanacademy/ts-no-error-suppressions */ -/** - * A side by side diff view for Perseus exercise items - * that do not have the standard question layout. - */ -import { - buildEmptyItemTreeForShape, - buildMapper, - itemToTree, - shapes, -} from "@khanacademy/perseus"; -import * as React from "react"; - -import RendererDiff from "./renderer-diff"; -import TagsDiff from "./tags-diff"; - -import type {Item, Path, Shape} from "@khanacademy/perseus"; - -type ItemList = [unknown, Path]; - -/** - * Outputs true if path begins with beginPath, false otherwise. - */ -function beginsWith(path: Path, beginPath: Path): boolean { - let matches = true; - for (let i = 0; i < beginPath.length; i++) { - if (i >= path.length) { - return false; - } - if (beginPath[i] !== path[i]) { - matches = false; - } - } - return matches; -} - -/** - * Outputs true if beforePath and afterPath are the same. - */ -function checkPath(beforePath: Path, afterPath: Path): boolean { - if (beforePath.length !== afterPath.length) { - return false; - } - for (let i = 0, l = beforePath.length; i < l; i++) { - if (beforePath[i] !== afterPath[i]) { - return false; - } - } - return true; -} - -/** - * Given a path, returns a title. Puts colons after numbers in the path. - */ -function getTitle(path: Path): string { - const title: Array = []; - for (let i = 0; i < path.length; i++) { - if (typeof path[i] === "number") { - // @ts-expect-error - TS2365 - Operator '+' cannot be applied to types 'string | number' and 'number'. - title.push((path[i] + 1).toString() + ":"); - } else { - // @ts-expect-error - TS2345 - Argument of type 'string | number' is not assignable to parameter of type 'string'. - title.push(path[i]); - } - } - return title.join(" "); -} - -type Tag = { - idToName: (arg1: string) => string; - nameToId: (arg1: string) => string; - names: ReadonlyArray; -}; - -type Props = { - after: Item; - before: Item; - shape: Shape; - tags: Tag; -}; - -class StructuredItemDiff extends React.Component { - /** - * Traverses the given shape and adds paths that are present in - * beforeList and afterList to result. Note that this method assumes - * the order of elements in beforeList and afterList, which are - * from buildMapper(), is the same order they appear in in the shape. - */ - static generateCompletePathsList( - beforeList: Array, - afterList: Array, - result: Array, - shape: Shape, - path: ReadonlyArray, - ): void { - if ( - shape.type === "content" || - shape.type === "hint" || - shape.type === "tags" - ) { - const beforePath = - beforeList.length > 0 && checkPath(path, beforeList[0][1]); - const afterPath = - afterList.length > 0 && checkPath(path, afterList[0][1]); - if (beforePath && afterPath) { - result.push(path); - beforeList.splice(0, 1); - afterList.splice(0, 1); - } else if (beforePath) { - result.push(path); - beforeList.splice(0, 1); - } else if (afterPath) { - result.push(path); - afterList.splice(0, 1); - } - } else if (shape.type === "array") { - let index = 0; - let newPath = path.concat(index); - - // For array types, the paths will be in the form [, n], - // where n is an integer > 0 and increments. - // As long as either beforeList or afterList has a next element that - // matches [, n], we recurse into that item with the new path. - while ( - (beforeList.length > 0 && - beginsWith(beforeList[0][1], newPath)) || - (afterList.length > 0 && beginsWith(afterList[0][1], newPath)) - ) { - StructuredItemDiff.generateCompletePathsList( - beforeList, - afterList, - result, - shape.elementShape, - newPath, - ); - index++; - newPath = path.concat(index); - } - } else if (shape.type === "object") { - const keys = Object.keys(shape.shape); - for (let i = 0; i < keys.length; i++) { - const newPath = path.concat([keys[i]]); - StructuredItemDiff.generateCompletePathsList( - beforeList, - afterList, - result, - shape.shape[keys[i]], - newPath, - ); - } - } - } - - render(): React.ReactNode { - const {before, after, shape, tags} = this.props; - - const beforeList: Array = []; - const afterList = []; - - buildMapper() - .setContentMapper((c, _, p) => beforeList.push([c, p])) - .setHintMapper((c, _, p) => beforeList.push([c, p])) - .setTagsMapper((c, _, p) => beforeList.push([c, p])) - .mapTree(itemToTree(before), shape); - - buildMapper() - // @ts-expect-error - TS2322 - Type 'unknown' is not assignable to type 'never'. | TS2322 - Type 'Path' is not assignable to type 'never'. - .setContentMapper((c, _, p) => afterList.push([c, p])) - // @ts-expect-error - TS2322 - Type 'unknown' is not assignable to type 'never'. | TS2322 - Type 'Path' is not assignable to type 'never'. - .setHintMapper((c, _, p) => afterList.push([c, p])) - // @ts-expect-error - TS2322 - Type 'unknown' is not assignable to type 'never'. | TS2322 - Type 'Path' is not assignable to type 'never'. - .setTagsMapper((c, _, p) => afterList.push([c, p])) - .mapTree(itemToTree(after), shape); - - // These are used in generateCompletePathsList() - // and are modified in that method. - const beforeListModified = beforeList.slice(); - const afterListModified = afterList.slice(); - - const allDiffPaths: Array = []; - StructuredItemDiff.generateCompletePathsList( - beforeListModified, - afterListModified, - allDiffPaths, - shape, - [], - ); - - const diffCount = allDiffPaths.length; - - const diffs: React.ReactNode = allDiffPaths.map((path, n) => { - const isTag = path[path.length - 1] === "tags"; - const currentTitle = getTitle(path); - - let before = beforeList.find((e) => { - return checkPath(e[1], path); - }); - let after = afterList.find((e) => { - return checkPath(e[1], path); - }); - - if (isTag) { - if (!before) { - before = [[], path]; - } - if (!after) { - // @ts-expect-error - TS2322 - Type 'Path[]' is not assignable to type 'undefined'. - after = [[], path]; - } - - const beforeTags: Array = []; - if (Array.isArray(before[0])) { - before[0].forEach((tagId) => { - if (typeof tagId === "string") { - beforeTags.push(tags.idToName(tagId)); - } - }); - } - const afterTags: Array = []; - // @ts-expect-error - TS2532 - Object is possibly 'undefined'. - if (Array.isArray(after[0])) { - // @ts-expect-error - TS2532 - Object is possibly 'undefined'. - after[0].forEach((tagId) => { - if (typeof tagId === "string") { - afterTags.push(tags.idToName(tagId)); - } - }); - } - - const intersection = beforeTags.filter((tag) => - afterTags.includes(tag), - ); - const beforeOnly = beforeTags.filter( - (tag) => !afterTags.includes(tag), - ); - const afterOnly = afterTags.filter( - (tag) => !beforeTags.includes(tag), - ); - - return ( - - ); - } - if (!before) { - before = [buildEmptyItemTreeForShape(shapes.content), path]; - } - if (!after) { - // @ts-expect-error - TS2322 - Type 'any[]' is not assignable to type 'undefined'. - after = [buildEmptyItemTreeForShape(shapes.content), path]; - } - return ( - - ); - }); - - return
{diffs}
; - } -} - -export default StructuredItemDiff; diff --git a/packages/perseus-editor/src/i18n.ts b/packages/perseus-editor/src/i18n.ts deleted file mode 100644 index 281b82f962..0000000000 --- a/packages/perseus-editor/src/i18n.ts +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Functions for extracting data from items for use in i18n. - */ -import {traverse, MultiItems, PerseusMarkdown} from "@khanacademy/perseus"; -import _ from "underscore"; - -const {findContentNodesInItem, findHintNodesInItem, inferItemShape} = - MultiItems; - -// Takes a renderer content and parses the markdown for images -function findImagesInContent(content: any, images: Array) { - // @ts-expect-error - TS2554 - Expected 2 arguments, but got 1. - const parsed = PerseusMarkdown.parse(content); - - PerseusMarkdown.traverseContent(parsed, function (node) { - if (node.type === "image") { - images.push(node.target); - } - }); -} - -// Background images in some widgets are annoying to deal with because -// sometimes the objects aren't full when there isn't an image. So, we do some -// extra checking to make sure we don't cause an error or push an empty image. -function handleBackgroundImage(graph, images: Array) { - if (graph && graph.backgroundImage && graph.backgroundImage.url) { - images.push(graph.backgroundImage.url); - } -} - -// The callback called for each widget. We check each of the areas of each -// widget where they contain a renderer for images by calling -// findImagesInContent. We don't have to recurse through child widgets, because -// traverseRendererDeep does that for us. -function widgetCallback(widgetInfo: any, images: Array) { - if (!widgetInfo.options) { - return; - } - - // TODO(emily/aria): Move this into the widget files, so we don't have the - // logic out here. - if (widgetInfo.type === "categorizer") { - _.each(widgetInfo.options.items, function (item) { - findImagesInContent(item, images); - }); - _.each(widgetInfo.options.categories, function (category) { - findImagesInContent(category, images); - }); - } else if (widgetInfo.type === "image") { - findImagesInContent(widgetInfo.options.title, images); - findImagesInContent(widgetInfo.options.caption, images); - } else if (widgetInfo.type === "matcher") { - _.each(widgetInfo.options.left, function (option) { - findImagesInContent(option, images); - }); - _.each(widgetInfo.options.right, function (option) { - findImagesInContent(option, images); - }); - _.each(widgetInfo.options.labels, function (label) { - findImagesInContent(label, images); - }); - } else if (widgetInfo.type === "matrix") { - findImagesInContent(widgetInfo.options.prefix, images); - findImagesInContent(widgetInfo.options.suffix, images); - } else if (widgetInfo.type === "orderer") { - _.each(widgetInfo.options.options, function (option) { - findImagesInContent(option.content, images); - }); - } else if (widgetInfo.type === "passage") { - findImagesInContent(widgetInfo.options.passageTitle, images); - } else if (widgetInfo.type === "radio") { - _.each(widgetInfo.options.choices, function (choice) { - findImagesInContent(choice.content, images); - }); - } else if (widgetInfo.type === "sorter") { - _.each(widgetInfo.options.correct, function (option) { - findImagesInContent(option, images); - }); - } else if (widgetInfo.type === "table") { - _.each(widgetInfo.options.headers, function (header) { - findImagesInContent(header, images); - }); - } - - if (widgetInfo.type === "grapher") { - handleBackgroundImage(widgetInfo.options.graph, images); - } else if (widgetInfo.type === "image") { - handleBackgroundImage(widgetInfo.options, images); - } else if (widgetInfo.type === "interactive-graph") { - handleBackgroundImage(widgetInfo.options, images); - } else if (widgetInfo.type === "measurer" && widgetInfo.options.image) { - images.push(widgetInfo.options.image.url); - } else if (widgetInfo.type === "plotter") { - images.push(widgetInfo.options.picUrl); - } -} - -function findImagesInRenderers(renderers) { - const images = []; - - _.each(renderers, (renderer) => { - traverse( - renderer, - (content) => { - findImagesInContent(content, images); - }, - (widget) => widgetCallback(widget, images), - ); - }); - - return images; -} - -// Calls findImagesInContent on all of the different content areas for -// assessment items -function findImagesInItemData(itemData: any): any { - let renderers = []; - if (itemData._multi) { - const shape = inferItemShape(itemData); - // @ts-expect-error - TS2345 - Argument of type 'ContentNode' is not assignable to parameter of type 'never'. - findContentNodesInItem(itemData, shape, (node) => renderers.push(node)); - // @ts-expect-error - TS2345 - Argument of type 'HintNode' is not assignable to parameter of type 'never'. - findHintNodesInItem(itemData, shape, (node) => renderers.push(node)); - } else { - // @ts-expect-error - TS2322 - Type 'any' is not assignable to type 'never'. | TS2322 - Type 'any' is not assignable to type 'never'. - renderers = [itemData.question, ...itemData.hints]; - } - return findImagesInRenderers(renderers); -} - -// Calls findImagesInContent on all of the different content areas for -// articles -function findImagesInArticles(perseusContent: any): any { - return findImagesInRenderers(perseusContent); -} - -export default { - findImagesInArticles: findImagesInArticles, - findImagesInItemData: findImagesInItemData, -}; diff --git a/packages/perseus-editor/src/index.ts b/packages/perseus-editor/src/index.ts index 73e1b5921f..964f9b7140 100644 --- a/packages/perseus-editor/src/index.ts +++ b/packages/perseus-editor/src/index.ts @@ -5,12 +5,9 @@ export {default as DeviceFramer} from "./components/device-framer"; export {default as ViewportResizer} from "./components/viewport-resizer"; export {default as ArticleDiff} from "./diffs/article-diff"; export {default as ItemDiff} from "./diffs/item-diff"; -export {default as StructuredItemDiff} from "./diffs/structured-item-diff"; export {default as EditorPage} from "./editor-page"; export {default as Editor} from "./editor"; -export {default as i18n} from "./i18n"; export {default as IframeContentRenderer} from "./iframe-content-renderer"; -export {default as MultiRendererEditor} from "./multirenderer-editor"; import "./styles/perseus-editor.less"; diff --git a/packages/perseus-editor/src/multirenderer-editor.tsx b/packages/perseus-editor/src/multirenderer-editor.tsx deleted file mode 100644 index 2d0445c7fd..0000000000 --- a/packages/perseus-editor/src/multirenderer-editor.tsx +++ /dev/null @@ -1,1060 +0,0 @@ -/** - * Editor for a multi-item question. - * - * TODO(mdr): The UI for managing arrays isn't visually consistent with - * HintsEditor. Should we bring them in line with each other? - */ -import { - ApiOptions, - buildEmptyItemTreeForShape, - components, - iconChevronDown, - iconTrash, - itemToTree, - MultiItems, -} from "@khanacademy/perseus"; -import {StyleSheet, css} from "aphrodite"; -// eslint-disable-next-line import/no-extraneous-dependencies -import lens from "hubble"; -import * as React from "react"; -import ReactDOM from "react-dom"; - -import JsonEditor from "./components/json-editor"; -import SimpleButton from "./components/simple-button"; -import Editor from "./editor"; -import {HintEditor} from "./hint-editor"; - -import type { - APIOptions, - ChangeHandler, - EditorMode, - - // Multi-item item types - Item, - ItemTree, - ItemObjectNode, - ItemArrayNode, - ContentNode, - HintNode, - TagsNode, - - // Multi-item shape types - Shape, - ArrayShape, - ObjectShape, - ContentShape, - HintShape, - TagsShape, -} from "@khanacademy/perseus"; - -const {InlineIcon} = components; -const {MultiRenderer} = MultiItems; - -// TODO(CP-4849): figure out when $ReadOnlyArray vs $ReadOnlyArray should be used -type Path = ReadonlyArray; - -type ModeDropdownProps = { - currentMode: EditorMode; - // A function that takes in a string signifying the mode (ex: "edit") - onChange: (mode: EditorMode) => unknown; -}; - -/** - * Component that displays the mode dropdown. - * - * The mode dropdown is the selector at the top of the editor that lets you - * switch between edit, preview, and dev-only JSON mode. - */ -class ModeDropdown extends React.Component { - _handleSelectMode = (event: React.ChangeEvent) => { - if (this.props.onChange) { - // event.target.value corresponds to the options' values below which - // are limited to EditorMode, but TypeScript doesn't know that so we have - // to cast through any here. - const value = event.target.value as EditorMode; - this.props.onChange(value); - } - }; - - render(): React.ReactNode { - return ( - - ); - } -} - -/** - * Convert a camel-cased string to a human-formatted string. - * "superCoolThings" -> "super cool things" - */ -function camelCaseToHuman(str: string) { - // Decapitalize the capital letters, and add a space before each. - return str.replace(/[A-Z]/g, (s) => " " + s.toLowerCase()); -} - -/** - * Capitalize the first letter of the given string. - * "super cool things" -> "Super cool things" - */ -function capitalize(str: string) { - return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); -} - -/** - * Convert the given pluralized word to a singularized word. - * "super cool things" -> "super cool thing" - */ -function pluralToSingular(str: string) { - if (str.charAt(str.length - 1) === "s") { - // Incredibly weak implementation :P - return str.slice(0, -1); - } - // Uh oh, dunno how to singularize anything but the simplest case! - // Let's just return the plural form, and hope the user forgives the - // grammatical inconsistency. - return str; -} - -/** - * When iterating through the editors, we don't keep track of the extra - * `_multi` part at the beginning. This is a helper function which takes a path - * and prepends that key. - */ -function multiPath(path: Path | Array) { - return ["_multi", ...path]; -} - -// Return an h1 if depth=0, h2 if depth=1, etc. -// NOTE: This component accepts pass-through props. -type HeaderProps = { - depth: number; -}; - -const Header = ({depth, ...props}: HeaderProps): React.ReactElement => { - const headerLevel = Math.min(depth, 5) + 1; - const HeaderTag = `h${headerLevel}`; - return ; -}; - -// Actions is inexact so that we can pass an instance MultiRendererEditor. -interface Actions { - addArrayElement: (path: Path, shape: Shape) => void; - mergeValueAtPath: (path: Path, newValue?: any) => void; - setValueAtPath: (path: Path, newValue?: any) => void; - moveArrayElementDown: (path: Path) => void; - moveArrayElementUp: (path: Path) => void; - removeArrayElement: (path: Path) => void; -} - -// This type is used to define prop types for various nodes. The S and D type -// params should match when used, e.g. the prop types for `HintNodeContent` is -// `NodePropTypes`. -type NodePropTypes = { - shape: S; - data: D; - path: Path; - actions: Actions; - apiOptions: APIOptions; - // For the left-hand column, we use edit mode and leave renderers empty. - // For the right-hand column, we use preview mode and provide renderers - // via a MultiRenderer. - // TODO(CP-4850): figure out how to type this, it appears to be a tree where the - // leaf nodes could be typed using RendererInterface. - renderers?: any; -}; - -/** - * Render a node in the editor tree, given the shape of the target - * node, the data stored in the target node, the path to the target - * node, and any UI controls that affect how this node relates to its - * parent (e.g. remove from parent array). - * - * This returns a container element with a pretty title and additional - * UI controls for this node. Its contents are produced by - * `NodeContent`. The two functions are mutually recursive. - * - * Leaf nodes, like items and hints, render an editor pod around their - * content. Container nodes, like arrays and objects, render a header above - * their content. - */ -const NodeContainer = ( - props: NodePropTypes & { - controls?: ReadonlyArray; - name?: string; - }, -) => { - const { - shape, - data, - path, - actions, - name: givenName, - controls, - ...otherProps - } = props; - - const name = givenName || camelCaseToHuman(path[path.length - 1] || ""); - - const children = ( - - ); - - const key = path.join("."); - - if (shape.type === "array") { - return ( - - {children} - - ); - } - if (shape.type === "object") { - return ( - - {children} - - ); - } - return ( - - {children} - - ); -}; - -type LeafContainerProps = { - name: string; - controls?: React.ReactNode; - children?: React.ReactNode; - path: Path; - shape: Shape; -}; -const LeafContainer = ({ - name, - controls, - children, - path, - shape, -}: LeafContainerProps): React.ReactElement => { - const hasPreviewHeading = shape.type === "content" || shape.type === "hint"; - const previewHeading = hasPreviewHeading && ( -
- {/* @ts-expect-error - TS2322 - Type '{ children: string; depth: number; className: string; }' is not assignable to type 'IntrinsicAttributes & HeaderProps & { children?: ReactNode; }'. */} -
- {capitalize(name)} -
-
- ); - return ( -
- -
-
-
- {capitalize(name)} -
- {controls} -
-
-
{previewHeading}
-
- {children} -
- ); -}; - -interface ArrayContainerActions { - addArrayElement: (path: Path, shape: Shape) => void; -} - -type ArrayContainerProps = { - name: string; - controls?: React.ReactNode; - children?: React.ReactNode; - path: Path; - shape: ArrayShape; - actions: ArrayContainerActions; -}; -const ArrayContainer = (props: ArrayContainerProps): React.ReactElement => { - const {name, controls, children, path, shape, actions} = props; - return ( -
- {controls && ( -
- {controls} -
- )} -
{children}
-
- {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} - - actions.addArrayElement(path, shape.elementShape) - } - > - Add a {pluralToSingular(name)} - -
-
- ); -}; - -type ObjectContainerProps = { - name: string; - controls?: React.ReactNode; - children?: React.ReactNode; - path: Path; -}; -const ObjectContainer = ({ - name, - controls, - children, - path, -}: ObjectContainerProps): React.ReactElement => { - const headingEditor = ( -
- {/* @ts-expect-error - TS2322 - Type '{ children: string; depth: number; className: string; }' is not assignable to type 'IntrinsicAttributes & HeaderProps & { children?: ReactNode; }'. */} -
- {capitalize(name)} -
- {controls} -
- ); - const headingPreview = (name || controls) && ( -
- {/* @ts-expect-error - TS2322 - Type '{ children: string; depth: number; className: string; }' is not assignable to type 'IntrinsicAttributes & HeaderProps & { children?: ReactNode; }'. */} -
- {capitalize(name)} -
-
- ); - const hasBothHeadings = headingEditor && headingPreview; - return ( -
- {hasBothHeadings && ( - -
- {headingEditor} -
-
- {headingPreview} -
-
- )} -
0 && styles.contentIndent)}> - {children} -
-
- ); -}; - -/** - * Render the content of node in the editor tree, given the shape of - * the target node, the data stored in the target node, and the path to - * the target node. - * - * If the target node is a leaf, this returns an editor. Otherwise, it - * iterates over the child nodes, and outputs `NodeContainer` for - * each of them. The two functions are mutually recursive. - */ -const NodeContent = (props: NodePropTypes) => { - const {shape, data, ...restProps} = props; - - // All uses of `data` have been cast through any which isn't safe. This was - // done to avoid introducing new logic which may have resulted in a change - // of behavior. Also, there doesn't appear to be a way to tell TypeScript the - // difference between and `TagsNode` and `ItemArrayNode`, see - // perseus-all-package/multi-items/item-types.js. - if (shape.type === "content") { - return ( - - ); - } - if (shape.type === "hint") { - return ( - - ); - } - if (shape.type === "tags") { - return ( - - ); - } - if (shape.type === "array") { - return ( - - ); - } - if (shape.type === "object") { - return ( - - ); - } - return null; -}; - -type WithStickinessProps = { - sticky: boolean; -}; - -type WithStickiness = T & WithStickinessProps; - -/** - * HOC that adds a "sticky" prop to the wrapped component that is true - * when the rendered component is taller than the window. Since sticky content - * can be somewhat distracting, we'd like to avoid it when not useful. This - * HOC is useful for only making content sticky when useful. - * - * It does so by polling the height and comparing it to the window height. - */ -const withStickiness = < - Config extends Record, - Component extends React.ComponentType>, ->( - WrappedComponent: Component, -): React.ComponentType => { - type State = { - sticky: boolean; - }; - return class StickyComponent extends React.Component { - // @ts-expect-error - TS2564 - Property 'stickynessTimer' has no initializer and is not definitely assigned in the constructor. - stickynessTimer: number; - - state = { - sticky: false, - }; - - componentDidMount() { - // TODO(jeff, CP-3128): Use Wonder Blocks Timing API. - // eslint-disable-next-line no-restricted-syntax - // @ts-expect-error - TS2322 - Type 'Timer' is not assignable to type 'number'. - this.stickynessTimer = setInterval(this.updateStickiness, 1000); - this.updateStickiness(); - } - - componentWillUnmount() { - // TODO(jeff, CP-3128): Use Wonder Blocks Timing API. - // eslint-disable-next-line no-restricted-syntax - clearInterval(this.stickynessTimer); - } - - updateStickiness = () => { - const domNode = ReactDOM.findDOMNode(this); - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'offsetHeight' does not exist on type 'Element | Text'. - const height = domNode.offsetHeight; - const windowHeight = window.innerHeight; - const sticky = height > windowHeight; - if (sticky !== this.state.sticky) { - this.setState({ - sticky, - }); - } - }; - - render(): React.ReactNode { - return ( - // @ts-expect-error - TS2322 - Type 'Readonly & { sticky: boolean; children?: ReactNode; }' is not assignable to type 'IntrinsicAttributes & LibraryManagedAttributes>>'. - - ); - } - }; -}; - -const ItemNodeContent = withStickiness( - (props: NodePropTypes & WithStickinessProps) => { - const {data, path, actions, apiOptions, renderers, sticky} = props; - - const preview = ( -
{lens(renderers).get(path)}
- ); - - return ( - -
-
-
- {/* TODO(CP-4852): only pass the props to Editor that it uses. */} - { - // @ts-expect-error - TS2769 - No overload matches this call. - - actions.mergeValueAtPath(path, newVal) - } - apiOptions={apiOptions} - /> - } -
-
-
-
- {preview} -
-
-
-
- ); - }, -); - -const HintNodeContent = withStickiness( - (props: NodePropTypes & WithStickinessProps) => { - const {data, path, actions, apiOptions, renderers, sticky} = props; - - const preview = ( -
{lens(renderers).get(path)}
- ); - - return ( -
-
-
- - actions.mergeValueAtPath(path, newVal) - } - apiOptions={apiOptions} - showTitle={false} - showRemoveButton={false} - showMoveButtons={false} - // no-op handler since showRemoveButton={false} - onRemove={() => {}} - // no-op handler since showMoveButtons={false} - onMove={(direction: number) => {}} - isFirst={true} - isLast={true} - /> -
-
-
-
- {preview} -
-
-
- ); - }, -); - -const TagsNodeContent = (props: NodePropTypes) => { - const {data, path, actions, apiOptions} = props; - const {GroupMetadataEditor} = apiOptions; - - if (GroupMetadataEditor == null) { - return null; - } - return ( -
-
- actions.setValueAtPath(path, newVal)} - showTitle={false} - /> -
-
- ); -}; - -const ArrayNodeContent = (props: NodePropTypes) => { - const {shape, data, path, actions, ...otherProps} = props; - - const collectionName = camelCaseToHuman(path[path.length - 1]); - const elementName = pluralToSingular(collectionName); - - const elementType = shape.elementShape.type; - const elementIsLeaf = elementType === "content" || elementType === "hint"; - - /** - * TODO(somewhatabstract, JIRA-XXXX): - * The NodePropTypes generic and specifically the ItemArrayNode could - * contain a variety of types that are not arrays. Probably need to refine - * the type more before doing this work, or rework the `ArrayNodeContent` - * component to be less permissive about the props it accepts. - */ - const children = data.map((subdata, i) => { - const subpath = path.concat(i); - const controls = [ - i > 0 && ( -
- actions.moveArrayElementUp(subpath)} - > -
- -
-
-
- ), - i < data.length - 1 && ( -
- actions.moveArrayElementDown(subpath)} - > - - -
- ), -
- actions.removeArrayElement(subpath)} - > - - -
, - ]; - - return ( -
- -
- ); - }); - - return
{children}
; -}; - -const ObjectNodeContent = ( - props: NodePropTypes, -) => { - const {shape, data, path, ...otherProps} = props; - - // Object iteration order should automatically match the order in which the - // keys were defined in the object literal. So, whatever order semantically - // made sense to the shape's author is the order in which we'll iterate :) - const children = Object.keys(shape.shape).map((subkey) => ( -
- -
- )); - - return
{children}
; -}; - -interface LayoutStatics { - shape: Shape; -} - -type MultiRendererEditorProps = { - // eslint-disable-next-line no-restricted-syntax - Layout: React.ComponentType & LayoutStatics; - apiOptions: APIOptions; - item: Item; - editorMode: EditorMode; - onChange: ChangeHandler; -}; - -class MultiRendererEditor extends React.Component { - layout: React.ElementRef | null | undefined; - - _renderLayout: () => React.ReactElement = () => { - const {Layout, apiOptions, item} = this.props; - - return ( - (this.layout = node)} - item={item} - apiOptions={apiOptions} - /> - ); - }; - - _renderJson: () => React.ReactElement> = () => { - return ( -
- this.props.onChange({editorMode})} - /> - this.props.onChange({item})} - /> -
- ); - }; - - _renderPreview: () => React.ReactElement> = - () => { - return ( -
- - this.props.onChange({editorMode}) - } - /> - {this._renderLayout()} -
- ); - }; - - mergeValueAtPath: (path: Path, newValue?: any) => void = ( - path: Path, - newValue: unknown, - ) => { - this.props.onChange({ - item: lens(this.props.item) - .merge(multiPath(path), newValue) - .freeze(), - }); - }; - - setValueAtPath: (path: Path, newValue?: any) => void = ( - path: Path, - newValue: unknown, - ) => { - this.props.onChange({ - item: lens(this.props.item).set(multiPath(path), newValue).freeze(), - }); - }; - - addArrayElement: (path: Path, shape: Shape) => void = ( - path: Path, - shape: Shape, - ) => { - const currentLength = lens(this.props.item).get(multiPath(path)).length; - const newElementPath = path.concat(currentLength); - const newValue = buildEmptyItemTreeForShape(shape); - this.props.onChange({ - item: lens(this.props.item) - .set(multiPath(newElementPath), newValue) - .freeze(), - }); - }; - - removeArrayElement: (path: Path) => void = (path: Path) => { - this.props.onChange({ - item: lens(this.props.item).del(multiPath(path)).freeze(), - }); - }; - - moveArrayElementDown: (path: Path) => void = (path: Path) => { - // Moving an element down can also be expressed as swapping it with the - // following element. - const index = path[path.length - 1]; - const nextElementIndex = index + 1; - const nextElementPath = path.slice(0, -1).concat(nextElementIndex); - - const element = lens(this.props.item).get(multiPath(path)); - const nextElement = lens(this.props.item).get( - multiPath(nextElementPath), - ); - - this.props.onChange({ - item: lens(this.props.item) - .set(multiPath(path), nextElement) - .set(multiPath(nextElementPath), element) - .freeze(), - }); - }; - - moveArrayElementUp: (path: Path) => void = (path: Path) => { - // Moving an element up can also be expressed as moving the previous - // element down. - const index = path[path.length - 1]; - const previousElementPath = path.slice(0, -1).concat(index - 1); - this.moveArrayElementDown(previousElementPath); - }; - - _renderEdit: () => React.ReactElement> = () => { - const apiOptions = { - ...ApiOptions.defaults, - ...this.props.apiOptions, - } as const; - - const {item} = this.props; - const itemShape: Shape = this.props.Layout.shape; - - return ( -
- this.props.onChange({editorMode})} - /> - - {}}, - useVideo: (() => {}) as any, - }} - > - {({renderers}) => ( - - )} - -
- ); - }; - - score: () => any | undefined = () => { - if (this.layout) { - // @ts-expect-error - TS2571 - Object is of type 'unknown'. - return this.layout.score(); - } - }; - - getSerializedState: () => any | undefined = () => { - if (this.layout) { - // @ts-expect-error - TS2571 - Object is of type 'unknown'. - return this.layout.getSerializedState(); - } - }; - - restoreSerializedState: (state?: any) => void = (state: any) => { - if (this.layout) { - // @ts-expect-error - TS2571 - Object is of type 'unknown'. - this.layout.restoreSerializedState(state); - } - }; - - _renderContent: () => - | React.ReactElement> - | React.ReactNode = () => { - switch (this.props.editorMode) { - case "json": - return this._renderJson(); - case "preview": - return this._renderPreview(); - case "edit": - return this._renderEdit(); - default: - return ( - - this.props.onChange({ - editorMode, - }) - } - /> - ); - } - }; - - render(): React.ReactNode { - return
{this._renderContent()}
; - } -} - -const styles = StyleSheet.create({ - // This is used in a number of places throughout this file. - container: {}, - - // eslint-disable-next-line react-native/no-unused-styles - editor: { - width: "100%", - }, - - // eslint-disable-next-line react-native/no-unused-styles - treePreview: { - position: "relative", - }, - - verticalFlip: { - transform: "scaleY(-1)", - }, - - control: { - marginLeft: 12, - }, - - containerHeader: { - alignItems: "flex-end", - display: "flex", - flexDirection: "row", - }, - - previewCollectionHeader: { - marginBottom: 16, - }, - - containerTitle: { - flexGrow: 1, - margin: 0, - }, - - contentIndent: { - marginLeft: 8, - }, - - hintEditor: { - paddingBottom: 0, - }, - - arrayElement: { - marginBottom: 16, - }, - - // Leaf nodes are already wrapped in cute little pods, so they don't need - // this extra border between array elements. - arrayElementAndNotLeaf: { - borderBottom: "1px solid #ccc", - ":first-child": { - borderTop: "1px solid #ccc", - paddingTop: 16, - }, - }, - - objectElement: { - marginBottom: 16, - }, - - tagsEditor: { - border: "1px solid #ddd", - padding: "5px 10px", - }, - - /** - * A row contains a fixed width editor and a preview that expands as - * needed. - */ - row: { - display: "flex", - position: "relative", - }, - - /** - * The editor. - */ - columnLeft: { - width: 360, - marginRight: 30, - // so that the `position: absolute` of line markers are positioned - // relative to this. - position: "relative", - }, - - /** - * The preview. - */ - columnRight: { - flex: 1, - marginLeft: 30, - position: "relative", - }, - - /** - * Sticks to just under the heading. - */ - sticky: { - position: "sticky", - top: 33, // height of the cute pod for the editor - }, - - /** - * Used for sticky headings. - */ - rowHeading: { - position: "sticky", - backgroundColor: "white", - width: "100%", - // TODO(joshuan): Make this less arbitrary. It should be higher than - // perseus content. - zIndex: 101, - top: -1, - }, -}); - -export default MultiRendererEditor; diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/components/interactive-graph-settings.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/components/interactive-graph-settings.tsx index 2ae43e2008..73a1ef623e 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/components/interactive-graph-settings.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/components/interactive-graph-settings.tsx @@ -159,8 +159,6 @@ class InteractiveGraphSettings extends React.Component { } UNSAFE_componentWillReceiveProps(nextProps) { - // Make sure that state updates when switching - // between different items in a multi-item editor. if ( !_.isEqual(this.props.labels, nextProps.labels) || !_.isEqual(this.props.gridStep, nextProps.gridStep) || diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-ellipse-settings.test.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-ellipse-settings.test.tsx index 056ab99f53..5f20a34d33 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-ellipse-settings.test.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-ellipse-settings.test.tsx @@ -393,7 +393,7 @@ describe("LockedEllipseSettings", () => { // Assert expect(onChangeProps).toHaveBeenCalledWith({ ariaLabel: - "Circle with radius 2, centered at (0, 0). Appearance solid gray border, with no fill.", + "Circle with radius 2, centered at 0 comma 0. Appearance solid gray border, with no fill.", }); }); @@ -421,7 +421,7 @@ describe("LockedEllipseSettings", () => { // Assert expect(onChangeProps).toHaveBeenCalledWith({ ariaLabel: - "Circle with radius 2, centered at (0, 0). Appearance solid gray border, with no fill.", + "Circle with radius 2, centered at 0 comma 0. Appearance solid gray border, with no fill.", }); }); @@ -448,7 +448,7 @@ describe("LockedEllipseSettings", () => { // Assert expect(onChangeProps).toHaveBeenCalledWith({ ariaLabel: - "Ellipse with x radius 2 and y radius 3, centered at (0, 0). Appearance solid gray border, with no fill.", + "Ellipse with x radius 2 and y radius 3, centered at 0 comma 0. Appearance solid gray border, with no fill.", }); }); @@ -476,7 +476,7 @@ describe("LockedEllipseSettings", () => { // Assert expect(onChangeProps).toHaveBeenCalledWith({ ariaLabel: - "Ellipse with x radius 2 and y radius 3, centered at (0, 0), rotated by 90 degrees. Appearance solid gray border, with no fill.", + "Ellipse with x radius 2 and y radius 3, centered at 0 comma 0, rotated by 90 degrees. Appearance solid gray border, with no fill.", }); }); @@ -508,7 +508,7 @@ describe("LockedEllipseSettings", () => { // Assert expect(onChangeProps).toHaveBeenCalledWith({ ariaLabel: - "Circle spoken A with radius 2, centered at (0, 0). Appearance solid gray border, with no fill.", + "Circle spoken A with radius 2, centered at 0 comma 0. Appearance solid gray border, with no fill.", }); }); @@ -544,7 +544,7 @@ describe("LockedEllipseSettings", () => { // Assert expect(onChangeProps).toHaveBeenCalledWith({ ariaLabel: - "Circle spoken A, spoken B with radius 2, centered at (0, 0). Appearance solid gray border, with no fill.", + "Circle spoken A, spoken B with radius 2, centered at 0 comma 0. Appearance solid gray border, with no fill.", }); }); }); diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-ellipse-settings.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-ellipse-settings.tsx index 48d7066586..91ec4936c4 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-ellipse-settings.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-ellipse-settings.tsx @@ -79,7 +79,7 @@ const LockedEllipseSettings = (props: Props) => { str += `Ellipse${visiblelabel} with x radius ${radius[0]} and y radius ${radius[1]}`; } - str += `, centered at (${center[0]}, ${center[1]})`; + str += `, centered at ${center[0]} comma ${center[1]}`; if (!isCircle && angle !== 0) { str += `, rotated by ${radianToDegree(angle)} degrees`; diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-label-settings.test.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-label-settings.test.tsx index 4f6e1b86d0..d58bb7617b 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-label-settings.test.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-label-settings.test.tsx @@ -146,6 +146,7 @@ describe("Locked Label Settings", () => { render( , { diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-label-settings.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-label-settings.tsx index 2413b028c7..c41a385cbf 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-label-settings.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-label-settings.tsx @@ -125,7 +125,7 @@ export default function LockedLabelSettings(props: Props) { onChangeProps({ text: newValue, diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-line-settings.test.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-line-settings.test.tsx index 0d444b82e3..44f40c0183 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-line-settings.test.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-line-settings.test.tsx @@ -625,7 +625,7 @@ describe("LockedLineSettings", () => { // Assert expect(onChangeProps).toHaveBeenCalledWith({ ariaLabel: - "Segment from point at (0, 0) to point at (2, 2). Appearance solid gray.", + "Segment from point at 0 comma 0 to point at 2 comma 2. Appearance solid gray.", }); }); @@ -651,7 +651,7 @@ describe("LockedLineSettings", () => { // Assert expect(onChangeProps).toHaveBeenCalledWith({ ariaLabel: - "Line from point at (0, 0) to point at (2, 2). Appearance solid gray.", + "Line from point at 0 comma 0 to point at 2 comma 2. Appearance solid gray.", }); }); @@ -682,7 +682,7 @@ describe("LockedLineSettings", () => { // Assert expect(onChangeProps).toHaveBeenCalledWith({ ariaLabel: - "Line spoken A from point at (0, 0) to point at (2, 2). Appearance solid gray.", + "Line spoken A from point at 0 comma 0 to point at 2 comma 2. Appearance solid gray.", }); }); @@ -717,7 +717,7 @@ describe("LockedLineSettings", () => { // Assert expect(onChangeProps).toHaveBeenCalledWith({ ariaLabel: - "Line spoken A, spoken B from point at (0, 0) to point at (2, 2). Appearance solid gray.", + "Line spoken A, spoken B from point at 0 comma 0 to point at 2 comma 2. Appearance solid gray.", }); }); @@ -758,7 +758,7 @@ describe("LockedLineSettings", () => { // Assert expect(onChangeProps).toHaveBeenCalledWith({ ariaLabel: - "Line spoken A from point spoken C at (0, 0) to point spoken D at (2, 2). Appearance solid gray.", + "Line spoken A from point spoken C at 0 comma 0 to point spoken D at 2 comma 2. Appearance solid gray.", }); }); @@ -809,7 +809,7 @@ describe("LockedLineSettings", () => { // Assert expect(onChangeProps).toHaveBeenCalledWith({ ariaLabel: - "Line spoken A, spoken B from point spoken C, spoken C2 at (0, 0) to point spoken D, spoken D2 at (2, 2). Appearance solid gray.", + "Line spoken A, spoken B from point spoken C, spoken C2 at 0 comma 0 to point spoken D, spoken D2 at 2 comma 2. Appearance solid gray.", }); }); }); diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-line-settings.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-line-settings.tsx index 982375bd88..da368b5f42 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-line-settings.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-line-settings.tsx @@ -83,7 +83,7 @@ const LockedLineSettings = (props: Props) => { const point1VisibleLabel = await joinLabelsAsSpokenMath(point1.labels); const point2VisibleLabel = await joinLabelsAsSpokenMath(point2.labels); - let str = `${capitalizeKind}${visiblelabel} from point${point1VisibleLabel} at (${point1.coord[0]}, ${point1.coord[1]}) to point${point2VisibleLabel} at (${point2.coord[0]}, ${point2.coord[1]})`; + let str = `${capitalizeKind}${visiblelabel} from point${point1VisibleLabel} at ${point1.coord[0]} comma ${point1.coord[1]} to point${point2VisibleLabel} at ${point2.coord[0]} comma ${point2.coord[1]}`; const lineAppearance = generateLockedFigureAppearanceDescription( lineColor, diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-point-settings.test.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-point-settings.test.tsx index 8f772e4ed6..026d873042 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-point-settings.test.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-point-settings.test.tsx @@ -422,7 +422,7 @@ describe("LockedPointSettings", () => { // generateSpokenMathDetails is mocked to return the input string // with "Spoken math details for " prepended. expect(onChangeProps).toHaveBeenCalledWith({ - ariaLabel: "Point at (0, 0). Appearance solid gray.", + ariaLabel: "Point at 0 comma 0. Appearance solid gray.", }); }); @@ -454,7 +454,7 @@ describe("LockedPointSettings", () => { // generateSpokenMathDetails is mocked to return the input string // with "Spoken math details for " prepended. expect(onChangeProps).toHaveBeenCalledWith({ - ariaLabel: "Point spoken A at (0, 0). Appearance solid gray.", + ariaLabel: "Point spoken A at 0 comma 0. Appearance solid gray.", }); }); @@ -491,7 +491,7 @@ describe("LockedPointSettings", () => { // with "Spoken math details for " prepended. expect(onChangeProps).toHaveBeenCalledWith({ ariaLabel: - "Point spoken A, spoken B at (0, 0). Appearance solid gray.", + "Point spoken A, spoken B at 0 comma 0. Appearance solid gray.", }); }); }); diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-point-settings.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-point-settings.tsx index bceb0c2ddf..757ad272f9 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-point-settings.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-point-settings.tsx @@ -119,7 +119,7 @@ const LockedPointSettings = (props: Props) => { async function getPrepopulatedAriaLabel() { const visiblelabel = await joinLabelsAsSpokenMath(labels); - let str = `Point${visiblelabel} at (${coord[0]}, ${coord[1]})`; + let str = `Point${visiblelabel} at ${coord[0]} comma ${coord[1]}`; const pointAppearance = generateLockedFigureAppearanceDescription(pointColor); diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-polygon-settings.test.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-polygon-settings.test.tsx index 62c3dd0a0a..35d485736a 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-polygon-settings.test.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-polygon-settings.test.tsx @@ -608,7 +608,7 @@ describe("LockedPolygonSettings", () => { // Assert expect(onChangeProps).toHaveBeenCalledWith({ ariaLabel: - "Polygon with 3 sides, vertices at (0, 0), (0, 1), (1, 1). Appearance solid gray border, with no fill.", + "Polygon with 3 sides, vertices at 0 comma 0, 0 comma 1, 1 comma 1. Appearance solid gray border, with no fill.", }); }); @@ -644,7 +644,7 @@ describe("LockedPolygonSettings", () => { // Assert expect(onChangeProps).toHaveBeenCalledWith({ ariaLabel: - "Polygon spoken A with 3 sides, vertices at (0, 0), (0, 1), (1, 1). Appearance solid gray border, with no fill.", + "Polygon spoken A with 3 sides, vertices at 0 comma 0, 0 comma 1, 1 comma 1. Appearance solid gray border, with no fill.", }); }); @@ -684,7 +684,7 @@ describe("LockedPolygonSettings", () => { // Assert expect(onChangeProps).toHaveBeenCalledWith({ ariaLabel: - "Polygon spoken A, spoken B with 3 sides, vertices at (0, 0), (0, 1), (1, 1). Appearance solid gray border, with no fill.", + "Polygon spoken A, spoken B with 3 sides, vertices at 0 comma 0, 0 comma 1, 1 comma 1. Appearance solid gray border, with no fill.", }); }); }); diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-polygon-settings.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-polygon-settings.tsx index e5b82f114c..5619442737 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-polygon-settings.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-polygon-settings.tsx @@ -75,7 +75,7 @@ const LockedPolygonSettings = (props: Props) => { let str = `Polygon${visiblelabel} with ${points.length} sides, vertices at `; // Add the coordinates of each point to the aria label - str += points.map(([x, y]) => `(${x}, ${y})`).join(", "); + str += points.map(([x, y]) => `${x} comma ${y}`).join(", "); const polygonAppearance = generateLockedFigureAppearanceDescription( color, diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-vector-settings.test.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-vector-settings.test.tsx index 1c52f82937..fe208e4aa5 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-vector-settings.test.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-vector-settings.test.tsx @@ -441,7 +441,7 @@ describe("Locked Vector Settings", () => { // Assert expect(onChangeProps).toHaveBeenCalledWith({ ariaLabel: - "Vector from (0, 0) to (2, 2). Appearance solid gray.", + "Vector from 0 comma 0 to 2 comma 2. Appearance solid gray.", }); }); @@ -472,7 +472,7 @@ describe("Locked Vector Settings", () => { // Assert expect(onChangeProps).toHaveBeenCalledWith({ ariaLabel: - "Vector spoken A from (0, 0) to (2, 2). Appearance solid gray.", + "Vector spoken A from 0 comma 0 to 2 comma 2. Appearance solid gray.", }); }); @@ -507,7 +507,7 @@ describe("Locked Vector Settings", () => { // Assert expect(onChangeProps).toHaveBeenCalledWith({ ariaLabel: - "Vector spoken A, spoken B from (0, 0) to (2, 2). Appearance solid gray.", + "Vector spoken A, spoken B from 0 comma 0 to 2 comma 2. Appearance solid gray.", }); }); }); diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-vector-settings.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-vector-settings.tsx index 9fee83fc04..2ae3247bc1 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-vector-settings.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-vector-settings.tsx @@ -72,7 +72,7 @@ const LockedVectorSettings = (props: Props) => { async function getPrepopulatedAriaLabel() { const visiblelabel = await joinLabelsAsSpokenMath(labels); - let str = `Vector${visiblelabel} from (${tail[0]}, ${tail[1]}) to (${tip[0]}, ${tip[1]})`; + let str = `Vector${visiblelabel} from ${tail[0]} comma ${tail[1]} to ${tip[0]} comma ${tip[1]}`; const vectorAppearance = generateLockedFigureAppearanceDescription(lineColor); diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/util.test.ts b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/util.test.ts index 4a32da91d3..43cc54e17b 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/util.test.ts +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/util.test.ts @@ -106,7 +106,7 @@ describe("getDefaultFigureForType", () => { expect(figure).toEqual({ type: "label", coord: [0, 0], - text: "", + text: "label", color: "grayH", size: "medium", }); diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/util.ts b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/util.ts index 500fd06b00..e2d8d5a30b 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/util.ts +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/util.ts @@ -95,7 +95,7 @@ export function getDefaultFigureForType(type: LockedFigureType): LockedFigure { return { type: "label", coord: [0, 0], - text: "", + text: "label", color: DEFAULT_COLOR, size: "medium", }; @@ -151,9 +151,10 @@ export async function generateSpokenMathDetails(mathString: string) { case "specialCharacter": // We don't want the backslash from special character // to show up in the generated aria label. - piece.content.length > 1 - ? (convertedSpeech += piece.content.slice(1)) - : (convertedSpeech += piece.content); + convertedSpeech += + piece.content.length > 1 + ? piece.content.slice(1) + : piece.content; break; default: convertedSpeech += piece.content; diff --git a/packages/perseus-linter/.eslintrc.js b/packages/perseus-linter/.eslintrc.js index 09c8638f13..6c26fa1595 100644 --- a/packages/perseus-linter/.eslintrc.js +++ b/packages/perseus-linter/.eslintrc.js @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable import/no-commonjs */ const path = require("path"); diff --git a/packages/perseus-linter/CHANGELOG.md b/packages/perseus-linter/CHANGELOG.md index 1cf4d6c5a2..71b67a4d18 100644 --- a/packages/perseus-linter/CHANGELOG.md +++ b/packages/perseus-linter/CHANGELOG.md @@ -1,5 +1,14 @@ # @khanacademy/perseus-linter +## 1.2.5 + +### Patch Changes + +- [#1971](https://github.com/Khan/perseus/pull/1971) [`341d316aa`](https://github.com/Khan/perseus/commit/341d316aa8727ebb9e7fde28fc4e2d8779aa3e82) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Move to using optional chaining in a few places to resolve new lint rule violations. + +- Updated dependencies [[`f9906728c`](https://github.com/Khan/perseus/commit/f9906728c1a8f09c91c11d824718b8a06d6a7609), [`341d316aa`](https://github.com/Khan/perseus/commit/341d316aa8727ebb9e7fde28fc4e2d8779aa3e82)]: + - @khanacademy/perseus-core@2.0.0 + ## 1.2.4 ### Patch Changes diff --git a/packages/perseus-linter/package.json b/packages/perseus-linter/package.json index 0db263a939..36b3e164c9 100644 --- a/packages/perseus-linter/package.json +++ b/packages/perseus-linter/package.json @@ -3,7 +3,7 @@ "description": "Linter engine for Perseus", "author": "Khan Academy", "license": "MIT", - "version": "1.2.4", + "version": "1.2.5", "publishConfig": { "access": "public" }, @@ -25,10 +25,10 @@ "test": "bash -c 'yarn --silent --cwd \"../..\" test ${@:0} $($([[ ${@: -1} = -* ]] || [[ ${@: -1} = bash ]]) && echo $PWD)'" }, "dependencies": { - "@khanacademy/perseus-core": "1.5.3" + "@khanacademy/perseus-core": "2.0.0" }, "devDependencies": { - "@khanacademy/pure-markdown": "^0.3.13", + "@khanacademy/pure-markdown": "^0.3.14", "prop-types": "15.6.1" }, "peerDependencies": { diff --git a/packages/perseus/.eslintrc.js b/packages/perseus/.eslintrc.js index 09c8638f13..6c26fa1595 100644 --- a/packages/perseus/.eslintrc.js +++ b/packages/perseus/.eslintrc.js @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable import/no-commonjs */ const path = require("path"); diff --git a/packages/perseus/CHANGELOG.md b/packages/perseus/CHANGELOG.md index 4b2c5b0579..002319ac8c 100644 --- a/packages/perseus/CHANGELOG.md +++ b/packages/perseus/CHANGELOG.md @@ -1,5 +1,57 @@ # @khanacademy/perseus +## 48.1.0 + +### Minor Changes + +- [#1988](https://github.com/Khan/perseus/pull/1988) [`cc9d3a4bc`](https://github.com/Khan/perseus/commit/cc9d3a4bc8866bbdaa09e2bf3fdb321fb9a125c1) Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! - Hiding graphie labels from screen readers. + +* [#1983](https://github.com/Khan/perseus/pull/1983) [`2748a1ff8`](https://github.com/Khan/perseus/commit/2748a1ff82b5e04b12f6b740b29b5e01a0561882) Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! - Updating how svg-image loads data + +## 48.0.0 + +### Major Changes + +- [#1973](https://github.com/Khan/perseus/pull/1973) [`f9906728c`](https://github.com/Khan/perseus/commit/f9906728c1a8f09c91c11d824718b8a06d6a7609) Thanks [@handeyeco](https://github.com/handeyeco)! - Remove `scoreInput` from ServerItemRenderer + +* [#1973](https://github.com/Khan/perseus/pull/1973) [`f9906728c`](https://github.com/Khan/perseus/commit/f9906728c1a8f09c91c11d824718b8a06d6a7609) Thanks [@handeyeco](https://github.com/handeyeco)! - Remove `guessAndScore` from Renderer + +### Patch Changes + +- [#1971](https://github.com/Khan/perseus/pull/1971) [`341d316aa`](https://github.com/Khan/perseus/commit/341d316aa8727ebb9e7fde28fc4e2d8779aa3e82) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Move to using optional chaining in a few places to resolve new lint rule violations. + +* [#1984](https://github.com/Khan/perseus/pull/1984) [`990a3170c`](https://github.com/Khan/perseus/commit/990a3170c2111ddc2c6e6f8f6420a4908cc71c82) Thanks [@catandthemachines](https://github.com/catandthemachines)! - Fixed crashing behavior that occurs in when selecting "unlimited" in polygon option of interactive graph editor. + +* Updated dependencies [[`f9906728c`](https://github.com/Khan/perseus/commit/f9906728c1a8f09c91c11d824718b8a06d6a7609), [`341d316aa`](https://github.com/Khan/perseus/commit/341d316aa8727ebb9e7fde28fc4e2d8779aa3e82)]: + - @khanacademy/perseus-core@2.0.0 + - @khanacademy/kas@0.4.3 + - @khanacademy/keypad-context@1.0.5 + - @khanacademy/kmath@0.1.17 + - @khanacademy/math-input@21.1.7 + - @khanacademy/perseus-linter@1.2.5 + - @khanacademy/pure-markdown@0.3.14 + - @khanacademy/simple-markdown@0.13.7 + +## 47.0.1 + +### Patch Changes + +- [#1975](https://github.com/Khan/perseus/pull/1975) [`335615bab`](https://github.com/Khan/perseus/commit/335615bab18685aa6331c7628c1225bdecc54aab) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Make all types in `perseus-types.ts` originate from it (no longer import Mafs types) + +* [#1953](https://github.com/Khan/perseus/pull/1953) [`acd8bd566`](https://github.com/Khan/perseus/commit/acd8bd56664c6a0849bf3d532be8978115a97dfd) Thanks [@daniellewhyte](https://github.com/daniellewhyte)! - [Dropdown] Change logic for aria-label + +## 47.0.0 + +### Major Changes + +- [#1955](https://github.com/Khan/perseus/pull/1955) [`e7b4db0bf`](https://github.com/Khan/perseus/commit/e7b4db0bf193241a36508804dd6e58c729f0a3db) Thanks [@handeyeco](https://github.com/handeyeco)! - Remove support for MultiRenderer + +## 46.0.1 + +### Patch Changes + +- [#1966](https://github.com/Khan/perseus/pull/1966) [`e22a931d9`](https://github.com/Khan/perseus/commit/e22a931d987291258b66f2c80db3536970a4555d) Thanks [@mark-fitzgerald](https://github.com/mark-fitzgerald)! - [Numeric Input] - BUGFIX - Adjust color contrast of tooltip text + ## 46.0.0 ### Major Changes diff --git a/packages/perseus/package.json b/packages/perseus/package.json index 7be372cce1..e41985efe7 100644 --- a/packages/perseus/package.json +++ b/packages/perseus/package.json @@ -3,7 +3,7 @@ "description": "Core Perseus API (includes renderers and widgets)", "author": "Khan Academy", "license": "MIT", - "version": "46.0.0", + "version": "48.1.0", "publishConfig": { "access": "public" }, @@ -40,14 +40,14 @@ "test": "bash -c 'yarn --silent --cwd \"../..\" test ${@:0} $($([[ ${@: -1} = -* ]] || [[ ${@: -1} = bash ]]) && echo $PWD)'" }, "dependencies": { - "@khanacademy/kas": "^0.4.2", - "@khanacademy/keypad-context": "^1.0.4", - "@khanacademy/kmath": "^0.1.16", - "@khanacademy/math-input": "^21.1.6", - "@khanacademy/perseus-core": "1.5.3", - "@khanacademy/perseus-linter": "^1.2.4", - "@khanacademy/pure-markdown": "^0.3.13", - "@khanacademy/simple-markdown": "^0.13.6", + "@khanacademy/kas": "^0.4.3", + "@khanacademy/keypad-context": "^1.0.5", + "@khanacademy/kmath": "^0.1.17", + "@khanacademy/math-input": "^21.1.7", + "@khanacademy/perseus-core": "2.0.0", + "@khanacademy/perseus-linter": "^1.2.5", + "@khanacademy/pure-markdown": "^0.3.14", + "@khanacademy/simple-markdown": "^0.13.7", "@types/classnames": "2.2.0", "@use-gesture/react": "^10.2.27", "mafs": "0.19.0", diff --git a/packages/perseus/src/__tests__/__snapshots__/renderer.test.tsx.snap b/packages/perseus/src/__tests__/__snapshots__/renderer.test.tsx.snap index eabc59e1b8..bbab3dbcbb 100644 --- a/packages/perseus/src/__tests__/__snapshots__/renderer.test.tsx.snap +++ b/packages/perseus/src/__tests__/__snapshots__/renderer.test.tsx.snap @@ -350,7 +350,7 @@ exports[`renderer snapshots correct answer: correct answer 1`] = ` > @@ -370,7 +370,6 @@ exports[`renderer snapshots correct answer: correct answer 1`] = ` aria-expanded="false" aria-haspopup="listbox" aria-label="Test ARIA label" - aria-labelledby="uid-dropdown-widget-2-dropdown-label" class="button_vr44p2-o_O-shared_u51dsh-o_O-default_3ie67y" id="uid-dropdown-widget-2-dropdown" role="combobox" @@ -438,7 +437,7 @@ exports[`renderer snapshots incorrect answer: incorrect answer 1`] = ` > @@ -458,7 +457,6 @@ exports[`renderer snapshots incorrect answer: incorrect answer 1`] = ` aria-expanded="false" aria-haspopup="listbox" aria-label="Test ARIA label" - aria-labelledby="uid-dropdown-widget-4-dropdown-label" class="button_vr44p2-o_O-shared_u51dsh-o_O-default_3ie67y" id="uid-dropdown-widget-4-dropdown" role="combobox" @@ -526,7 +524,7 @@ exports[`renderer snapshots initial render: initial render 1`] = ` > @@ -546,7 +544,6 @@ exports[`renderer snapshots initial render: initial render 1`] = ` aria-expanded="false" aria-haspopup="listbox" aria-label="Test ARIA label" - aria-labelledby="uid-dropdown-widget-0-dropdown-label" class="button_vr44p2-o_O-shared_u51dsh-o_O-default_3ie67y" id="uid-dropdown-widget-0-dropdown" role="combobox" diff --git a/packages/perseus/src/__tests__/a11y.test.ts b/packages/perseus/src/__tests__/a11y.test.ts index 5a83c58828..c1c101b96d 100644 --- a/packages/perseus/src/__tests__/a11y.test.ts +++ b/packages/perseus/src/__tests__/a11y.test.ts @@ -275,29 +275,6 @@ describe("a11y", () => { const result = violatingWidgets(emptyImageWithoutAltText); expect(result).toHaveLength(0); }); - - it("should handle all these same cases in multi-items", () => { - const decorateItem = (item) => ({ - __type: "content", - ...item.question, - }); - const result = violatingWidgets({ - _multi: { - sharedContext: decorateItem(oneInaccessibleWidget), - questions: [ - decorateItem(noWidgets), - decorateItem(oneAccessibleWidget), - decorateItem(imageWithAltText), - decorateItem(imageWithoutAltText), - decorateItem(emptyImageWithoutAltText), - ], - }, - }); - result.sort(); // don't depend on iteration order - expect(result).toHaveLength(2); - expect(result[0]).toBe("image"); - expect(result[1]).toBe("matrix"); - }); }); }); }); diff --git a/packages/perseus/src/__tests__/renderability.test.ts b/packages/perseus/src/__tests__/renderability.test.ts index 7226d73d96..d1076f8a5c 100644 --- a/packages/perseus/src/__tests__/renderability.test.ts +++ b/packages/perseus/src/__tests__/renderability.test.ts @@ -416,65 +416,5 @@ describe("Renderability", () => { expect(result).toBe(false); }); }); - - describe("Multi-items", () => { - it("should be renderable with no items", () => { - const result = isItemRenderableByVersion( - { - _multi: { - questions: [], - }, - }, - PerseusItemVersion, - ); - expect(result).toBe(true); - }); - - it("should be renderable when all items are", () => { - const result = isItemRenderableByVersion( - { - _multi: { - sharedContext: { - __type: "content", - ...sampleV0InputNumberItem.question, - }, - questions: [ - { - __type: "content", - ...sampleV1MeasurerItem.question, - }, - ], - }, - }, - PerseusItemVersion, - ); - expect(result).toBe(true); - }); - - it("should not be renderable when one item is not", () => { - const result = isItemRenderableByVersion( - { - _multi: { - sharedContext: { - __type: "content", - ...sampleV0InputNumberItem.question, - }, - questions: [ - { - __type: "content", - ...sampleImpossibleWidgetsItem.question, - }, - { - __type: "content", - ...sampleV1MeasurerItem.question, - }, - ], - }, - }, - PerseusItemVersion, - ); - expect(result).toBe(false); - }); - }); }); }); diff --git a/packages/perseus/src/__tests__/renderer-api.test.tsx b/packages/perseus/src/__tests__/renderer-api.test.tsx index f39e19bea8..c4c67919f3 100644 --- a/packages/perseus/src/__tests__/renderer-api.test.tsx +++ b/packages/perseus/src/__tests__/renderer-api.test.tsx @@ -12,6 +12,7 @@ import {ClassNames} from "../perseus-api"; import Renderer from "../renderer"; import {mockStrings} from "../strings"; import {registerAllWidgetsForTesting} from "../util/register-all-widgets-for-testing"; +import {scorePerseusItemTesting} from "../util/test-utils"; import {renderQuestion} from "../widgets/__testutils__/renderQuestion"; import imageItem from "./test-items/image-item"; @@ -46,8 +47,13 @@ describe("Perseus API", function () { // Act act(() => renderer.setInputValue(["input-number 1"], "5")); + const score = scorePerseusItemTesting( + inputNumber1Item.question, + renderer.getUserInputMap(), + ); + // Assert - expect(renderer).toHaveBeenAnsweredCorrectly(); + expect(score).toHaveBeenAnsweredCorrectly(); }); it("should be able to produce a wrong value", function () { @@ -57,8 +63,13 @@ describe("Perseus API", function () { // Act act(() => renderer.setInputValue(["input-number 1"], "3")); + const score = scorePerseusItemTesting( + inputNumber1Item.question, + renderer.getUserInputMap(), + ); + // Assert - expect(renderer).toHaveBeenAnsweredIncorrectly(); + expect(score).toHaveBeenAnsweredIncorrectly(); }); it("should be able to produce an empty score", function () { @@ -66,10 +77,22 @@ describe("Perseus API", function () { const {renderer} = renderQuestion(inputNumber1Item.question); act(() => renderer.setInputValue(["input-number 1"], "3")); - expect(renderer).toHaveBeenAnsweredIncorrectly(); + + let score = scorePerseusItemTesting( + inputNumber1Item.question, + renderer.getUserInputMap(), + ); + + expect(score).toHaveBeenAnsweredIncorrectly(); act(() => renderer.setInputValue(["input-number 1"], "")); - expect(renderer).toHaveInvalidInput(); + + score = scorePerseusItemTesting( + inputNumber1Item.question, + renderer.getUserInputMap(), + ); + + expect(score).toHaveInvalidInput(); }); it("should be able to accept a callback", function (done) { diff --git a/packages/perseus/src/__tests__/renderer.test.tsx b/packages/perseus/src/__tests__/renderer.test.tsx index fccdbddebb..8db770a186 100644 --- a/packages/perseus/src/__tests__/renderer.test.tsx +++ b/packages/perseus/src/__tests__/renderer.test.tsx @@ -63,6 +63,14 @@ describe("renderer", () => { jest.spyOn(Dependencies, "getDependencies").mockReturnValue( testDependencies, ); + + // Mocked for loading graphie in svg-image + global.fetch = jest.fn(() => + Promise.resolve({ + text: () => "", + ok: true, + }), + ) as jest.Mock; }); afterEach(() => { diff --git a/packages/perseus/src/__tests__/server-item-renderer.test.tsx b/packages/perseus/src/__tests__/server-item-renderer.test.tsx index aca6b1a413..60ffc47268 100644 --- a/packages/perseus/src/__tests__/server-item-renderer.test.tsx +++ b/packages/perseus/src/__tests__/server-item-renderer.test.tsx @@ -118,31 +118,6 @@ describe("server item renderer", () => { expect(screen.getByRole("textbox")).toBeVisible(); }); - it("should be invalid if no input provided", async () => { - // Arrange - const {renderer} = renderQuestion(itemWithInput); - - // Act - const score = await act(() => renderer.scoreInput()); - - // Assert - expect(score.correct).toBe(false); - expect(score.empty).toBe(true); - }); - - it("should be answerable", async () => { - // Arrange - const {renderer} = renderQuestion(itemWithInput); - await userEvent.type(screen.getByRole("textbox"), "-42"); - - // Act - const score = await act(() => renderer.scoreInput()); - - // Assert - expect(score.correct).toBe(true); - expect(score.empty).toBe(false); - }); - it("should pass showSolutions to the widgets", () => { // Arrange renderQuestion(itemWithRadioAndExpressionWidgets, Object.freeze({}), { diff --git a/packages/perseus/src/a11y.ts b/packages/perseus/src/a11y.ts index 2803f6db7f..778d79689b 100644 --- a/packages/perseus/src/a11y.ts +++ b/packages/perseus/src/a11y.ts @@ -5,12 +5,9 @@ import _ from "underscore"; -import MultiItems from "./multi-items"; import {traverse} from "./traversal"; import * as Widgets from "./widgets"; -const {findContentNodesInItem, inferItemShape} = MultiItems; - // Iterate over a single Perseus renderer, mutating `widgets` by appending // violating widget types discovered in this item. function traverseRenderer(itemData, widgets: Array) { @@ -31,14 +28,7 @@ export function violatingWidgets(itemData: any): any { // TODO(jordan): Hints as well const widgets = []; - if (itemData._multi) { - const shape = inferItemShape(itemData); - findContentNodesInItem(itemData, shape, (content) => - traverseRenderer(content, widgets), - ); - } else { - traverseRenderer(itemData.question, widgets); - } + traverseRenderer(itemData.question, widgets); // Uniquify the list of widgets (by type) return _.uniq(widgets); diff --git a/packages/perseus/src/components/__tests__/__snapshots__/graph.test.tsx.snap b/packages/perseus/src/components/__tests__/__snapshots__/graph.test.tsx.snap index 6e245cd588..a5722937de 100644 --- a/packages/perseus/src/components/__tests__/__snapshots__/graph.test.tsx.snap +++ b/packages/perseus/src/components/__tests__/__snapshots__/graph.test.tsx.snap @@ -7,6 +7,7 @@ exports[`graph should render 1`] = ` style="width: 400px; height: 400px;" >