Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/calm-rice-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@khanacademy/perseus": patch
"@khanacademy/perseus-core": patch
---

[Explanation] Add more visual regression tests
1 change: 1 addition & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import {mergeConfig} from "vite";
import {configureSort} from "storybook-multilevel-sort";
import type {StorybookConfig} from "@storybook/react-vite";
Expand Down Expand Up @@ -65,6 +65,7 @@
},
editors: null,
widgets: {
"nested widgets": null,
"**": {
docs: null,
accessibility: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ export function generateExplanationWidget(
// Explanations are not graded
graded: false,
version: {major: 0, minor: 0},
// NOTE: The explanation widget doesn't consume this directly,
// instead, Perseus renders an overlay <div /> over top of the
// widget that intercepts interactions to it.
static: false,
alignment: "default",
options: generateExplanationOptions(),
Expand Down
8 changes: 8 additions & 0 deletions packages/perseus/src/__docs__/hints-renderer-decorator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as React from "react";

// Wraps hints in a `bibliotron-exercise` container to reflect hint styling in prod.
export const bibliotronExerciseDecorator = (Story) => (
<div className="bibliotron-exercise">
<Story />
</div>
);
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,42 @@ import {
import {View} from "@khanacademy/wonder-blocks-core";
import React from "react";

import {themeModes} from "../../../../.storybook/modes";
import HintsRenderer from "../hints-renderer";
import {ApiOptions} from "../perseus-api";
import {storybookDependenciesV2} from "../testing/test-dependencies";
import {ipsumExample} from "../widgets/explanation/explanation.testdata";
import {earthMoonImage} from "../widgets/image/utils";

import {bibliotronExerciseDecorator} from "./hints-renderer-decorator";

import type {Meta, StoryObj} from "@storybook/react-vite";

const defaultApiOptions = ApiOptions.defaults;

const meta: Meta<typeof HintsRenderer> = {
title: "Renderers/Hints Renderer",
title: "Renderers/Hints Renderer/Visual Regression Tests/Initial State",
component: HintsRenderer,
tags: ["!autodocs", "!manifest"],
decorators: [
(Story) => {
return (
<View style={{left: 80}}>
<View style={{paddingLeft: 80}}>
<Story />
</View>
);
},
],
argTypes: {
hintsVisible: {
control: {min: 0},
defaultValue: 3,
parameters: {
docs: {
description: {
component:
"Regression tests for the Hints renderer that do NOT " +
"need any interactions to test, which will be used with " +
"Chromatic.",
},
},
chromatic: {disableSnapshot: false, modes: themeModes},
},
};

Expand All @@ -51,6 +61,7 @@ export default meta;
type Story = StoryObj<typeof HintsRenderer>;

export const Interactive: Story = {
decorators: [bibliotronExerciseDecorator],
args: {
dependencies: storybookDependenciesV2,
hints: [
Expand All @@ -77,6 +88,7 @@ export const Interactive: Story = {
};

export const WithAllInteractiveGraphs: Story = {
decorators: [bibliotronExerciseDecorator],
args: {
apiOptions: defaultApiOptions,
dependencies: storybookDependenciesV2,
Expand Down Expand Up @@ -146,17 +158,7 @@ export const WithAllInteractiveGraphs: Story = {
};

export const ImageWidgetInHint: Story = {
// Need to wrap this in a container with the `bibliotron-exercise` class
// to show what this would really look like with hint styling in prod.
decorators: [
(Story) => {
return (
<div className="bibliotron-exercise">
<Story />
</div>
);
},
],
decorators: [bibliotronExerciseDecorator],
args: {
dependencies: storybookDependenciesV2,
hints: [
Expand All @@ -178,3 +180,11 @@ export const ImageWidgetInHint: Story = {
],
},
};

export const ExplanationWidgetInHint: Story = {
decorators: [bibliotronExerciseDecorator],
args: {
dependencies: storybookDependenciesV2,
hints: [{...ipsumExample, replace: false}],
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {View} from "@khanacademy/wonder-blocks-core";
import React from "react";

import {themeModes} from "../../../../.storybook/modes";
import HintsRenderer from "../hints-renderer";
import {storybookDependenciesV2} from "../testing/test-dependencies";
import {ipsumExample} from "../widgets/explanation/explanation.testdata";

import {bibliotronExerciseDecorator} from "./hints-renderer-decorator";

import type {Meta, StoryObj} from "@storybook/react-vite";

const meta: Meta<typeof HintsRenderer> = {
title: "Renderers/Hints Renderer/Visual Regression Tests/Interactions",
component: HintsRenderer,
tags: ["!autodocs", "!manifest"],
decorators: [
(Story) => {
return (
<View style={{paddingLeft: 80}}>
<Story />
</View>
);
},
],
parameters: {
docs: {
description: {
component:
"Regression tests for the Hints renderer that DO need " +
"some sort of interaction to test, which will be used with " +
"Chromatic.",
},
},
chromatic: {disableSnapshot: false, modes: themeModes},
},
};

export default meta;

type Story = StoryObj<typeof HintsRenderer>;

export const ExplanationWidgetInHint: Story = {
decorators: [bibliotronExerciseDecorator],
args: {
dependencies: storybookDependenciesV2,
hints: [{...ipsumExample, replace: false}],
},
play: async ({canvas, userEvent}) => {
const buttonText =
ipsumExample.widgets["explanation 1"]?.options.showPrompt;
const explanationTrigger = canvas.getByRole("button", {
name: buttonText,
});
await userEvent.click(explanationTrigger);
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {themeModes} from "../../../../../.storybook/modes";

import {articleRendererDecorator} from "./nested-widgets-renderer-decorator";
import {gradedGroupWithRadioAndExplanation} from "./nested-widgets.testdata";

import type {GradedGroupWidget} from "@khanacademy/perseus-core";
import type {Meta, StoryObj} from "@storybook/react-vite";

const meta: Meta = {
title: "Widgets/Nested Widgets/Visual Regression Tests/Initial State",
tags: ["!autodocs", "!manifest"],
parameters: {
docs: {
description: {
component:
"Regression tests for widgets nested inside other widgets that do NOT " +
"need any interactions to test, which will be used with " +
"Chromatic. Stories are all displayed on one page.",
},
},
chromatic: {disableSnapshot: false, modes: themeModes},
controls: {disable: true},
},
};

export default meta;

type GradedGroupStory = StoryObj<GradedGroupWidget["options"]>;

export const GradedGroupWithRadioAndExplanation: GradedGroupStory = {
decorators: [articleRendererDecorator],
parameters: {
question: gradedGroupWithRadioAndExplanation,
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {themeModes} from "../../../../../.storybook/modes";
import {explanationRendererDecorator} from "../explanation/__docs__/explanation-renderer-decorator";

import {articleRendererDecorator} from "./nested-widgets-renderer-decorator";
import {
gradedGroupWithRadioAndExplanation,
imageInContent,
videoInContent,
} from "./nested-widgets.testdata";

import type {
GradedGroupWidget,
PerseusExplanationWidgetOptions,
} from "@khanacademy/perseus-core";
import type {Meta, StoryObj} from "@storybook/react-vite";

const meta: Meta = {
title: "Widgets/Nested Widgets/Visual Regression Tests/Interactions",
tags: ["!autodocs", "!manifest"],
parameters: {
docs: {
description: {
component:
"Regression tests for widgets nested inside other widgets that DO need some sort of interaction to test, which will be used with Chromatic. Stories are displayed on their own page.",
},
},
chromatic: {disableSnapshot: false, modes: themeModes},
controls: {disable: true},
},
};

export default meta;

type GradedGroupStory = StoryObj<GradedGroupWidget["options"]>;
type ExplanationStory = StoryObj<PerseusExplanationWidgetOptions>;

export const GradedGroupExplanationClicked: GradedGroupStory = {
decorators: [articleRendererDecorator],
parameters: {
question: gradedGroupWithRadioAndExplanation,
},
play: async ({canvas, userEvent}) => {
const explanationTrigger = canvas.getByRole("button", {
name: "Show explanation",
});
await userEvent.click(explanationTrigger);
},
};

const videoExample = videoInContent.widgets["explanation 1"]?.options;

export const VideoInContent: ExplanationStory = {
decorators: [explanationRendererDecorator],
args: {
hidePrompt: videoExample.hidePrompt,
explanation: videoExample.explanation,
showPrompt: videoExample.showPrompt,
},
parameters: {
content: videoInContent.content,
widgets: videoExample.widgets,
},
play: async ({canvas, userEvent}) => {
const explanationTrigger = canvas.getByRole("button", {
name: videoExample.showPrompt,
});
await userEvent.click(explanationTrigger);
},
};

const imageExample = imageInContent.widgets["explanation 1"]?.options;

export const ImageInContent: ExplanationStory = {
decorators: [explanationRendererDecorator],
args: {
hidePrompt: imageExample.hidePrompt,
explanation: imageExample.explanation,
showPrompt: imageExample.showPrompt,
},
parameters: {
content: imageInContent.content,
widgets: imageExample.widgets,
},
play: async ({canvas, userEvent}) => {
const explanationTrigger = canvas.getByRole("button", {
name: imageExample.showPrompt,
});
await userEvent.click(explanationTrigger);
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as React from "react";

import ArticleRenderer from "../../article-renderer";
import {storybookDependenciesV2} from "../../testing/test-dependencies";

export const articleRendererDecorator = (_, {parameters}) => {
return (
<ArticleRenderer
json={parameters.question}
dependencies={storybookDependenciesV2}
/>
);
};
Loading
Loading