Skip to content

Commit 79a6fc6

Browse files
authored
[Numeric Input] Fix rendering error in InputWithExamples, changing replaceAll to replace to be old browser compatible (#2903)
## Summary: Fix rendering error in InputWithExamples, changing replaceAll to replace to be old browser compatible Issue: LEMS-XXXX ## Test plan: Author: ivyolamit Reviewers: jeremywiebe Required Reviewers: Approved By: jeremywiebe Checks: ✅ 10 checks were successful Pull Request URL: #2903
1 parent 1ec4595 commit 79a6fc6

File tree

3 files changed

+221
-4
lines changed

3 files changed

+221
-4
lines changed

.changeset/stupid-rockets-float.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/perseus": patch
3+
---
4+
5+
Fix rendering error in InputWithExamples, changing replaceAll to replace to be old browser compatible
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import {render, screen} from "@testing-library/react";
2+
import {userEvent as userEventLib} from "@testing-library/user-event";
3+
import * as React from "react";
4+
5+
import {testDependencies} from "../../../../../testing/test-dependencies";
6+
import * as Dependencies from "../../dependencies";
7+
8+
import InputWithExamples from "./input-with-examples";
9+
10+
import type {UserEvent} from "@testing-library/user-event";
11+
12+
describe("InputWithExamples", () => {
13+
let userEvent: UserEvent;
14+
15+
beforeEach(() => {
16+
userEvent = userEventLib.setup({
17+
advanceTimers: jest.advanceTimersByTime,
18+
});
19+
20+
jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
21+
testDependencies,
22+
);
23+
});
24+
25+
const defaultProps = {
26+
value: "",
27+
onChange: jest.fn(),
28+
examples: ["example 1", "example 2"],
29+
shouldShowExamples: true,
30+
id: "test-input",
31+
};
32+
33+
it("renders without crashing", () => {
34+
render(<InputWithExamples {...defaultProps} />);
35+
expect(screen.getByRole("textbox")).toBeInTheDocument();
36+
});
37+
38+
it("displays examples tooltip when focused and shouldShowExamples is true", async () => {
39+
render(<InputWithExamples {...defaultProps} />);
40+
41+
const input = screen.getByRole("textbox");
42+
await userEvent.click(input);
43+
44+
// The tooltip content is rendered in the tooltip, not the aria text
45+
expect(
46+
screen.getByText("example 1 example 2", {selector: ".paragraph"}),
47+
).toBeInTheDocument();
48+
});
49+
50+
it("does not display examples tooltip when shouldShowExamples is false", async () => {
51+
render(
52+
<InputWithExamples {...defaultProps} shouldShowExamples={false} />,
53+
);
54+
55+
const input = screen.getByRole("textbox");
56+
await userEvent.click(input);
57+
58+
expect(
59+
screen.queryByText("example 1 example 2"),
60+
).not.toBeInTheDocument();
61+
});
62+
63+
describe("aria text processing", () => {
64+
it("removes asterisks from examples for screen readers", () => {
65+
const examplesWithAsterisks = ["*example*", "test*"];
66+
render(
67+
<InputWithExamples
68+
{...defaultProps}
69+
examples={examplesWithAsterisks}
70+
/>,
71+
);
72+
73+
// The aria text should be in a hidden span and formatted with first example, newline, then "or" and rest
74+
const ariaElement = screen.getByText((content, element) => {
75+
return (
76+
element?.tagName === "SPAN" &&
77+
content.includes("example") &&
78+
content.includes("test") &&
79+
!content.includes("*")
80+
);
81+
});
82+
expect(ariaElement).toHaveStyle({display: "none"});
83+
});
84+
85+
it("removes dollar signs from examples for screen readers", () => {
86+
const examplesWithDollars = ["$example$", "test$"];
87+
render(
88+
<InputWithExamples
89+
{...defaultProps}
90+
examples={examplesWithDollars}
91+
/>,
92+
);
93+
94+
const ariaElement = screen.getByText((content, element) => {
95+
return (
96+
element?.tagName === "SPAN" &&
97+
content.includes("example") &&
98+
content.includes("test") &&
99+
!content.includes("$")
100+
);
101+
});
102+
expect(ariaElement).toHaveStyle({display: "none"});
103+
});
104+
105+
it("replaces TeX pi notation with readable text for screen readers", () => {
106+
const examplesWithPi = ["\\ \\text{pi}", "2\\ \\text{pi}"];
107+
render(
108+
<InputWithExamples
109+
{...defaultProps}
110+
examples={examplesWithPi}
111+
/>,
112+
);
113+
114+
const ariaElement = screen.getByText((content, element) => {
115+
return (
116+
element?.tagName === "SPAN" &&
117+
content.includes("pi") &&
118+
content.includes("2") &&
119+
!content.includes("\\text{pi}")
120+
);
121+
});
122+
expect(ariaElement).toHaveStyle({display: "none"});
123+
});
124+
125+
it("replaces backslash-space notation with 'and' for screen readers", () => {
126+
const examplesWithBackslash = ["example\\ test", "another"];
127+
render(
128+
<InputWithExamples
129+
{...defaultProps}
130+
examples={examplesWithBackslash}
131+
/>,
132+
);
133+
134+
const ariaElement = screen.getByText((content, element) => {
135+
return (
136+
element?.tagName === "SPAN" &&
137+
content.includes("example") &&
138+
content.includes("and") &&
139+
content.includes("test") &&
140+
content.includes("another")
141+
);
142+
});
143+
expect(ariaElement).toHaveStyle({display: "none"});
144+
});
145+
146+
it("processes multiple text replacements correctly", () => {
147+
const complexExamples = ["*$\\ \\text{pi}*$", "\\ test"];
148+
render(
149+
<InputWithExamples
150+
{...defaultProps}
151+
examples={complexExamples}
152+
/>,
153+
);
154+
155+
// Should remove *, $, replace \text{pi} with pi, and \ with "and"
156+
const ariaElement = screen.getByText((content, element) => {
157+
return (
158+
element?.tagName === "SPAN" &&
159+
content.includes("pi") &&
160+
content.includes("and") &&
161+
content.includes("test") &&
162+
!content.includes("*") &&
163+
!content.includes("$")
164+
);
165+
});
166+
expect(ariaElement).toHaveStyle({display: "none"});
167+
});
168+
169+
it("handles empty examples array", () => {
170+
render(
171+
<InputWithExamples
172+
{...defaultProps}
173+
examples={[]}
174+
shouldShowExamples={false}
175+
/>,
176+
);
177+
178+
expect(screen.getByRole("textbox")).toBeInTheDocument();
179+
});
180+
181+
it("processes examples with multiple occurrences of the same character", () => {
182+
const examplesWithMultiple = ["***test***", "$$$money$$$"];
183+
render(
184+
<InputWithExamples
185+
{...defaultProps}
186+
examples={examplesWithMultiple}
187+
/>,
188+
);
189+
190+
const ariaElement = screen.getByText((content, element) => {
191+
return (
192+
element?.tagName === "SPAN" &&
193+
content.includes("test") &&
194+
content.includes("money") &&
195+
!content.includes("*") &&
196+
!content.includes("$")
197+
);
198+
});
199+
expect(ariaElement).toHaveStyle({display: "none"});
200+
});
201+
});
202+
203+
it("calls onChange when input value changes", async () => {
204+
const mockOnChange = jest.fn();
205+
render(<InputWithExamples {...defaultProps} onChange={mockOnChange} />);
206+
207+
const input = screen.getByRole("textbox");
208+
await userEvent.type(input, "123");
209+
210+
expect(mockOnChange).toHaveBeenCalled();
211+
});
212+
});

packages/perseus/src/widgets/numeric-input/input-with-examples.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,10 @@ const InputWithExamples = forwardRef<Focusable, Props>(
103103
const examplesAria = shouldShowExamples
104104
? `${props.examples[0]}
105105
${props.examples.slice(1).join(", or\n")}`
106-
.replaceAll("*", "")
107-
.replaceAll("$", "")
108-
.replaceAll("\\ \\text{pi}", " pi")
109-
.replaceAll("\\ ", " and ")
106+
.replace(/\*/g, "")
107+
.replace(/\$/g, "")
108+
.replace(/\\ \\text{pi}/g, " pi")
109+
.replace(/\\ /g, " and ")
110110
: "";
111111

112112
const inputProps = {

0 commit comments

Comments
 (0)