-
-
Notifications
You must be signed in to change notification settings - Fork 178
/
main.spec.tsx
129 lines (115 loc) · 4 KB
/
main.spec.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import fc from 'fast-check';
import React from 'react';
import TodoList from './src/TodoList';
import { render, act, cleanup } from '@testing-library/react';
import { AddItemCommand } from './model-based/AddItemCommand';
import { ToggleItemCommand } from './model-based/ToggleItemCommand';
import { RemoveItemCommand } from './model-based/RemoveItemCommand';
import { listTodos, sortTodos } from './model-based/Model';
describe('TodoList', () => {
it('should detect potential issues with the TodoList', async () => {
await fc.assert(
fc
.asyncProperty(
fc.scheduler(),
TodoListCommands,
fc.uniqueArray(
fc.record({ id: fc.hexaString({ minLength: 8, maxLength: 8 }), label: fc.string(), checked: fc.boolean() }),
{ selector: (entry) => entry.id },
),
fc.infiniteStream(fc.boolean()),
async (s, commands, initialTodos, allFailures) => {
const { mockedApi, expectedTodos } = mockApi(s, initialTodos, allFailures);
// Execute all the commands
const wrapper = render(<TodoList {...mockedApi} />);
await fc.scheduledModelRun(s, () => ({ model: { todos: [], wrapper }, real: {} }), commands);
// Check the final state (no more items should be loading)
expect(
sortTodos((await listTodos()).map((t) => ({ label: t.label, checked: t.checked, loading: t.loading }))),
).toEqual(sortTodos(expectedTodos().map((t) => ({ label: t.label, checked: t.checked, loading: false }))));
},
)
.beforeEach(async () => {
await cleanup();
}),
);
});
});
// Helpers
const TodoListCommands = fc.commands([
fc.string().map((label) => new AddItemCommand(label)),
fc.nat().map((pos) => new ToggleItemCommand(pos)),
fc.nat().map((pos) => new RemoveItemCommand(pos)),
]);
type ApiTodoItem = { id: string; label: string; checked: boolean };
const mockApi = (s: fc.Scheduler, initialTodos: ApiTodoItem[], allFailures: fc.Stream<boolean>) => {
let lastIdx = 0;
let allTodos = [...initialTodos];
const fetchAllTodos = s.scheduleFunction(async function fetchAllTodos(): Promise<{
status: 'success';
response: ApiTodoItem[];
}> {
return { status: 'success', response: allTodos.slice() };
}, act);
const addTodo = s.scheduleFunction(async function addTodo(label: string): Promise<
| {
status: 'success';
response: ApiTodoItem;
}
| { status: 'error' }
> {
const newTodo = {
id: `${Math.random().toString(16).substring(2)}-${++lastIdx}`,
label,
checked: false,
};
if (allFailures.next().value) {
return { status: 'error' };
}
allTodos.push(newTodo);
return { status: 'success', response: newTodo };
}, act);
const toggleTodo = s.scheduleFunction(async function toggleTodo(id: string): Promise<
| {
status: 'success';
response: ApiTodoItem;
}
| { status: 'error' }
> {
const foundTodo = allTodos.find((t) => t.id === id);
if (!foundTodo || allFailures.next().value) {
return { status: 'error' };
}
allTodos = allTodos.map((t) => {
if (t.id !== id) return t;
return { id, label: t.label, checked: !t.checked };
});
return { status: 'success', response: { ...foundTodo, checked: !foundTodo.checked } };
}, act);
const removeTodo = s.scheduleFunction(async function removeTodo(id: string): Promise<
| {
status: 'success';
response: ApiTodoItem;
}
| { status: 'error' }
> {
const foundTodo = allTodos.find((t) => t.id === id);
if (!foundTodo || allFailures.next().value) {
return { status: 'error' };
}
allTodos = allTodos.filter((t) => {
if (t.id !== id) return true;
return false;
});
return { status: 'success', response: foundTodo };
}, act);
return {
mockedApi: {
fetchAllTodos,
addTodo,
toggleTodo,
removeTodo,
},
expectedTodos: () => allTodos.slice(),
};
};