Skip to content

Commit 154ee99

Browse files
authored
Answerless Table (#2396)
## Summary: I probably could have kept cleaning this widget up, but... - Table is `hidden` - This display name for Table is: "Table (deprecated - use markdown table instead)" - I did more than what I needed for SSS The only thing it was really using `PerseusTableWidgetOptions.answers` for was to determine the number of rows/columns, but `PerseusTableWidgetOptions` has `rows: number` and `columns: number` so I just used those. Before switching things over however, I wrote some unit tests for current behavior and then made sure those tests passed with answerless data. Issue: LEMS-2982 Author: handeyeco Reviewers: benchristel, handeyeco, Myranae, jeremywiebe Required Reviewers: Approved By: benchristel Checks: ✅ 8 checks were successful Pull Request URL: #2396
1 parent e47a222 commit 154ee99

File tree

8 files changed

+489
-109
lines changed

8 files changed

+489
-109
lines changed

.changeset/khaki-bikes-care.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/perseus": patch
3+
---
4+
5+
Update the Table widget to allow for rendering answerless data

packages/perseus/src/util.test.ts

+15
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,18 @@ describe("firstNumericalParse", () => {
55
expect(Util.firstNumericalParse("6/8")).toBe(0.75);
66
});
77
});
8+
9+
describe("stringArrayOfSize", () => {
10+
it("makes an array of strings", () => {
11+
expect(Util.stringArrayOfSize(2)).toEqual(["", ""]);
12+
});
13+
});
14+
15+
describe("stringArrayOfSize2D", () => {
16+
it("makes a 2D array of strings", () => {
17+
expect(Util.stringArrayOfSize2D({rows: 2, columns: 4})).toEqual([
18+
["", "", "", ""],
19+
["", "", "", ""],
20+
]);
21+
});
22+
});

packages/perseus/src/util.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,16 @@ function firstNumericalParse(text: string): ParsedValue | null | undefined {
146146
}
147147

148148
function stringArrayOfSize(size: number): ReadonlyArray<string> {
149-
return _(size).times(function () {
150-
return "";
151-
});
149+
return Array(size).fill("");
150+
}
151+
152+
function stringArrayOfSize2D(opt: {
153+
rows: number;
154+
columns: number;
155+
}): ReadonlyArray<ReadonlyArray<string>> {
156+
const {rows, columns} = opt;
157+
const rowArr = stringArrayOfSize(rows);
158+
return rowArr.map(() => stringArrayOfSize(columns));
152159
}
153160

