Test isWaiting() behavior using Completers to verify loading states work correctly
This skill tests isWaiting() behavior in Cubit methods.
Tests that:
isWaiting() returns true while operation is in progressisWaiting() returns false after operation completesUse Completer to control when async operations complete:
import 'dart:async';
import 'package:bloc_superpowers/bloc_superpowers.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
void main() {
late UserCubit cubit;
late MockApi mockApi;
setUp(() {
Superpowers.clear();
mockApi = MockApi();
cubit = UserCubit(api: mockApi);
});
tearDown(() => cubit.close());
test('isWaiting is true during operation', () async {
// Create a completer to control when the future completes
final completer = Completer<User>();
// Mock returns a future we control
when(() => mockApi.getUser()).thenAnswer((_) => completer.future);
// Start the operation
cubit.loadUser();
// Allow async to process
await Future.delayed(Duration.zero);
// Verify loading state is true
expect(Superpowers.isWaiting(UserCubit), isTrue);
// Complete the operation
completer.complete(User(name: 'John'));
// Allow async to process
await Future.delayed(Duration.zero);
// Verify loading state is false
expect(Superpowers.isWaiting(UserCubit), isFalse);
});
}
test('loading state pattern', () async {
final completer = Completer<Data>();
when(() => mockApi.getData()).thenAnswer((_) => completer.future);
// 1. Start operation
cubit.loadData();
await Future.delayed(Duration.zero);
// 2. Assert loading is true
expect(Superpowers.isWaiting(key), isTrue);
// 3. Complete operation
completer.complete(data);
await Future.delayed(Duration.zero);
// 4. Assert loading is false
expect(Superpowers.isWaiting(key), isFalse);
});
// Cubit uses: key: this (becomes UserCubit)
test('loading with type key', () async {
final completer = Completer<User>();
when(() => mockApi.getUser()).thenAnswer((_) => completer.future);
cubit.loadUser();
await Future.delayed(Duration.zero);
expect(Superpowers.isWaiting(UserCubit), isTrue);
completer.complete(User(name: 'John'));
await Future.delayed(Duration.zero);
expect(Superpowers.isWaiting(UserCubit), isFalse);
});
// Cubit uses: key: UserAction.load
test('loading with enum key', () async {
final completer = Completer<User>();
when(() => mockApi.getUser()).thenAnswer((_) => completer.future);
cubit.loadUser();
await Future.delayed(Duration.zero);
expect(Superpowers.isWaiting(UserAction.load), isTrue);
completer.complete(User(name: 'John'));
await Future.delayed(Duration.zero);
expect(Superpowers.isWaiting(UserAction.load), isFalse);
});
// Cubit uses: key: (DeleteItem, itemId)
test('loading with composite key', () async {
final completer = Completer<void>();
when(() => mockApi.deleteItem('123')).thenAnswer((_) => completer.future);
cubit.deleteItem('123');
await Future.delayed(Duration.zero);
// Only this specific item is loading
expect(Superpowers.isWaiting((DeleteItem, '123')), isTrue);
expect(Superpowers.isWaiting((DeleteItem, '456')), isFalse);
completer.complete();
await Future.delayed(Duration.zero);
expect(Superpowers.isWaiting((DeleteItem, '123')), isFalse);
});
test('multiple operations have independent loading states', () async {
final loadCompleter = Completer<User>();
final saveCompleter = Completer<void>();
when(() => mockApi.getUser()).thenAnswer((_) => loadCompleter.future);
when(() => mockApi.saveUser(any())).thenAnswer((_) => saveCompleter.future);
// Start both operations
cubit.loadUser();
cubit.saveUser(User(name: 'John'));
await Future.delayed(Duration.zero);
// Both are loading
expect(Superpowers.isWaiting(UserAction.load), isTrue);
expect(Superpowers.isWaiting(UserAction.save), isTrue);
// Complete load
loadCompleter.complete(User(name: 'John'));
await Future.delayed(Duration.zero);
// Load done, save still loading
expect(Superpowers.isWaiting(UserAction.load), isFalse);
expect(Superpowers.isWaiting(UserAction.save), isTrue);
// Complete save
saveCompleter.complete();
await Future.delayed(Duration.zero);
// Both done
expect(Superpowers.isWaiting(UserAction.load), isFalse);
expect(Superpowers.isWaiting(UserAction.save), isFalse);
});
test('loading state clears on error', () async {
when(() => mockApi.getUser()).thenThrow(Exception('Error'));
cubit.loadUser();
await Future.delayed(Duration.zero);
// Loading should be false (operation completed, with error)
expect(Superpowers.isWaiting(UserCubit), isFalse);
// Error state should be true
expect(Superpowers.isFailed(UserCubit), isTrue);
});
import 'dart:async';
import 'package:bloc_superpowers/bloc_superpowers.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
class MockApi extends Mock implements Api {}
void main() {
late ProductCubit cubit;
late MockApi mockApi;
setUp(() {
Superpowers.clear();
mockApi = MockApi();
cubit = ProductCubit(api: mockApi);
});
tearDown(() => cubit.close());
group('loading state', () {
test('isWaiting is true while loading products', () async {
final completer = Completer<List<Product>>();
when(() => mockApi.getProducts()).thenAnswer((_) => completer.future);
cubit.loadProducts();
await Future.delayed(Duration.zero);
expect(Superpowers.isWaiting(ProductCubit), isTrue);
completer.complete([Product(id: '1', name: 'Widget')]);
await Future.delayed(Duration.zero);
expect(Superpowers.isWaiting(ProductCubit), isFalse);
});
test('per-item loading for delete', () async {
final completer = Completer<void>();
when(() => mockApi.deleteProduct('1')).thenAnswer((_) => completer.future);
cubit.deleteProduct('1');
await Future.delayed(Duration.zero);
expect(Superpowers.isWaiting((DeleteProduct, '1')), isTrue);
expect(Superpowers.isWaiting((DeleteProduct, '2')), isFalse);
completer.complete();
await Future.delayed(Duration.zero);
expect(Superpowers.isWaiting((DeleteProduct, '1')), isFalse);
});
test('loading clears on error', () async {
when(() => mockApi.getProducts()).thenThrow(Exception('Error'));
cubit.loadProducts();
await Future.delayed(Duration.zero);
expect(Superpowers.isWaiting(ProductCubit), isFalse);
expect(Superpowers.isFailed(ProductCubit), isTrue);
});
});
}
Completer to control async timingFuture.delayed(Duration.zero) to let async processSuperpowers.isWaiting(key) static method in testsmix() callSuperpowers.clear() in setUpnpx skills add marcglasberg/test-loading-state下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer