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

Updating Article Renderer to be able to return the focusedElement so that we can scroll these elements into view above the keypad. #755

Merged
merged 8 commits into from
Oct 23, 2023
5 changes: 5 additions & 0 deletions .changeset/nice-days-compete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": minor
---

Adding logic to ArticleRenderer so that it can return our currently focused element.
30 changes: 28 additions & 2 deletions packages/perseus/src/__stories__/article-renderer.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
multiSectionArticle,
passageArticle,
articleWithExpression,
multiSectionArticleWithExpression,
} from "../__testdata__/article-renderer.testdata";
import ArticleRenderer from "../article-renderer";
import TestKeypadContextWrapper from "../widgets/__stories__/test-keypad-context-wrapper";
Expand Down Expand Up @@ -55,15 +56,40 @@ export const PassageArticle = ({useNewStyles}): any => (
export const ExpressionArticle = ({useNewStyles}): any => (
<TestKeypadContextWrapper>
<KeypadContext.Consumer>
{({keypadElement, setRenderer, scrollableElement}) => (
{({keypadElement, setRenderer}) => (
<ArticleRenderer
ref={(node) => {
setRenderer(node);
}}
json={articleWithExpression}
dependencies={storybookDependenciesV2}
useNewStyles={useNewStyles}
apiOptions={{isMobile: true, customKeypad: true}}
apiOptions={{
isMobile: true,
customKeypad: true,
}}
keypadElement={keypadElement}
/>
)}
</KeypadContext.Consumer>
</TestKeypadContextWrapper>
);

export const MultiSectionedExpressionArticle = ({useNewStyles}): any => (
<TestKeypadContextWrapper>
<KeypadContext.Consumer>
{({keypadElement, setRenderer}) => (
<ArticleRenderer
ref={(node) => {
setRenderer(node);
}}
json={multiSectionArticleWithExpression}
dependencies={storybookDependenciesV2}
useNewStyles={useNewStyles}
apiOptions={{
isMobile: true,
customKeypad: true,
}}
keypadElement={keypadElement}
/>
)}
Expand Down
85 changes: 85 additions & 0 deletions packages/perseus/src/__testdata__/article-renderer.testdata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,88 @@ export const multiSectionArticle: ReadonlyArray<PerseusRenderer> = [
widgets: {},
},
];

export const multiSectionArticleWithExpression: ReadonlyArray<PerseusRenderer> =
[
{
content:
"### Practice Problem\n\n$8\\cdot(11i+2)=$ [[☃ expression 1]] \n*Your answer should be a complex number in the form $a+bi$ where $a$ and $b$ are real numbers.*",
images: {},
widgets: {
"expression 1": {
alignment: "default",
graded: true,
options: {
answerForms: [
{
considered: "correct",
form: true,
simplify: false,
value: "16+88i",
},
],
buttonSets: ["basic"],
functions: ["f", "g", "h"],
times: false,
},
static: false,
type: "expression",
version: {major: 1, minor: 0},
},
},
},
{
content:
"### Practice Problem\n\n$8\\cdot(11i+2)=$ [[☃ expression 2]] \n*Your answer should be a complex number in the form $a+bi$ where $a$ and $b$ are real numbers.*",
images: {},
widgets: {
"expression 2": {
alignment: "default",
graded: true,
options: {
answerForms: [
{
considered: "correct",
form: true,
simplify: false,
value: "16+88i",
},
],
buttonSets: ["basic"],
functions: ["f", "g", "h"],
times: false,
},
static: false,
type: "expression",
version: {major: 1, minor: 0},
},
},
},
{
content:
"### Practice Problem\n\n$8\\cdot(11i+2)=$ [[☃ expression 3]] \n*Your answer should be a complex number in the form $a+bi$ where $a$ and $b$ are real numbers.*",
images: {},
widgets: {
"expression 3": {
alignment: "default",
graded: true,
options: {
answerForms: [
{
considered: "correct",
form: true,
simplify: false,
value: "16+88i",
},
],
buttonSets: ["basic"],
functions: ["f", "g", "h"],
times: false,
},
static: false,
type: "expression",
version: {major: 1, minor: 0},
},
},
},
];
131 changes: 131 additions & 0 deletions packages/perseus/src/__tests__/article-renderer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import {
StatefulKeypadContextProvider,
KeypadContext,
} from "@khanacademy/math-input";
import {RenderStateRoot} from "@khanacademy/wonder-blocks-core";
import {screen, render, fireEvent, waitFor} from "@testing-library/react";
import * as React from "react";
import "@testing-library/jest-dom"; // Imports custom matchers

import {
testDependencies,
testDependenciesV2,
} from "../../../../testing/test-dependencies";
import KeypadSwitch from "../../../math-input/src/components/keypad-switch";
import {articleWithExpression} from "../__testdata__/article-renderer.testdata";
import ArticleRenderer from "../article-renderer";
import * as Dependencies from "../dependencies";
import {ApiOptions} from "../perseus-api";

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

function KeypadWithContext() {
return (
<KeypadContext.Consumer>
{({setKeypadElement}) => {
return (
<KeypadSwitch
onElementMounted={setKeypadElement}
onDismiss={() => {}}
onAnalyticsEvent={async () => {}}
useV2Keypad
/>
);
}}
</KeypadContext.Consumer>
);
}
// This looks alot like `widgets/__tests__/renderQuestion.jsx', except we use
// the ArticleRenderer instead of Renderer
export const RenderArticle = (
apiOptions: APIOptions = Object.freeze({}),
): {
container: HTMLElement;
renderer: ArticleRenderer;
} => {
let renderer: ArticleRenderer | null = null;
const {container} = render(
<RenderStateRoot>
<StatefulKeypadContextProvider>
<KeypadContext.Consumer>
{({keypadElement, setRenderer}) => (
<ArticleRenderer
ref={(node) => {
renderer = node;
setRenderer(node);
}}
json={articleWithExpression}
dependencies={testDependenciesV2}
apiOptions={{...apiOptions}}
keypadElement={keypadElement}
/>
)}
</KeypadContext.Consumer>
<KeypadWithContext />
</StatefulKeypadContextProvider>
</RenderStateRoot>,
);
if (!renderer) {
throw new Error(`Failed to render!`);
}
return {container, renderer};
};

describe("article renderer", () => {
beforeEach(() => {
// Mock ResizeObserver used by the mobile keypad
window.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}));

jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
testDependencies,
);
});

afterEach(() => {
// The Renderer uses a timer to wait for widgets to complete rendering.
// If we don't spin the timers here, then the timer fires in the test
// _after_ and breaks it because we do setState() in the callback,
// and by that point the component has been unmounted.
jest.runOnlyPendingTimers();
});

it("should render the content", () => {
// Arrange and Act
RenderArticle({
...ApiOptions.defaults,
isMobile: false,
customKeypad: false,
});

// Assert
expect(screen.getByRole("textbox")).toBeInTheDocument();
});

it("should call the onFocusChanged callback when an input is focused", async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

// Arrange
const answerableCallback = jest.fn();

// Act
RenderArticle({
...ApiOptions.defaults,
onFocusChange: answerableCallback,
isMobile: true,
customKeypad: true,
});

const input = screen.getByLabelText(
"Math input box Tap with one or two fingers to open keyboard",
);

fireEvent.touchStart(input);

await waitFor(() => {
expect(screen.getByRole("button", {name: "4"})).toBeVisible();
});
expect(answerableCallback).toHaveBeenCalled();
});
});
Loading