154161
/**
@@ -593,6 +600,7 @@ const Util = {
593600
split,
594601
firstNumericalParse,
595602
stringArrayOfSize,
603+
stringArrayOfSize2D,
596604
gridDimensionConfig,
597605
getGridStep,
598606
snapStepFromGridStep,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`table answerful vs answerless answerful: snapshots 1`] = `
4+
<div>
5+
<div
6+
class="perseus-renderer perseus-renderer-responsive"
7+
>
8+
<div
9+
class="paragraph"
10+
data-perseus-paragraph-index="0"
11+
>
12+
<div
13+
class="paragraph"
14+
>
15+
The answer is 42
16+
17+
<div
18+
class="perseus-widget-container widget-nohighlight widget-block"
19+
>
20+
<table
21+
class="perseus-widget-table-of-values non-markdown"
22+
>
23+
<thead>
24+
<tr>
25+
<th>
26+
<div
27+
class="perseus-renderer perseus-renderer-responsive"
28+
>
29+
<div
30+
class="paragraph"
31+
data-perseus-paragraph-index="0"
32+
>
33+
<div
34+
class="paragraph"
35+
>
36+
Column 1
37+
</div>
38+
</div>
39+
</div>
40+
</th>
41+
<th>
42+
<div
43+
class="perseus-renderer perseus-renderer-responsive"
44+
>
45+
<div
46+
class="paragraph"
47+
data-perseus-paragraph-index="0"
48+
>
49+
<div
50+
class="paragraph"
51+
>
52+
Column 2
53+
</div>
54+
</div>
55+
</div>
56+
</th>
57+
</tr>
58+
</thead>
59+
<tbody>
60+
<tr>
61+
<td>
62+
<input
63+
type="text"
64+
value=""
65+
/>
66+
</td>
67+
<td>
68+
<input
69+
type="text"
70+
value=""
71+
/>
72+
</td>
73+
</tr>
74+
<tr>
75+
<td>
76+
<input
77+
type="text"
78+
value=""
79+
/>
80+
</td>
81+
<td>
82+
<input
83+
type="text"
84+
value=""
85+
/>
86+
</td>
87+
</tr>
88+
</tbody>
89+
</table>
90+
</div>
91+
</div>
92+
</div>
93+
</div>
94+
</div>
95+
`;
96+
97+
exports[`table answerful vs answerless answerless: snapshots 1`] = `
98+
<div>
99+
<div
100+
class="perseus-renderer perseus-renderer-responsive"
101+
>
102+
<div
103+
class="paragraph"
104+
data-perseus-paragraph-index="0"
105+
>
106+
<div
107+
class="paragraph"
108+
>
109+
The answer is 42
110+
111+
<div
112+
class="perseus-widget-container widget-nohighlight widget-block"
113+
>
114+
<table
115+
class="perseus-widget-table-of-values non-markdown"
116+
>
117+
<thead>
118+
<tr>
119+
<th>
120+
<div
121+
class="perseus-renderer perseus-renderer-responsive"
122+
>
123+
<div
124+
class="paragraph"
125+
data-perseus-paragraph-index="0"
126+
>
127+
<div
128+
class="paragraph"
129+
>
130+
Column 1
131+
</div>
132+
</div>
133+
</div>
134+
</th>
135+
<th>
136+
<div
137+
class="perseus-renderer perseus-renderer-responsive"
138+
>
139+
<div
140+
class="paragraph"
141+
data-perseus-paragraph-index="0"
142+
>
143+
<div
144+
class="paragraph"
145+
>
146+
Column 2
147+
</div>
148+
</div>
149+
</div>
150+
</th>
151+
</tr>
152+
</thead>
153+
<tbody>
154+
<tr>
155+
<td>
156+
<input
157+
type="text"
158+
value=""
159+
/>
160+
</td>
161+
<td>
162+
<input
163+
type="text"
164+
value=""
165+
/>
166+
</td>
167+
</tr>
168+
<tr>
169+
<td>
170+
<input
171+
type="text"
172+
value=""
173+
/>
174+
</td>
175+
<td>
176+
<input
177+
type="text"
178+
value=""
179+
/>
180+
</td>
181+
</tr>
182+
</tbody>
183+
</table>
184+
</div>
185+
</div>
186+
</div>
187+
</div>
188+
</div>
189+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {ServerItemRendererWithDebugUI} from "../../../../../testing/server-item-renderer-with-debug-ui";
2+
import {generateTestPerseusItem} from "../../util/test-utils";
3+
4+
import {generateTableRenderer} from "./test-util";
5+
6+
import type {PerseusItem} from "@khanacademy/perseus-core";
7+
import type {Meta, StoryObj} from "@storybook/react";
8+
9+
const meta: Meta = {
10+
title: "Perseus/Widgets/Table",
11+
component: ServerItemRendererWithDebugUI,
12+
};
13+
export default meta;
14+
15+
type Story = StoryObj<typeof ServerItemRendererWithDebugUI>;
16+
17+
const tableItem: PerseusItem = generateTestPerseusItem({
18+
question: generateTableRenderer(),
19+
});
20+
21+
export const AnswerfulTable: Story = {
22+
args: {
23+
item: tableItem,
24+
},
25+
};
26+
27+
export const AnswerlessTable: Story = {
28+
args: {
29+
item: tableItem,
30+
startAnswerless: true,
31+
},
32+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import {splitPerseusItem, type PerseusItem} from "@khanacademy/perseus-core";
2+
import {scorePerseusItem} from "@khanacademy/perseus-score";
3+
import {screen} from "@testing-library/react";
4+
import {userEvent as userEventLib} from "@testing-library/user-event";
5+
6+
import {generateTestPerseusItem} from "../../util/test-utils";
7+
import {renderQuestion} from "../__testutils__/renderQuestion";
8+
9+
import {generateTableRenderer} from "./test-util";
10+
11+
import type {UserEvent} from "@testing-library/user-event";
12+
13+
function getFullItem(): PerseusItem {
14+
return generateTestPerseusItem({question: generateTableRenderer()});
15+
}
16+
17+
function getSplitItem(): PerseusItem {
18+
const item = getFullItem();
19+
item.question = splitPerseusItem(item.question);
20+
return item;
21+
}
22+
23+
describe("table", () => {
24+
let userEvent: UserEvent;
25+
beforeEach(() => {
26+
userEvent = userEventLib.setup({
27+
advanceTimers: jest.advanceTimersByTime,
28+
});
29+
});
30+
31+
describe.each([
32+
{optionsMode: "answerful", renderItem: getFullItem()},
33+
{optionsMode: "answerless", renderItem: getSplitItem()},
34+
])("answerful vs answerless", ({optionsMode, renderItem}) => {
35+
it(`${optionsMode}: renders`, () => {
36+
renderQuestion(renderItem.question);
37+
38+
expect(screen.getByText("Column 1")).toBeInTheDocument();
39+
expect(screen.getByText("Column 2")).toBeInTheDocument();
40+
});
41+
42+
it(`${optionsMode}: snapshots`, () => {
43+
const {container} = renderQuestion(renderItem.question);
44+
45+
expect(container).toMatchSnapshot();
46+
});
47+
48+
it(`${optionsMode}: can be answered`, async () => {
49+
const {renderer} = renderQuestion(renderItem.question);
50+
51+
const inputs = screen.getAllByRole("textbox");
52+
for (let i = 0; i < 4; i++) {
53+
await userEvent.type(inputs[i], "8675309");
54+
}
55+
56+
expect(renderer.getUserInputMap()).toEqual({
57+
"table 1": [
58+
["8675309", "8675309"],
59+
["8675309", "8675309"],
60+
],
61+
});
62+
});
63+
64+
it(`${optionsMode}: can be scored`, async () => {
65+
const {renderer} = renderQuestion(renderItem.question);
66+
67+
const inputs = screen.getAllByRole("textbox");
68+
for (let i = 0; i < 4; i++) {
69+
await userEvent.type(inputs[i], "42");
70+
}
71+
72+
const userInput = renderer.getUserInputMap();
73+
const answerful = generateTableRenderer();
74+
const score = scorePerseusItem(answerful, userInput, "en");
75+
76+
expect(score).toHaveBeenAnsweredCorrectly();
77+
});
78+
79+
it(`${optionsMode}: returns user input in correct order`, async () => {
80+
const {renderer} = renderQuestion(renderItem.question);
81+
82+
const inputs = screen.getAllByRole("textbox");
83+
for (let i = 0; i < 4; i++) {
84+
await userEvent.type(inputs[i], `${i}`);
85+
}
86+
87+
const userInput = renderer.getUserInputMap();
88+
expect(userInput).toEqual({
89+
"table 1": [
90+
["0", "1"],
91+
["2", "3"],
92+
],
93+
});
94+
});
95+
});
96+
});

0 commit comments

Comments
 (0)