@khanacademy/perseus 17.3.1 → 17.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,11 @@
1
1
  import * as PerseusLinter from "@khanacademy/perseus-linter";
2
2
  /**
3
- * A copy of the ItemRenderer which renders its question renderer and hints
4
- * renderer normally instead of ReactDOM.render()ing them into elements in the
5
- * DOM.
3
+ * The main item (aka Exercise) renderer in Perseus. This component renders its
4
+ * question renderer and hints renderer using standard React practices (an
5
+ * earlier ItemRenderer component used to use jQuery and ReactDOM.render()ing
6
+ * to render into elements it didn't own.
6
7
  *
7
- * This allows this component to be used in server-rendering of a perseus
8
- * exercise.
8
+ * This component is compatible with server-rendering of a perseus exercise.
9
9
  */
10
10
  import * as React from "react";
11
11
  import Renderer from "./renderer";
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Core Perseus API (includes renderers and widgets)",
4
4
  "author": "Khan Academy",
5
5
  "license": "MIT",
6
- "version": "17.3.1",
6
+ "version": "17.3.3",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -0,0 +1,76 @@
1
+ import {screen, render} from "@testing-library/react";
2
+ import * as React from "react";
3
+
4
+ import {testDependencies} from "../../../../testing/test-dependencies";
5
+ import * as Dependencies from "../dependencies";
6
+ import ErrorBoundary from "../error-boundary";
7
+
8
+ import "@testing-library/jest-dom";
9
+
10
+ const ProblematicComponent = () => {
11
+ throw new Error("I can haz error");
12
+ };
13
+
14
+ describe("error boundary", () => {
15
+ beforeEach(() => {
16
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
17
+ testDependencies,
18
+ );
19
+ });
20
+
21
+ it("should render children", () => {
22
+ render(
23
+ <ErrorBoundary>
24
+ <span>Hello world</span>
25
+ </ErrorBoundary>,
26
+ );
27
+
28
+ expect(screen.getByText("Hello world")).toBeInTheDocument();
29
+ });
30
+
31
+ it("should render error icon when children error", () => {
32
+ render(
33
+ <ErrorBoundary>
34
+ <ProblematicComponent />
35
+ </ErrorBoundary>,
36
+ );
37
+
38
+ expect(screen.getByRole("img")).toBeInTheDocument();
39
+ expect(screen.getByTitle("Rendering Error!")).toBeInTheDocument();
40
+ });
41
+
42
+ it("should call the onError callback when children error", () => {
43
+ const onErrorCallback = jest.fn();
44
+
45
+ render(
46
+ <ErrorBoundary onError={onErrorCallback}>
47
+ <ProblematicComponent />
48
+ </ErrorBoundary>,
49
+ );
50
+
51
+ expect(onErrorCallback).toHaveBeenCalled();
52
+ });
53
+
54
+ it("should log the error when children error", () => {
55
+ const errorSpy = jest.spyOn(testDependencies.Log, "error");
56
+
57
+ render(
58
+ <ErrorBoundary>
59
+ <ProblematicComponent />
60
+ </ErrorBoundary>,
61
+ );
62
+
63
+ expect(errorSpy).toHaveBeenCalledWith(
64
+ "Unhandled Perseus error: I can haz error",
65
+ "Internal",
66
+ {
67
+ cause: expect.anything(),
68
+ loggedMetadata: {
69
+ componentStack: `
70
+ in ProblematicComponent
71
+ in ErrorBoundary`,
72
+ },
73
+ },
74
+ );
75
+ });
76
+ });
@@ -22,13 +22,27 @@ class ErrorBoundary extends React.Component<Props, State> {
22
22
  componentDidCatch(error: Error, info: any) {
23
23
  this.setState({error: error.toString()});
24
24
  this.props.onError?.(error, info);
25
- Log.error("Perseus error boundary caught error", Errors.Internal, {
26
- cause: error,
27
- loggedMetadata: {
28
- componentStack: info.componentStack,
29
- ...this.props.metadata,
25
+ Log.error(
26
+ // NOTE(jeremy): We concatenate the error messsage here. Typical
27
+ // Khan Academy error handling guidance says that you should never
28
+ // "build" the error message that might be sent to our error
29
+ // reporting tool (currently Sentry). However, if we don't
30
+ // differentiate between the different errors that are thrown, they
31
+ // all end up being grouped as a single Sentry event, which is very
32
+ // unhelpful.
33
+ "Unhandled Perseus error: " + error.message,
34
+ Errors.Internal,
35
+ {
36
+ cause: error,
37
+ loggedMetadata: {
38
+ componentStack:
39
+ !!info && "componentStack" in info
40
+ ? info.componentStack
41
+ : "componentStack not provided",
42
+ ...this.props.metadata,
43
+ },
30
44
  },
31
- });
45
+ );
32
46
  }
33
47
 
34
48
  render(): React.ReactNode {
@@ -39,7 +53,8 @@ class ErrorBoundary extends React.Component<Props, State> {
39
53
  // errors into inline elements.
40
54
  // TODO(michaelpolyak): Link error icon to "Report a problem".
41
55
  return (
42
- <svg height="16" width="16" viewBox="0 0 16 16">
56
+ <svg height="16" width="16" viewBox="0 0 16 16" role={"img"}>
57
+ <title>Rendering Error!</title>
43
58
  <path
44
59
  d="m8 16c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm0-3c0.55 0 1-0.45 1-1s-0.45-1-1-1-1 0.45-1 1 0.45 1 1 1zm0-9c-0.55 0-1 0.45-1 1v4c0 0.55.45 1 1 1s1-0.45 1-1v-4c0-0.55-0.45-1-1-1z"
45
60
  fill="#d92916"
@@ -2,12 +2,12 @@
2
2
  import * as PerseusLinter from "@khanacademy/perseus-linter";
3
3
  import {StyleSheet, css} from "aphrodite";
4
4
  /**
5
- * A copy of the ItemRenderer which renders its question renderer and hints
6
- * renderer normally instead of ReactDOM.render()ing them into elements in the
7
- * DOM.
5
+ * The main item (aka Exercise) renderer in Perseus. This component renders its
6
+ * question renderer and hints renderer using standard React practices (an
7
+ * earlier ItemRenderer component used to use jQuery and ReactDOM.render()ing
8
+ * to render into elements it didn't own.
8
9
  *
9
- * This allows this component to be used in server-rendering of a perseus
10
- * exercise.
10
+ * This component is compatible with server-rendering of a perseus exercise.
11
11
  */
12
12
  import * as React from "react";
13
13
  import _ from "underscore";
@@ -2,7 +2,24 @@
2
2
  @import "./variables.less";
3
3
  @import "./util.less";
4
4
 
5
- // Article Renderer
5
+ // Common styles for _all_ of Perseus
6
+ .framework-perseus {
7
+ code {
8
+ font-family: Courier, monospace;
9
+ }
10
+
11
+ pre {
12
+ background-color: @gray95;
13
+ border-radius: 4px;
14
+ color: @gray17;
15
+ font-size: 18px;
16
+ padding: 16px;
17
+ white-space: pre;
18
+ overflow: auto;
19
+ }
20
+ }
21
+
22
+ // Desktop Articles
6
23
  .framework-perseus.perseus-article:not(.perseus-mobile) {
7
24
  @textColor: #21242c;
8
25
  .body-text() {
@@ -190,25 +207,12 @@
190
207
  }
191
208
  }
192
209
 
193
- code {
194
- font-family: Courier, monospace;
195
- }
196
-
197
210
  pre {
198
- background-color: @gray95;
199
- border-radius: 4px;
200
- color: @gray17;
201
- font-size: 18px;
202
- padding: 16px;
203
- white-space: pre;
204
- overflow: auto;
205
211
  margin: 0 -16px 32px -16px;
206
212
  }
207
213
  }
208
214
 
209
- // HACK(emily): This is a copy of a bunch of the mixins from the block of
210
- // styles down below. However, those styles are limited to mobile devices
211
- // only, so this block is for non-mobile devices.
215
+ // Desktop Exercises
212
216
  .framework-perseus:not(.perseus-article):not(.perseus-mobile) {
213
217
  // Mixins
214
218
  .text(@fontSize; @lineHeight; @color) {
@@ -251,6 +255,7 @@
251
255
  .choice-text(14px, 1.25);
252
256
  }
253
257
 
258
+ // Mobile Articles and Exercises
254
259
  .framework-perseus.perseus-mobile {
255
260
  .perseus-article {
256
261
  .perseus-widget-container {