Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create PhET simulation widget #1512

Merged
merged 33 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
1e69d6b
Initial widget creation
aemandine Jul 15, 2024
8602fdc
Added locale checks on iframe
aemandine Jul 31, 2024
691bfe3
Small text update in storybook test data
aemandine Aug 1, 2024
fa03b2d
Check locale availability
aemandine Aug 1, 2024
1d03ea2
Changed URL from string to URL type
aemandine Aug 2, 2024
eb1b3bd
Adjusting state vs class vars
aemandine Aug 2, 2024
059d87d
Add different locale checker
aemandine Aug 6, 2024
3207721
Update available locale logic
aemandine Aug 9, 2024
6a741a1
Rebase onto main
aemandine Aug 9, 2024
b9859a9
Make width and height auto
aemandine Aug 9, 2024
256bfa5
Design updates from pair with Caitlyn
aemandine Aug 12, 2024
6aa63ac
Combine strings file changes
aemandine Aug 13, 2024
b479863
Combine import changes
aemandine Aug 13, 2024
9a41c13
Add PhET editor skeleton
aemandine Aug 13, 2024
047b960
Add tests
aemandine Aug 13, 2024
81a8989
Mock fetch call for jest tests
aemandine Aug 14, 2024
b2f4049
Fix state and props usage
aemandine Aug 14, 2024
ccdc783
Remove package.json changes
aemandine Aug 19, 2024
424fce9
Address PR comments
aemandine Aug 19, 2024
c70aec2
Make prettier
aemandine Aug 19, 2024
b439aed
Add error test
aemandine Aug 19, 2024
1916140
Remove the error test temporarily
aemandine Aug 19, 2024
bbdc466
Remove unnecessary imports
aemandine Aug 19, 2024
fce9cff
Make prettier
aemandine Aug 19, 2024
38d4545
Remove direct use of older Promise-based APIs
aemandine Aug 19, 2024
be76aba
Update tests
aemandine Aug 19, 2024
2538ea2
Make prettier
aemandine Aug 19, 2024
6ae1821
Remove unnecessary comments
aemandine Aug 19, 2024
c655233
Add makeSafeUrl tests
aemandine Aug 19, 2024
3767590
Lint
aemandine Aug 19, 2024
267c645
Remove unnecessary View tag
aemandine Aug 19, 2024
1338f6c
Refactor story into newer format
aemandine Aug 19, 2024
fbdc96b
Fix import issue
aemandine Aug 19, 2024
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
5 changes: 5 additions & 0 deletions .changeset/empty-seals-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": minor
---

