bottleneckcrm/mobile/test/unit/web_forms_provider_test.dart
Ashwin e26ab380fc Add unit tests for ticket board provider, status settable, web forms, CSV import, deal aging badge, and tickets selection
- Implement comprehensive tests for the ticket board provider to validate kanban and pipeline modes, including ticket movement and error handling.
- Create tests for ticket status settable to ensure proper status selection logic.
- Enhance web form model tests to cover ticket-specific fields and submission behavior.
- Introduce CSV import sheet tests to verify file handling and error reporting.
- Add deal aging badge tests to confirm correct display of aging statuses.
- Implement tickets selection tests to ensure UI responsiveness and functionality at various screen sizes.
2026-09-26 16:53:22 +05:30

181 lines
6 KiB
Dart

import 'dart:convert';
import 'package:bottle_crm/data/models/web_form.dart';
import 'package:bottle_crm/providers/web_forms_provider.dart';
import 'package:bottle_crm/services/api_service.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
class _FakeClient extends http.BaseClient {
int status = 200;
String body = '{}';
final List<http.BaseRequest> sent = [];
final List<String> bodies = [];
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
sent.add(request);
final bytes = await request.finalize().toBytes();
bodies.add(utf8.decode(bytes));
return http.StreamedResponse(
Stream.value(utf8.encode(body)),
status,
request: request,
);
}
}
void main() {
group('WebFormsNotifier', () {
late ProviderContainer container;
late _FakeClient client;
setUp(() {
client = _FakeClient();
ApiService().setClientForTesting(client);
container = ProviderContainer();
});
tearDown(() => container.dispose());
Future<WebFormsState> readForms() {
container.listen(webFormsProvider, (_, _) {});
return container.read(webFormsProvider.future);
}
test(
'reads the totals the server computed, not the page it returned',
() async {
// The page holds one row while the org holds thirteen forms. Counting
// `results` would report "1 published of 1" on a settings screen.
client.body = '''
{"count": 13,
"results": [
{"id": "f1", "name": "Contact us", "is_published": true,
"submission_count": 4, "field_count": 3}
],
"totals": {"count": 13, "published": 5, "submissions_30d": 40,
"spam_30d": 7}}
''';
final state = await readForms();
expect(state.forms.single.name, 'Contact us');
expect(state.totals.count, 13);
expect(state.totals.published, 5);
expect(state.totals.drafts, 8);
expect(state.totals.submissions30d, 40);
expect(state.totals.spam30d, 7);
expect(state.truncated, isTrue);
},
);
test('is not truncated when the page holds everything', () async {
client.body =
'{"count": 1, "results": [{"id": "f1", "name": "One"}], '
'"totals": {"count": 1, "published": 0}}';
final state = await readForms();
expect(state.truncated, isFalse);
});
test('survives a response with no totals block', () async {
client.body = '{"count": 0, "results": []}';
final state = await readForms();
expect(state.totals.count, 0);
expect(state.totals.drafts, 0);
});
test(
'a refused publish returns the server message rather than swallowing it',
() async {
client.body = '{"count": 0, "results": [], "totals": {}}';
await readForms();
client.status = 400;
client.body =
'{"error": true, '
'"errors": "Add an email field before publishing."}';
final message = await container
.read(webFormsProvider.notifier)
.publish('f1');
// The instruction IS the response. A generic "could not publish" would
// throw away the only thing the person needs to know.
expect(message, contains('Add an email field'));
},
);
test('a refused write does not flip local state', () async {
client.body = '''
{"count": 1,
"results": [{"id": "f1", "name": "Contact us", "is_published": false}],
"totals": {"count": 1, "published": 0}}
''';
final before = await readForms();
expect(before.forms.single.isPublished, isFalse);
client.status = 403;
client.body = '{"error": true, "errors": "Admin access required"}';
await container.read(webFormsProvider.notifier).publish('f1');
// Nothing optimistic: a form shown as live when the server refused is an
// admin believing an endpoint is open when it is closed, or the reverse.
final after = await container.read(webFormsProvider.future);
expect(after.forms.single.isPublished, isFalse);
});
test(
'the update sends PUT and omits the secret when none was typed',
() async {
client.body = '{"count": 0, "results": [], "totals": {}}';
await readForms();
final form = WebForm.fromJson({
'id': 'f1',
'name': 'Contact us',
'captcha_provider': 'turnstile',
'has_captcha_secret': true,
});
client.bodies.clear();
client.sent.clear();
await container
.read(webFormsProvider.notifier)
.updateWebForm('f1', form.toJson());
expect(client.sent.first.method, 'PUT');
final sent = jsonDecode(client.bodies.first) as Map<String, dynamic>;
expect(sent.containsKey('captcha_secret'), isFalse);
expect(sent.containsKey('is_published'), isFalse);
expect(sent['name'], 'Contact us');
},
);
test('the create sends only a name and a target', () async {
client.body = '{"count": 0, "results": [], "totals": {}}';
await readForms();
client.bodies.clear();
client.body = '{"id": "new-id", "name": "Newsletter"}';
final id = await container
.read(webFormsProvider.notifier)
.createWebForm('Newsletter');
expect(id, 'new-id');
final sent = jsonDecode(client.bodies.first) as Map<String, dynamic>;
expect(sent, {'name': 'Newsletter', 'target': 'lead'});
});
test('a ticket form is created with the ticket target', () async {
client.body = '{"count": 0, "results": [], "totals": {}}';
await readForms();
client.bodies.clear();
client.body = '{"id": "new-id", "name": "Support"}';
await container
.read(webFormsProvider.notifier)
.createWebForm('Support', target: WebForm.targetTicket);
final sent = jsonDecode(client.bodies.first) as Map<String, dynamic>;
expect(sent, {'name': 'Support', 'target': 'ticket'});
});
});
}