Skip to content

Commit 12e1500

Browse files
authored
fix: fix error handling in parseWithSchema (@fehmer) (monkeytypegame#6229)
`instanceof ZodError` is not working if the code is packaged as a module for unknown reason. Found while adding tests for the url-handler in monkeytypegame#6207.
1 parent 3719ac0 commit 12e1500

File tree

4 files changed

+287
-6
lines changed

4 files changed

+287
-6
lines changed
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import { Difficulty, Mode, Mode2 } from "@monkeytype/contracts/schemas/shared";
2+
import { compressToURI } from "lz-ts";
3+
import * as UpdateConfig from "../../src/ts/config";
4+
import * as Notifications from "../../src/ts/elements/notifications";
5+
import { CustomTextSettings } from "../../src/ts/test/custom-text";
6+
import * as TestLogic from "../../src/ts/test/test-logic";
7+
import * as TestState from "../../src/ts/test/test-state";
8+
import * as Misc from "../../src/ts/utils/misc";
9+
import { loadTestSettingsFromUrl } from "../../src/ts/utils/url-handler";
10+
11+
//mock modules to avoid dependencies
12+
vi.mock("../../src/ts/test/test-logic", () => ({
13+
restart: vi.fn(),
14+
}));
15+
16+
describe("url-handler", () => {
17+
describe("loadTestSettingsFromUrl", () => {
18+
const findGetParameterMock = vi.spyOn(Misc, "findGetParameter");
19+
20+
const setModeMock = vi.spyOn(UpdateConfig, "setMode");
21+
const setTimeConfigMock = vi.spyOn(UpdateConfig, "setTimeConfig");
22+
const setWordCountMock = vi.spyOn(UpdateConfig, "setWordCount");
23+
const setQuoteLengthMock = vi.spyOn(UpdateConfig, "setQuoteLength");
24+
const setSelectedQuoteIdMock = vi.spyOn(TestState, "setSelectedQuoteId");
25+
const setPunctuationMock = vi.spyOn(UpdateConfig, "setPunctuation");
26+
const setNumbersMock = vi.spyOn(UpdateConfig, "setNumbers");
27+
const setLanguageMock = vi.spyOn(UpdateConfig, "setLanguage");
28+
const setDifficultyMock = vi.spyOn(UpdateConfig, "setDifficulty");
29+
const setFunboxMock = vi.spyOn(UpdateConfig, "setFunbox");
30+
31+
const restartTestMock = vi.spyOn(TestLogic, "restart");
32+
const addNotificationMock = vi.spyOn(Notifications, "add");
33+
34+
beforeEach(() => {
35+
[
36+
findGetParameterMock,
37+
setModeMock,
38+
setTimeConfigMock,
39+
setWordCountMock,
40+
setQuoteLengthMock,
41+
setSelectedQuoteIdMock,
42+
setPunctuationMock,
43+
setNumbersMock,
44+
setLanguageMock,
45+
setDifficultyMock,
46+
setFunboxMock,
47+
restartTestMock,
48+
addNotificationMock,
49+
].forEach((it) => it.mockReset());
50+
51+
findGetParameterMock.mockImplementation((override) => override);
52+
});
53+
afterEach(() => {});
54+
55+
it("handles null", () => {
56+
//GIVEN
57+
findGetParameterMock.mockReturnValue("null");
58+
59+
//WHEN
60+
loadTestSettingsFromUrl("");
61+
62+
//THEN
63+
expect(setModeMock).not.toHaveBeenCalled();
64+
});
65+
it("handles mode2 as number", () => {
66+
//GIVEN
67+
findGetParameterMock.mockReturnValue(
68+
urlData({ mode: "time", mode2: 60 })
69+
);
70+
71+
//WHEN
72+
loadTestSettingsFromUrl("");
73+
74+
//THEN
75+
expect(setModeMock).toHaveBeenCalledWith("time", true);
76+
expect(setTimeConfigMock).toHaveBeenCalledWith(60, true);
77+
expect(restartTestMock).toHaveBeenCalled();
78+
});
79+
it("sets time", () => {
80+
//GIVEN
81+
findGetParameterMock.mockReturnValue(
82+
urlData({ mode: "time", mode2: "30" })
83+
);
84+
85+
//WHEN
86+
loadTestSettingsFromUrl("");
87+
88+
//THEN
89+
expect(setModeMock).toHaveBeenCalledWith("time", true);
90+
expect(setTimeConfigMock).toHaveBeenCalledWith(30, true);
91+
expect(restartTestMock).toHaveBeenCalled();
92+
});
93+
it("sets word count", () => {
94+
//GIVEN
95+
findGetParameterMock.mockReturnValue(
96+
urlData({ mode: "words", mode2: "50" })
97+
);
98+
99+
//WHEN
100+
loadTestSettingsFromUrl("");
101+
102+
//THEN
103+
expect(setModeMock).toHaveBeenCalledWith("words", true);
104+
expect(setWordCountMock).toHaveBeenCalledWith(50, true);
105+
expect(restartTestMock).toHaveBeenCalled();
106+
});
107+
it("sets quote length", () => {
108+
//GIVEN
109+
findGetParameterMock.mockReturnValue(
110+
urlData({ mode: "quote", mode2: "512" })
111+
);
112+
113+
//WHEN
114+
loadTestSettingsFromUrl("");
115+
116+
//THEN
117+
expect(setModeMock).toHaveBeenCalledWith("quote", true);
118+
expect(setQuoteLengthMock).toHaveBeenCalledWith(-2, false);
119+
expect(setSelectedQuoteIdMock).toHaveBeenCalledWith(512);
120+
expect(restartTestMock).toHaveBeenCalled();
121+
});
122+
it("sets punctuation", () => {
123+
//GIVEN
124+
findGetParameterMock.mockReturnValue(urlData({ punctuation: true }));
125+
126+
//WHEN
127+
loadTestSettingsFromUrl("");
128+
129+
//THEN
130+
expect(setPunctuationMock).toHaveBeenCalledWith(true, true);
131+
expect(restartTestMock).toHaveBeenCalled();
132+
});
133+
it("sets numbers", () => {
134+
//GIVEN
135+
findGetParameterMock.mockReturnValue(urlData({ numbers: false }));
136+
137+
//WHEN
138+
loadTestSettingsFromUrl("");
139+
140+
//THEN
141+
expect(setNumbersMock).toHaveBeenCalledWith(false, true);
142+
expect(restartTestMock).toHaveBeenCalled();
143+
});
144+
it("sets language", () => {
145+
//GIVEN
146+
findGetParameterMock.mockReturnValue(urlData({ language: "english" }));
147+
148+
//WHEN
149+
loadTestSettingsFromUrl("");
150+
151+
//THEN
152+
expect(setLanguageMock).toHaveBeenCalledWith("english", true);
153+
expect(restartTestMock).toHaveBeenCalled();
154+
});
155+
it("sets difficulty", () => {
156+
//GIVEN
157+
findGetParameterMock.mockReturnValue(urlData({ difficulty: "master" }));
158+
159+
//WHEN
160+
loadTestSettingsFromUrl("");
161+
162+
//THEN
163+
expect(setDifficultyMock).toHaveBeenCalledWith("master", true);
164+
expect(restartTestMock).toHaveBeenCalled();
165+
});
166+
it("sets funbox", () => {
167+
//GIVEN
168+
findGetParameterMock.mockReturnValue(
169+
urlData({ funbox: "crt#choo_choo" })
170+
);
171+
172+
//WHEN
173+
loadTestSettingsFromUrl("");
174+
175+
//THEN
176+
expect(setFunboxMock).toHaveBeenCalledWith("crt#choo_choo", true);
177+
expect(restartTestMock).toHaveBeenCalled();
178+
});
179+
it("adds notification", () => {
180+
//GIVEN
181+
findGetParameterMock.mockReturnValue(
182+
urlData({
183+
mode: "time",
184+
mode2: "60",
185+
customText: {
186+
text: ["abcabc"],
187+
limit: { value: 5, mode: "time" },
188+
mode: "random",
189+
pipeDelimiter: true,
190+
},
191+
punctuation: true,
192+
numbers: true,
193+
language: "english",
194+
difficulty: "master",
195+
funbox: "a#b",
196+
})
197+
);
198+
199+
//WHEN
200+
loadTestSettingsFromUrl("");
201+
202+
//THEN
203+
expect(addNotificationMock).toHaveBeenCalledWith(
204+
"Settings applied from URL:<br><br>mode: time<br>mode2: 60<br>custom text settings<br>punctuation: on<br>numbers: on<br>language: english<br>difficulty: master<br>funbox: a#b<br>",
205+
1,
206+
{
207+
duration: 10,
208+
allowHTML: true,
209+
}
210+
);
211+
});
212+
it("rejects invalid values", () => {
213+
//GIVEN
214+
findGetParameterMock.mockReturnValue(
215+
urlData({
216+
mode: "invalidMode",
217+
mode2: "invalidMode2",
218+
customText: {
219+
text: "invalid",
220+
limit: "invalid",
221+
mode: "invalid",
222+
pipeDelimiter: "invalid",
223+
},
224+
punctuation: "invalid",
225+
numbers: "invalid",
226+
language: "invalid",
227+
difficulty: "invalid",
228+
funbox: ["invalid"],
229+
} as any)
230+
);
231+
232+
//WHEN
233+
loadTestSettingsFromUrl("");
234+
235+
//THEN
236+
expect(addNotificationMock).toHaveBeenCalledWith(
237+
`Failed to load test settings from URL: \"0\" Invalid enum value. Expected 'time' | 'words' | 'quote' | 'custom' | 'zen', received 'invalidMode'
238+
\"1\" Needs to be a number or a number represented as a string e.g. \"10\".
239+
\"2.text\" Expected array, received string
240+
\"2.mode\" Invalid enum value. Expected 'repeat' | 'random' | 'shuffle', received 'invalid'
241+
\"2.limit\" Expected object, received string
242+
\"2.pipeDelimiter\" Expected boolean, received string
243+
\"3\" Expected boolean, received string
244+
\"4\" Expected boolean, received string
245+
\"6\" Invalid enum value. Expected 'normal' | 'expert' | 'master', received 'invalid'
246+
\"7\" Expected string, received array`,
247+
0
248+
);
249+
});
250+
});
251+
});
252+
253+
const urlData = (
254+
data: Partial<{
255+
mode: Mode | undefined;
256+
mode2: Mode2<any> | number;
257+
customText: CustomTextSettings;
258+
punctuation: boolean;
259+
numbers: boolean;
260+
language: string;
261+
difficulty: Difficulty;
262+
funbox: string;
263+
}>
264+
): string => {
265+
return compressToURI(
266+
JSON.stringify([
267+
data.mode ?? null,
268+
data.mode2 ?? null,
269+
data.customText ?? null,
270+
data.punctuation ?? null,
271+
data.numbers ?? null,
272+
data.language ?? null,
273+
data.difficulty ?? null,
274+
data.funbox ?? null,
275+
])
276+
);
277+
};

frontend/src/ts/test/custom-text.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const CustomTextSettingsSchema = z.object({
3636
pipeDelimiter: z.boolean(),
3737
});
3838

39-
type CustomTextSettings = z.infer<typeof CustomTextSettingsSchema>;
39+
export type CustomTextSettings = z.infer<typeof CustomTextSettingsSchema>;
4040

4141
type CustomTextLimit = z.infer<typeof CustomTextSettingsSchema>["limit"];
4242

frontend/src/ts/utils/url-handler.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,13 @@ export function loadTestSettingsFromUrl(getOverride?: string): void {
169169
applied["mode"] = de[0];
170170
}
171171

172+
const mode = de[0] ?? Config.mode;
172173
if (de[1] !== null) {
173-
if (Config.mode === "time") {
174+
if (mode === "time") {
174175
UpdateConfig.setTimeConfig(parseInt(de[1], 10), true);
175-
} else if (Config.mode === "words") {
176+
} else if (mode === "words") {
176177
UpdateConfig.setWordCount(parseInt(de[1], 10), true);
177-
} else if (Config.mode === "quote") {
178+
} else if (mode === "quote") {
178179
UpdateConfig.setQuoteLength(-2, false);
179180
TestState.setSelectedQuoteId(parseInt(de[1], 10));
180181
ManualRestart.set();

packages/util/src/json.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ export function parseWithSchema<T extends z.ZodTypeAny>(
1616
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
1717
return schema.parse(jsonParsed) as z.infer<T>;
1818
} catch (error) {
19-
if (error instanceof ZodError) {
20-
throw new Error(error.issues.map(prettyErrorMessage).join("\n"));
19+
// instanceof ZodError is not working from our module
20+
if ((error as ZodError)["issues"] !== undefined) {
21+
throw new Error(
22+
(error as ZodError).issues.map(prettyErrorMessage).join("\n")
23+
);
2124
} else {
2225
throw error;
2326
}

0 commit comments

Comments
 (0)