Skip to content

Commit b6b37ee

Browse files
committed
☕ Add tests for utils and fix it
- `regexIndexOf()` returns incorrect results when given a negative position. Negative values were never used, so they didn't cause problems.
1 parent 2139dc0 commit b6b37ee

File tree

2 files changed

+174
-1
lines changed

2 files changed

+174
-1
lines changed

scripts/utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ export async function downloadString(url: string): Promise<string> {
2121
*/
2222
export function regexIndexOf(s: string, pattern: RegExp, offset = 0): number {
2323
const index = s.slice(offset).search(pattern);
24-
return index < 0 ? index : index + offset;
24+
if (index < 0 || offset <= -s.length) {
25+
return index;
26+
} else {
27+
return index + offset + (offset < 0 ? s.length : 0);
28+
}
2529
}
2630

2731
/** Count the number of occurrences of a name. */

scripts/utils_test.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import {
2+
assertEquals,
3+
assertInstanceOf,
4+
assertRejects,
5+
} from "https://deno.land/std@0.183.0/testing/asserts.ts";
6+
import { stub } from "https://deno.land/std@0.183.0/testing/mock.ts";
7+
8+
import { Counter, downloadString, regexIndexOf } from "./utils.ts";
9+
10+
async function mockFetch({ body, init, fn }: {
11+
body?: BodyInit;
12+
init?: ResponseInit;
13+
fn: () => void | Promise<void>;
14+
}): Promise<void> {
15+
const fetchStub = stub(
16+
globalThis,
17+
"fetch",
18+
() => Promise.resolve(new Response(body, init)),
19+
);
20+
try {
21+
await fn();
22+
} finally {
23+
fetchStub.restore();
24+
}
25+
}
26+
27+
Deno.test(downloadString.name, async (t) => {
28+
await t.step("Returns body of response", async () => {
29+
const url = "https://example.net/foo.ts";
30+
const expected = "Fetched content\nFoo Bar\n\nBaz";
31+
await mockFetch({
32+
body: expected,
33+
async fn() {
34+
const actual = await downloadString(url);
35+
assertEquals(actual, expected);
36+
},
37+
});
38+
});
39+
40+
await t.step("Throws error if fetch fails", async () => {
41+
const url = "https://example.net/foo.ts";
42+
const expectedMessage = "Failed to read https://example.net/foo.ts";
43+
await mockFetch({
44+
init: { status: 404 },
45+
async fn() {
46+
await assertRejects(
47+
() => downloadString(url),
48+
Error,
49+
expectedMessage,
50+
);
51+
},
52+
});
53+
});
54+
});
55+
56+
Deno.test(regexIndexOf.name, async (t) => {
57+
// Returns position of first pattern match
58+
for (
59+
const { args, expected } of [
60+
{ args: ["foobarbazbaz", /foo/], expected: 0 },
61+
{ args: ["foobarbazbaz", /baz/], expected: 6 },
62+
// String contains new line
63+
{ args: ["foobar\nbazbaz", /baz/], expected: 7 },
64+
// Pattern contains new line
65+
{ args: ["foobar\nbazbaz", /\nbaz/], expected: 6 },
66+
] as Array<{
67+
args: Parameters<typeof regexIndexOf>;
68+
expected: ReturnType<typeof regexIndexOf>;
69+
}>
70+
) {
71+
await t.step(`Returns position of first pattern match: ${args}`, () => {
72+
const actual = regexIndexOf(...args);
73+
assertEquals(actual, expected);
74+
});
75+
}
76+
77+
// Search from specified position
78+
for (
79+
const { args, expected } of [
80+
{ args: ["foobarbazbaz", /baz/, 0], expected: 6 },
81+
{ args: ["foobarbazbaz", /baz/, 6], expected: 6 },
82+
{ args: ["foobarbazbaz", /baz/, 7], expected: 9 },
83+
// Minus offset
84+
{ args: ["foobarbazbaz", /baz/, -5], expected: 9 },
85+
// Minus offset less than string length
86+
{ args: ["foobarbazbaz", /baz/, -20], expected: 6 },
87+
] as Array<{
88+
args: Parameters<typeof regexIndexOf>;
89+
expected: ReturnType<typeof regexIndexOf>;
90+
}>
91+
) {
92+
await t.step(`Search from specified position: ${args}`, () => {
93+
const actual = regexIndexOf(...args);
94+
assertEquals(actual, expected);
95+
});
96+
}
97+
98+
// Returns -1 if pattern not match
99+
for (
100+
const { args } of [
101+
{ args: ["foobarbazbaz", /qux/] },
102+
{ args: ["foobar\nbazbaz", /barbaz/] },
103+
{ args: ["foobarbazbaz", /baz/, 10] },
104+
// Minus offset
105+
{ args: ["foobarbazbaz", /baz/, -2] },
106+
] as Array<{
107+
args: Parameters<typeof regexIndexOf>;
108+
}>
109+
) {
110+
await t.step(`Returns -1 if pattern not match: ${args}`, () => {
111+
const actual = regexIndexOf(...args);
112+
assertEquals(actual, -1);
113+
});
114+
}
115+
});
116+
117+
Deno.test(Counter.name, async (t) => {
118+
await t.step("constructor", () => {
119+
const actual = new Counter();
120+
assertInstanceOf(actual, Counter);
121+
});
122+
123+
await t.step(Counter.prototype.count.name, async (t) => {
124+
await t.step("Returns 1 if name specified first time", () => {
125+
const obj = new Counter();
126+
assertEquals(obj.count("foo"), 1);
127+
assertEquals(obj.count("bar"), 1);
128+
assertEquals(obj.count("baz"), 1);
129+
});
130+
131+
await t.step("Returns +N if name specified N times", () => {
132+
const obj = new Counter();
133+
assertEquals(obj.count("foo"), 1);
134+
assertEquals(obj.count("foo"), 2);
135+
assertEquals(obj.count("bar"), 1);
136+
assertEquals(obj.count("foo"), 3);
137+
assertEquals(obj.count("bar"), 2);
138+
assertEquals(obj.count("baz"), 1);
139+
});
140+
});
141+
142+
await t.step(Counter.prototype.get.name, async (t) => {
143+
await t.step(
144+
"Returns +N if count() is called N times with same name",
145+
() => {
146+
const obj = new Counter();
147+
obj.count("foo");
148+
obj.count("bar");
149+
obj.count("foo");
150+
obj.count("baz");
151+
obj.count("foo");
152+
obj.count("baz");
153+
assertEquals(obj.get("foo"), 3);
154+
assertEquals(obj.get("bar"), 1);
155+
assertEquals(obj.get("baz"), 2);
156+
},
157+
);
158+
159+
await t.step(
160+
"Returns 0 if count() is not called with same name",
161+
() => {
162+
const obj = new Counter();
163+
assertEquals(obj.get("foo"), 0);
164+
assertEquals(obj.get("bar"), 0);
165+
assertEquals(obj.get("baz"), 0);
166+
},
167+
);
168+
});
169+
});

0 commit comments

Comments
 (0)