Add PhET widget
2 changes: 2 additions & 0 deletions packages/perseus-editor/src/all-editors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import OrdererEditor from "./widgets/orderer-editor";
import PassageEditor from "./widgets/passage-editor";
import PassageRefEditor from "./widgets/passage-ref-editor";
import PassageRefTargetEditor from "./widgets/passage-ref-target-editor";
import PhetSimEditor from "./widgets/phet-sim-editor";
import PlotterEditor from "./widgets/plotter-editor";
import PythonProgramEditor from "./widgets/python-program-editor";
import RadioEditor from "./widgets/radio/editor";
Expand Down Expand Up @@ -64,6 +65,7 @@ export default [
PassageEditor,
PassageRefEditor,
PassageRefTargetEditor,
PhetSimEditor,
PlotterEditor,
PythonProgramEditor,
SimpleMarkdownTesterEditor,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {action} from "@storybook/addon-actions";

import PhetSimEditor from "../phet-sim-editor";

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

const meta: Meta<typeof PhetSimEditor> = {
component: PhetSimEditor,
title: "PerseusEditor/Widgets/PhET Simulation Editor",
};

export default meta;
type Story = StoryObj<typeof PhetSimEditor>;

export const Primary: Story = {
args: {
onChange: action("onChange"),
},
};
46 changes: 46 additions & 0 deletions packages/perseus-editor/src/widgets/phet-sim-editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* eslint-disable @khanacademy/ts-no-error-suppressions */
/* eslint-disable react/sort-comp */
import {Changeable, EditorJsonify} from "@khanacademy/perseus";
import * as React from "react";

import BlurInput from "../components/blur-input";

type PhetSimEditorProps = any;

class PhetSimEditor extends React.Component<PhetSimEditorProps> {
static propTypes = {
...Changeable.propTypes,
};

static widgetName = "phet-sim" as const;

static defaultProps: PhetSimEditorProps = {
url: "",
description: "",
};

change: (arg1: any, arg2: any) => any = (...args) => {
return Changeable.change.apply(this, args);
};

render(): React.ReactNode {
return (
<div>
<label>
URL:
<BlurInput
value={this.props.url}
// @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
onChange={this.change("url")}
/>
</label>
</div>
);
}

serialize: () => any = () => {
return EditorJsonify.serialize.call(this);
};
}

export default PhetSimEditor;
3 changes: 2 additions & 1 deletion packages/perseus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
},
"dependencies": {
"@khanacademy/kas": "^0.3.11",
"@khanacademy/kmath": "^0.1.13",
"@khanacademy/keypad-context": "^1.0.0",
"@khanacademy/kmath": "^0.1.13",
"@khanacademy/math-input": "^21.0.0",
"@khanacademy/perseus-core": "1.5.0",
"@khanacademy/perseus-linter": "^1.1.0",
Expand All @@ -61,6 +61,7 @@
"@khanacademy/wonder-blocks-dropdown": "5.3.0",
"@khanacademy/wonder-blocks-form": "^4.7.1",
"@khanacademy/wonder-blocks-icon": "4.1.0",
"@khanacademy/wonder-blocks-icon-button": "^5.3.3",
aemandine marked this conversation as resolved.
Show resolved Hide resolved
"@khanacademy/wonder-blocks-layout": "2.0.32",
"@khanacademy/wonder-blocks-link": "6.1.1",
"@khanacademy/wonder-blocks-pill": "2.2.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/perseus/src/extra-widgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import Orderer from "./widgets/orderer";
import Passage from "./widgets/passage";
import PassageRef from "./widgets/passage-ref";
import PassageRefTarget from "./widgets/passage-ref-target";
import PhetSim from "./widgets/phet-sim";
import Plotter from "./widgets/plotter";
import PythonProgram from "./widgets/python-program";
import Sorter from "./widgets/sorter";
Expand Down Expand Up @@ -58,6 +59,7 @@ export default [
Passage,
PassageRef,
PassageRefTarget,
PhetSim,
Plotter,
PythonProgram,
Sorter,
Expand Down
15 changes: 15 additions & 0 deletions packages/perseus/src/perseus-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export type PerseusWidgetsMap = {
[key in `passage-ref ${number}`]: PassageRefWidget;
} & {
[key in `passage-ref-target ${number}`]: PassageRefWidget;
} & {
[key in `phet-sim ${number}`]: PhetSimWidget;
} & {
[key in `plotter ${number}`]: PlotterWidget;
} & {
Expand Down Expand Up @@ -232,6 +234,8 @@ export type PassageRefWidget = Widget<'passage-ref', PerseusPassageRefWidgetOpti
// prettier-ignore
export type PassageWidget = Widget<'passage', PerseusPassageWidgetOptions>;
// prettier-ignore
export type PhetSimWidget = Widget<'phet-sim', PerseusPhetSimWidgetOptions>;
// prettier-ignore
export type PlotterWidget = Widget<'plotter', PerseusPlotterWidgetOptions>;
// prettier-ignore
export type PythonProgramWidget = Widget<'python-program', PerseusPythonProgramWidgetOptions>;
Expand Down Expand Up @@ -286,6 +290,7 @@ export type PerseusWidget =
| OrdererWidget
| PassageRefWidget
| PassageWidget
| PhetSimWidget
| PlotterWidget
| PythonProgramWidget
| RadioWidget
Expand Down Expand Up @@ -1536,6 +1541,15 @@ export type PerseusIFrameWidgetOptions = {
static: boolean;
};

export type PerseusPhetSimWidgetOptions = {
// A URL to display, must start with https://phet.colorado.edu/
url: string;
// Translatable Text; Description of the sim for Khanmigo and alt text
description: string;
// Always false
static: boolean;
};

export type PerseusVideoWidgetOptions = {
location: string;
static?: boolean;
Expand Down Expand Up @@ -1604,6 +1618,7 @@ export type PerseusWidgetOptions =
| PerseusPassageRefTargetWidgetOptions
| PerseusPassageRefWidgetOptions
| PerseusPassageWidgetOptions
| PerseusPhetSimWidgetOptions
| PerseusPlotterWidgetOptions
| PerseusRadioWidgetOptions
| PerseusSimpleMarkdownTesterWidgetOptions
Expand Down
8 changes: 8 additions & 0 deletions packages/perseus/src/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ export type PerseusStrings = {
videoWrapper: string;
mathInputTitle: string;
mathInputDescription: string;
simulationLoadFail: string;
simulationLocaleWarning: string;
};

/**
Expand Down Expand Up @@ -291,6 +293,9 @@ export const strings: {
mathInputTitle: "mathematics keyboard",
mathInputDescription:
"Use keyboard/mouse to interact with math-based input fields",
simulationLoadFail: "Sorry, this simulation cannot load.",
simulationLocaleWarning:
"Sorry, this simulation isn't available in your language.",
};

/**
Expand Down Expand Up @@ -441,4 +446,7 @@ export const mockStrings: PerseusStrings = {
mathInputTitle: "mathematics keyboard",
mathInputDescription:
"Use keyboard/mouse to interact with math-based input fields",
simulationLoadFail: "Sorry, this simulation cannot load.",
simulationLocaleWarning:
"Sorry, this simulation isn't available in your language.",
};
3 changes: 3 additions & 0 deletions packages/perseus/src/util/widget-enum-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type WidgetName =
| "passage"
| "passage-ref"
| "passage-ref-target"
| "phet-sim"
| "plotter"
| "python-program"
| "reaction-diagram"
Expand Down Expand Up @@ -73,6 +74,7 @@ type WidgetEnum =
| "PASSAGE"
| "PASSAGE_REF"
| "PASSAGE_REF_TARGET"
| "PHET_SIM"
| "PLOTTER"
| "PYTHON_PROGRAM"
| "REACTION_DIAGRAM"
Expand Down Expand Up @@ -113,6 +115,7 @@ const widgetNameToEnum: Record<WidgetName, WidgetEnum> = {
passage: "PASSAGE",
"passage-ref": "PASSAGE_REF",
"passage-ref-target": "PASSAGE_REF_TARGET",
"phet-sim": "PHET_SIM",
plotter: "PLOTTER",
"python-program": "PYTHON_PROGRAM",
"reaction-diagram": "REACTION_DIAGRAM",
Expand Down
18 changes: 18 additions & 0 deletions packages/perseus/src/widgets/__stories__/phet-sim.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {PhetSim} from "../phet-sim";

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

const meta: Meta<typeof PhetSim> = {
component: PhetSim,
title: "Perseus/Widgets/PhET Simulation",
};

export default meta;
type Story = StoryObj<typeof PhetSim>;

export const Primary: Story = {
args: {
url: "https://phet.colorado.edu/sims/html/projectile-data-lab/latest/projectile-data-lab_all.html",
description: "Projectile Data Lab",
},
};
40 changes: 40 additions & 0 deletions packages/perseus/src/widgets/__testdata__/phet-sim.testdata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type {PerseusRenderer} from "../../perseus-types";

export const question1: PerseusRenderer = {
content:
"Do this fun PhET simulation! A projectile data lab!\n[[\u2603 phet-sim 1]]\n",
images: {},
widgets: {
"phet-sim 1": {
graded: false,
version: {major: 0, minor: 0},
static: false,
type: "phet-sim",
options: {
url: "https://phet.colorado.edu/sims/html/projectile-data-lab/latest/projectile-data-lab_all.html",
description: "Projectile Data Lab",
static: false,
},
alignment: "default",
},
},
};

export const nonPhetUrl: PerseusRenderer = {
content: "This should display an error!\n[[\u2603 phet-sim 2]]\n",
images: {},
widgets: {
"phet-sim 2": {
graded: false,
version: {major: 0, minor: 0},
static: false,
type: "phet-sim",
options: {
url: "https://google.com/",
description: "Google",
static: false,
},
alignment: "default",
},
},
};
91 changes: 91 additions & 0 deletions packages/perseus/src/widgets/__tests__/phet-sim.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {screen, waitFor} from "@testing-library/react";

import {testDependencies} from "../../../../../testing/test-dependencies";
import * as Dependencies from "../../dependencies";
import {nonPhetUrl, question1} from "../__testdata__/phet-sim.testdata";
import {makeSafeUrl} from "../phet-sim";

import {renderQuestion} from "./renderQuestion";

import type {APIOptions} from "../../types";

describe("phet-sim widget", () => {
beforeEach(() => {
jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
testDependencies,
);
global.fetch = jest.fn(() =>
Promise.resolve({
json: () =>
Promise.resolve({
en: {stringConstant: "localized string"},
}),
ok: true,
}),
) as jest.Mock;
global.URL.canParse = jest.fn(() => true) as jest.Mock;
});

it("should display with valid PhET URL", async () => {
// Arrange
const apiOptions: APIOptions = {
isMobile: false,
};

// Act
renderQuestion(question1, apiOptions);

// Assert
await waitFor(() => {
expect(screen.queryByTitle("Projectile Data Lab")).toHaveAttribute(
"src",
"https://phet.colorado.edu/sims/html/projectile-data-lab/latest/projectile-data-lab_all.html?locale=en",
);
});
});

it("should display an error for a non-PhET URL", async () => {
// Arrange
const apiOptions: APIOptions = {
isMobile: false,
};

// Act
renderQuestion(nonPhetUrl, apiOptions);

// Assert
await waitFor(() => {
expect(screen.queryByTitle("Google")).toHaveAttribute(
"srcDoc",
"Sorry, this simulation cannot load.",
);
});
});

it("should make the URL with the correct locale", async () => {
// Arrange
const baseUrl =
"https://phet.colorado.edu/sims/html/projectile-data-lab/latest/projectile-data-lab_all.html";
const locale = "fr";

// Act
const url: URL | null = makeSafeUrl(baseUrl, locale);

// Assert
expect(url?.toString()).toBe(
"https://phet.colorado.edu/sims/html/projectile-data-lab/latest/projectile-data-lab_all.html?locale=fr",
);
});

it("should erase a non-PhET URL", async () => {
// Arrange
const baseUrl = "https://google.com";
const locale = "en";

// Act
const url: URL | null = makeSafeUrl(baseUrl, locale);

// Assert
expect(url).toBe(null);
});
});
Loading
Loading