Skip to content

Commit f072967

Browse files
committed
test: Add more unit test coverage
1 parent 41643f5 commit f072967

File tree

10 files changed

+351
-0
lines changed

10 files changed

+351
-0
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { excludeFalsy } from './excludeFalsy';
4+
5+
describe('excludeFalsy', () => {
6+
it('filters out falsy values while preserving truthy entries', () => {
7+
const items = [1, 0, 'hello', '', false, true, null, undefined, { foo: 'bar' }];
8+
const result = items.filter(excludeFalsy);
9+
10+
expect(result).toEqual([1, 'hello', true, { foo: 'bar' }]);
11+
});
12+
13+
it('acts as a type guard when filtering arrays', () => {
14+
const items: Array<string | undefined> = ['a', undefined, 'b'];
15+
const filtered = items.filter(excludeFalsy);
16+
17+
filtered.forEach(value => {
18+
expect(typeof value).toBe('string');
19+
});
20+
21+
expect(filtered).toEqual(['a', 'b']);
22+
});
23+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { excludePropertyFalsy } from './excludePropertyFalsy';
4+
5+
describe('excludePropertyFalsy', () => {
6+
it('filters out items where the property is undefined', () => {
7+
const items: Array<{ id?: number | null; name: string }> = [
8+
{ id: 1, name: 'first' },
9+
{ id: undefined, name: 'second' },
10+
{ name: 'third' },
11+
{ id: 0, name: 'fourth' },
12+
{ id: null, name: 'fifth' },
13+
];
14+
15+
const result = items.filter(excludePropertyFalsy('id'));
16+
17+
result.forEach(item => {
18+
const confirmedId: number | null = item.id;
19+
expect(confirmedId).not.toBeUndefined();
20+
});
21+
22+
expect(result).toEqual([
23+
{ id: 1, name: 'first' },
24+
{ id: 0, name: 'fourth' },
25+
{ id: null, name: 'fifth' },
26+
]);
27+
});
28+
29+
it('keeps falsy but defined values such as false', () => {
30+
const items: Array<{ active?: boolean; name: string }> = [
31+
{ active: false, name: 'inactive' },
32+
{ active: true, name: 'active' },
33+
{ name: 'unknown' },
34+
];
35+
36+
const result = items.filter(excludePropertyFalsy('active'));
37+
38+
expect(result).toEqual([
39+
{ active: false, name: 'inactive' },
40+
{ active: true, name: 'active' },
41+
]);
42+
});
43+
});

src/lib/arrays/findBy.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { findBy } from './findBy';
4+
5+
describe('findBy', () => {
6+
const data = [
7+
{ id: 1, name: 'alpha' },
8+
{ id: 2, name: 'beta' },
9+
{ id: 3, name: 'gamma' },
10+
];
11+
12+
it('returns the first element matching the field value', () => {
13+
expect(findBy(data, 'id', 2)).toEqual({ id: 2, name: 'beta' });
14+
});
15+
16+
it('returns undefined when no element matches', () => {
17+
expect(findBy(data, 'id', 99)).toBeUndefined();
18+
});
19+
20+
it('can search by string fields', () => {
21+
expect(findBy(data, 'name', 'gamma')).toEqual({ id: 3, name: 'gamma' });
22+
});
23+
});

src/lib/arrays/mapBy.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { mapBy } from './mapBy';
4+
5+
describe('mapBy', () => {
6+
it('maps values from the provided field in order', () => {
7+
const data = [
8+
{ id: 1, name: 'alpha' },
9+
{ id: 2, name: 'beta' },
10+
{ id: 3, name: 'gamma' },
11+
];
12+
13+
expect(mapBy(data, 'name')).toEqual(['alpha', 'beta', 'gamma']);
14+
});
15+
16+
it('returns an empty array when given an empty list', () => {
17+
expect(mapBy([], 'id')).toEqual([]);
18+
});
19+
20+
it('returns an empty array when provided a falsy array', () => {
21+
const result = mapBy(undefined as unknown as Array<{ id: number }>, 'id');
22+
23+
expect(result).toEqual([]);
24+
});
25+
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { transformNodes } from './transformNodes';
4+
5+
type Node = { id: string; children?: Node[] };
6+
7+
describe('transformNodes', () => {
8+
it('transforms nested nodes while passing parent chain', () => {
9+
const nodes: Node[] = [
10+
{
11+
id: 'root',
12+
children: [
13+
{ id: 'child-1' },
14+
{
15+
id: 'child-2',
16+
children: [{ id: 'grandchild-1' }],
17+
},
18+
],
19+
},
20+
];
21+
22+
const transformed = transformNodes(
23+
nodes,
24+
'children',
25+
(node, parents) => ({
26+
id: node.id,
27+
depth: parents.length,
28+
parentIds: parents.map(parent => parent.id),
29+
}),
30+
);
31+
32+
expect(transformed).toEqual([
33+
{
34+
id: 'root',
35+
depth: 0,
36+
parentIds: [],
37+
children: [
38+
{ id: 'child-1', depth: 1, parentIds: ['root'] },
39+
{
40+
id: 'child-2',
41+
depth: 1,
42+
parentIds: ['root'],
43+
children: [
44+
{ id: 'grandchild-1', depth: 2, parentIds: ['root', 'child-2'] },
45+
],
46+
},
47+
],
48+
},
49+
]);
50+
});
51+
52+
it('handles nodes without nested collections', () => {
53+
const flatNodes: Node[] = [{ id: 'a' }, { id: 'b' }];
54+
55+
const transformed = transformNodes(
56+
flatNodes,
57+
'children',
58+
(node, parents) => ({
59+
value: node.id,
60+
hasParents: parents.length > 0,
61+
}),
62+
);
63+
64+
expect(transformed).toEqual([
65+
{ value: 'a', hasParents: false },
66+
{ value: 'b', hasParents: false },
67+
]);
68+
});
69+
});

src/lib/countBy.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { countBy } from './countBy';
4+
5+
describe('countBy', () => {
6+
it('counts occurrences by a string property and skips undefined values', () => {
7+
const items = [
8+
{ id: 1, group: 'alpha' },
9+
{ id: 2, group: 'alpha' },
10+
{ id: 3, group: 'beta' },
11+
{ id: 4, group: undefined as string | undefined },
12+
];
13+
14+
const result = countBy(items, 'group');
15+
16+
expect(result).toEqual({
17+
alpha: 2,
18+
beta: 1,
19+
});
20+
});
21+
22+
it('increments counts for every value in an array property', () => {
23+
const items = [
24+
{ id: 1, tags: ['red', 'sweet'] },
25+
{ id: 2, tags: ['sweet'] },
26+
{ id: 3, tags: ['red', 'bitter'] },
27+
];
28+
29+
const result = countBy(items, 'tags');
30+
31+
expect(result).toEqual({
32+
red: 2,
33+
sweet: 2,
34+
bitter: 1,
35+
});
36+
});
37+
38+
it('coerces non-string property values to strings when counting', () => {
39+
const items = [
40+
{ id: 1, count: 1 },
41+
{ id: 2, count: 1 },
42+
{ id: 3, count: 2 },
43+
];
44+
45+
const result = countBy(items, 'count');
46+
47+
expect(result).toEqual({
48+
'1': 2,
49+
'2': 1,
50+
});
51+
});
52+
});

src/lib/keyBy.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { keyBy } from './keyBy';
4+
5+
describe('keyBy', () => {
6+
it('maps items by a string property and overwrites duplicate keys with the last item', () => {
7+
const items = [
8+
{ id: 1, slug: 'alpha' },
9+
{ id: 2, slug: 'beta' },
10+
{ id: 3, slug: 'alpha' },
11+
{ id: 4, slug: undefined as string | undefined },
12+
];
13+
14+
const result = keyBy(items, 'slug');
15+
16+
expect(result).toEqual({
17+
alpha: { id: 3, slug: 'alpha' },
18+
beta: { id: 2, slug: 'beta' },
19+
});
20+
});
21+
22+
it('handles array properties by assigning the item to each key value', () => {
23+
const items = [
24+
{ id: 1, tags: ['red', 'sweet'] },
25+
{ id: 2, tags: ['sweet', 'tart'] },
26+
];
27+
28+
const result = keyBy(items, 'tags');
29+
30+
expect(result).toEqual({
31+
red: { id: 1, tags: ['red', 'sweet'] },
32+
sweet: { id: 2, tags: ['sweet', 'tart'] },
33+
tart: { id: 2, tags: ['sweet', 'tart'] },
34+
});
35+
});
36+
37+
it('supports non-string property values by coercing them to string keys', () => {
38+
const items = [
39+
{ id: 1, count: 1 },
40+
{ id: 2, count: 10 },
41+
];
42+
43+
const result = keyBy(items, 'count');
44+
45+
expect(result).toEqual({
46+
'1': { id: 1, count: 1 },
47+
'10': { id: 2, count: 10 },
48+
});
49+
});
50+
});

src/lib/string/safeParse.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
3+
import { safeParse } from './safeParse';
4+
5+
describe('safeParse', () => {
6+
it('parses valid JSON strings', () => {
7+
const parsed = safeParse<{ id: number; name: string }>('{"id":1,"name":"test"}');
8+
9+
expect(parsed).toEqual({ id: 1, name: 'test' });
10+
});
11+
12+
it('does not check the schema', () => {
13+
const parsed = safeParse<{ id: number; name: string }>('{"id":"a STRING oh my goodness","name":"how SNEAKY!"}');
14+
15+
expect(parsed).toHaveProperty('id', expect.any(String));
16+
expect(parsed).toHaveProperty('name', expect.any(String));
17+
});
18+
19+
it('returns null for nullish sentinel values', () => {
20+
expect(safeParse('null')).toBeNull();
21+
expect(safeParse('undefined')).toBeNull();
22+
expect(safeParse(null)).toBeNull();
23+
expect(safeParse(undefined)).toBeNull();
24+
});
25+
26+
it('returns null and logs when parsing fails', () => {
27+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
28+
29+
const result = safeParse('not-json');
30+
31+
expect(result).toBeNull();
32+
expect(errorSpy).toHaveBeenCalledTimes(1);
33+
34+
const [message, err] = errorSpy.mock.calls[0];
35+
expect(message).toBe('safeParse failed to parse value, returning null instead');
36+
expect(err).toBeInstanceOf(Error);
37+
38+
errorSpy.mockRestore();
39+
});
40+
});

src/lib/string/safeParse.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
/**
2+
* Attempts to parse a string as JSON, and narrows the type down to the provided generic. Note that
3+
* no schema validation happens as a part of this, so the type might be inaccurate. But exceptions
4+
* won't be thrown by the actual parsing if invalid JSON is provided.
5+
* @param value
6+
*/
17
export function safeParse<T>(value: string | null | undefined): T | null {
28
try {
39
if (value === 'null' || value === 'undefined' || value === null || value === undefined) {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { itemKeyIsDefined } from './itemKeyIsDefined';
4+
5+
describe('itemKeyIsDefined', () => {
6+
it('returns true when property exists even if the value is falsy or null', () => {
7+
const item = { count: 0, active: false, label: '', data: null as null | string };
8+
9+
expect(itemKeyIsDefined(item, 'count')).toBe(true);
10+
expect(itemKeyIsDefined(item, 'active')).toBe(true);
11+
expect(itemKeyIsDefined(item, 'label')).toBe(true);
12+
expect(itemKeyIsDefined(item, 'data')).toBe(true);
13+
});
14+
15+
it('returns false when property value is undefined', () => {
16+
const item: { id: number; missing?: string } = { id: 1 };
17+
18+
expect(itemKeyIsDefined(item, 'missing')).toBe(false);
19+
});
20+
});

0 commit comments

Comments
 (0)