Skip to content

Commit 5134f36

Browse files
add test
1 parent 1b890d5 commit 5134f36

1 file changed

Lines changed: 85 additions & 0 deletions

File tree

examples/mobile/test/login_screen_test.dart

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1234,6 +1234,91 @@ void main() {
12341234
// ===== WEBSOCKET TESTS =====
12351235

12361236
group('WebSocket Events', () {
1237+
test(
1238+
'WS task_created DURING initial load does NOT create duplicate',
1239+
() async {
1240+
// BUG: WebSocket task_created arrives DURING initial HTTP load,
1241+
// BEFORE HTTP response. Then HTTP arrives and REPLACES the list,
1242+
// but HTTP data + WS data = duplicate!
1243+
//
1244+
// Timeline:
1245+
// 1. Login succeeds
1246+
// 2. Component starts GET /tasks (async)
1247+
// 3. WebSocket connects, server sends task_created event
1248+
// 4. WS handler adds task to state (currently empty)
1249+
// 5. HTTP /tasks responds with SAME task
1250+
// 6. _loadTasks does tasksState.set() - REPLACES state!
1251+
//
1252+
// Result: WS added task + HTTP replaced with same task = OK normally
1253+
// BUT if we don't deduplicate in set(), we can get race issues.
1254+
//
1255+
// Actually the REAL bug: WS arrives AFTER HTTP completes, but for
1256+
// a task that was ALREADY in the HTTP response. Let's test that.
1257+
1258+
Future<Result<JSObject, String>> racingFetch(
1259+
String url, {
1260+
String method = 'GET',
1261+
String? token,
1262+
Map<String, Object?>? body,
1263+
}) async {
1264+
if (url.contains('/auth/login')) {
1265+
return Success(
1266+
createJSObject({
1267+
'success': true,
1268+
'data': {
1269+
'token': 'tok',
1270+
'user': {'name': 'Alice'},
1271+
},
1272+
}),
1273+
);
1274+
}
1275+
if (url.contains('/tasks') && method == 'GET') {
1276+
// HTTP returns task, but ALSO WS will send same task later
1277+
return Success(
1278+
createJSObject({
1279+
'success': true,
1280+
'data': [
1281+
{'id': 'task_47', 'title': 'Existing Task', 'completed': false},
1282+
],
1283+
}),
1284+
);
1285+
}
1286+
throw StateError('No mock for $method $url');
1287+
}
1288+
1289+
final result = render(MobileApp(fetchFn: racingFetch));
1290+
1291+
// Login
1292+
final inputs = result.container.querySelectorAll('input');
1293+
await userType(inputs[0], 'a@b.com');
1294+
await userType(inputs[1], 'pass');
1295+
fireClick(result.container.querySelectorAll('button').first);
1296+
1297+
// Wait for task list to load
1298+
await waitForText(result, 'Existing Task');
1299+
1300+
// WS sends task_created for same task that's already loaded!
1301+
// This simulates server broadcasting to all clients
1302+
simulateWsMessage(
1303+
'{"type":"task_created","data":'
1304+
'{"id":"task_47","title":"Existing Task","completed":false}}',
1305+
);
1306+
1307+
await Future<void>.delayed(const Duration(milliseconds: 100));
1308+
1309+
// Count occurrences - must be exactly 1!
1310+
final textContent = result.container.textContent;
1311+
final matches = 'Existing Task'.allMatches(textContent).length;
1312+
expect(
1313+
matches,
1314+
1,
1315+
reason: 'WS task_created for existing task should NOT duplicate!',
1316+
);
1317+
1318+
result.unmount();
1319+
},
1320+
);
1321+
12371322
test('task_created event adds task to list', () async {
12381323
final mockFetch = createMockFetch({
12391324
'/auth/login': {

0 commit comments

Comments
 (0)