implemented full interactive poll support in Talk, including creation, voting, and closing functionality

This commit is contained in:
2026-07-12 17:54:30 +02:00
parent 3f44e9302f
commit 44e45c9b78
15 changed files with 946 additions and 87 deletions
+127
View File
@@ -0,0 +1,127 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:marianum_mobile/api/marianumcloud/talk/create_poll/create_poll_params.dart';
import 'package:marianum_mobile/api/marianumcloud/talk/get_poll/get_poll_state_response.dart';
import 'package:marianum_mobile/api/marianumcloud/talk/vote_poll/vote_poll_params.dart';
GetPollStateResponseObject _poll({
dynamic votes = const <String, dynamic>{},
String actorType = 'users',
String actorId = 'creator',
int status = pollStatusOpen,
int resultMode = pollResultModePublic,
int maxVotes = 1,
List<int> votedSelf = const [],
int? numVoters,
}) => GetPollStateResponseObject(
1,
'Frage?',
const ['A', 'B'],
votes,
actorType,
actorId,
'Creator',
status,
resultMode,
maxVotes,
votedSelf,
numVoters,
null,
);
void main() {
group('GetPollStateResponseObject.voteCounts', () {
test('map input is returned as num map', () {
final poll = _poll(votes: {'option-0': 2, 'option-1': 5});
expect(poll.voteCounts, {'option-0': 2, 'option-1': 5});
});
test('empty list (hidden results) becomes empty map', () {
expect(_poll(votes: const []).voteCounts, isEmpty);
});
test('null votes becomes empty map', () {
expect(_poll(votes: null).voteCounts, isEmpty);
});
test('non-num values are filtered out', () {
final poll = _poll(votes: {'option-0': 3, 'option-1': 'x'});
expect(poll.voteCounts, {'option-0': 3});
});
});
group('GetPollStateResponseObject status/resultMode', () {
test('isClosed reflects status', () {
expect(_poll(status: pollStatusOpen).isClosed, isFalse);
expect(_poll(status: pollStatusClosed).isClosed, isTrue);
});
test('resultsHidden reflects resultMode', () {
expect(_poll(resultMode: pollResultModePublic).resultsHidden, isFalse);
expect(_poll(resultMode: pollResultModeHidden).resultsHidden, isTrue);
});
});
group('GetPollStateResponseObject.canClose', () {
test('creator may close', () {
final poll = _poll(actorId: 'me');
expect(poll.canClose(selfId: 'me', participantType: 3), isTrue);
});
test('moderator types may close', () {
final poll = _poll(actorId: 'someone');
for (final type in [1, 2, 6]) {
expect(
poll.canClose(selfId: 'me', participantType: type),
isTrue,
reason: 'participantType $type should be allowed',
);
}
});
test('regular participant may not close', () {
final poll = _poll(actorId: 'someone');
expect(poll.canClose(selfId: 'me', participantType: 3), isFalse);
});
test('closed poll can never be closed again', () {
final poll = _poll(actorId: 'me', status: pollStatusClosed);
expect(poll.canClose(selfId: 'me', participantType: 1), isFalse);
});
test('guest with matching id is not treated as creator', () {
final poll = _poll(actorType: 'guests', actorId: 'me');
expect(poll.canClose(selfId: 'me', participantType: 3), isFalse);
});
});
group('poll params serialization', () {
// Regression: der ursprüngliche Bug entstand, weil optionIds nicht als
// valides JSON serialisiert wurde und der Server ein leeres Array sah.
test('VotePollParams encodes optionIds as a JSON array', () {
final json = jsonEncode(VotePollParams(optionIds: [0, 2]).toJson());
expect(json, '{"optionIds":[0,2]}');
});
test('empty VotePollParams encodes an empty array', () {
final json = jsonEncode(VotePollParams(optionIds: []).toJson());
expect(json, '{"optionIds":[]}');
});
test('CreatePollParams encodes all fields', () {
final json = jsonEncode(
CreatePollParams(
question: 'Q?',
options: const ['A', 'B'],
resultMode: pollResultModeHidden,
maxVotes: 0,
).toJson(),
);
expect(
json,
'{"question":"Q?","options":["A","B"],"resultMode":1,"maxVotes":0}',
);
});
});
}