-
Notifications
You must be signed in to change notification settings - Fork 309
Support polls (read-only) #885
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
For ease of cross-reference: the previous PR #823 originally contained these changes too, and had a round of UX review starting with these screenshots: |
be7ead6
to
3a0f853
Compare
Moving this to integration review because these changes went through maintainer review in the original PR. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Here's a review focused on the first two commits, which set up the model:
144d5af api: Construct polls data store from submessages.
8388936 event: Handle submessage event for polls.
I didn't read through all of the tests, because I left some high-level comments there that I think will make some of them easier to read.
Left for a future round are the remaining commits:
6cc7d0d (optional) test: Add a test helper for inspecting logs.
3a0f853 poll: Support read-only poll widget UI.
lib/api/model/submessage.dart
Outdated
/// The limit of options any single user can add to a poll. | ||
/// | ||
/// See https://github.com/zulip/zulip/blob/304d948416465c1a085122af5d752f03d6797003/web/shared/src/poll_data.ts#L69-L71 | ||
static const maxIdx = 1000; // TODO validate |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's involved in this TODO?
lib/api/model/submessage.dart
Outdated
final Set<String> _optionNames = {}; | ||
final Map<String, PollOption> _options = {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the relationship between these two data structures?
Relatedly, what are the keys in the _options
map?
Those would both be good questions to answer in dartdoc on these fields.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extracted a OptionKey
type alias to make it clearer that _options
's keys are keys.
Also renamed _optionNames
to _existingOptionTexts
with a new comment.
lib/api/model/submessage.dart
Outdated
Poll({ | ||
required this.pollSenderId, | ||
required this.question, | ||
required final List<String> options, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
required final List<String> options, | |
required List<String> options, |
The final
just stops us from mutating this parameter as a local variable, right? So that it doesn't have any effect on the interface for callers.
We don't routinely use that feature, and I don't think it's unusually helpful on this constructor. So let's leave it out, just to not add another thing for the reader looking at the interface to have to process.
lib/api/model/submessage.dart
Outdated
for (int index = 0; index < options.length; index += 1) { | ||
// Initial poll options use a placeholder senderId. | ||
// See [PollEventSubmessage.optionKey] for details. | ||
_addOption(null, options[index], idx: index); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This call site is probably clearer if the parameters are made named. (Or the senderId
, anyway, but then they might as well all three be named.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also probably clearer if senderId comes first, then idx, then option. The first two are basically specifying a key at which the latter will be set as a value.
lib/api/model/submessage.dart
Outdated
/// - https://zulip.com/help/create-a-poll | ||
/// - https://github.com/zulip/zulip/blob/304d948416465c1a085122af5d752f03d6797003/web/shared/src/poll_data.ts | ||
class Poll { | ||
Poll({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be made private — its only caller is the other constructor, and that seems like how it should stay.
Then put the public constructor first.
test/model/message_test.dart
Outdated
}); | ||
|
||
test('message has no submessages', () async { | ||
final streamMessage = eg.streamMessage(id: 123); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
final streamMessage = eg.streamMessage(id: 123); | |
final message = eg.streamMessage(id: 123); |
The fact it's a stream message isn't important, right? So can leave that out of the story this tells to the reader. (It's still there in the eg.streamMessage
call, but gets less emphasis.)
test/model/message_test.dart
Outdated
final messageId = await preparePollMessage(); | ||
await store.handleEvent( | ||
eg.submessageEvent( | ||
streamMessage.id, | ||
eg.selfUser.userId, | ||
content: jsonEncode(PollQuestionEventSubmessage(question: 'New question')))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
final messageId = await preparePollMessage(); | |
await store.handleEvent( | |
eg.submessageEvent( | |
streamMessage.id, | |
eg.selfUser.userId, | |
content: jsonEncode(PollQuestionEventSubmessage(question: 'New question')))); | |
final messageId = await preparePollMessage(question: 'Old question'); | |
await store.handleEvent( | |
eg.submessageEvent( | |
streamMessage.id, | |
eg.selfUser.userId, | |
content: jsonEncode(PollQuestionEventSubmessage(question: 'New question')))); |
This way the test case is self-contained for the reader — the contrast between the original question value and the new question value is explicit within the test case's own source code.
(Similar to my comment above about eg.pollWidgetDataFavoriteLetter
.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a great catch. I find it to be quite a good demonstration.
test/model/message_test.dart
Outdated
@@ -576,4 +603,197 @@ void main() { | |||
.reactions.isNotNull().jsonEquals([eg.unicodeEmojiReaction]); | |||
}); | |||
}); | |||
|
|||
group('handleSubmessageEvent', () { | |||
late StreamMessage streamMessage; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
late StreamMessage streamMessage; | |
late Message message; |
Similar to my preceding comment (on code a bit below this) — the detail that it's a stream message doesn't matter to the narrative, so can be left out.
test/test_log.dart
Outdated
logHistory = []; | ||
try { | ||
await callback(); | ||
return check(logHistory).isA<List<String>>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return check(logHistory).isA<List<String>>(); | |
return check(logHistory!); |
If logHistory
is back to null at this point, that seems like not just a normal failure of the calling test case — it means something that shouldn't be messing with that variable has a surprising bug where it's doing so anyway. So we can avoid cluttering test failure messages with a line about how the log history was expected to be (and is) a List<String>
.
lib/log.dart
Outdated
|
||
/// Whether [debugLog] should do anything. | ||
/// | ||
/// This has an effect only in a debug build. | ||
bool debugLogEnabled = false; | ||
|
||
/// Used for log inspection in tests. | ||
@visibleForTesting | ||
List<String>? logHistory; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
List<String>? logHistory; | |
List<String>? debugLogHistory; |
This will live in our global namespace (e.g. it'll appear in autocomplete from all over the app) so the "debug" prefix is important context.
284f08b
to
5b82669
Compare
Thanks for the review @gnprice! I have addressed them and updated the PR. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the revision! Comments below.
I didn't get to a careful read-through of this revision today, but wanted to get a round of feedback to you since it's been a couple of days.
test/example_data.dart
Outdated
'options': ['A', 'B', 'C'], | ||
} | ||
}; | ||
Map<String, Object> pollWidgetDataJson({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this return a PollWidgetData instead?
That's what we generally do in other example-data functions — it's good for keeping things well-typed. The caller can always say .toJson()
when that's what it needs. (And for a lot of purposes it won't even need to: e.g. passing to eg.submessage
, where the jsonEncode
should take care of that.)
test/example_data.dart
Outdated
|
||
Submessage submessage({ | ||
SubmessageType? msgType, | ||
required Object? content, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then this can be typed SubmessageData
, which I think would help make the interface clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bump
lib/api/model/submessage.dart
Outdated
@override | ||
bool operator ==(Object other) { | ||
if (other is! PollOption) return false; | ||
|
||
return other.hashCode == hashCode; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. This override is still incorrect, though, for the reason I gave in the first comment (even in this revision which has fixed the secondary issue in the third comment). See the doc on Object.==
.
It may work in tests because the right-hand side of the comparison is a throwaway that won't be mutated later… but an override here isn't restricted to use in tests, so having an incorrect ==
implementation is dangerous as a source of future bugs.
Looking at those tests, you can express those checks without a ==
override by using .deepEquals(<Condition<Object?>>[ … ])
. See test/notifications/display_test.dart
for an example, in the checkNotification
helper. See also #878 (not yet merged) for further examples in a slightly different style, with the conditionActiveNotif
and conditionSummaryActiveNotif
helpers.
: null; | ||
} | ||
|
||
static Poll? _readPoll(Map<Object?, Object?> json, String key) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool. Yeah, the new nicely-short _readPoll
makes sense to keep on Message
, as it's expressing the details that are about property names on message objects.
lib/api/model/model.g.dart
Outdated
)..poll = Message._readPoll(json, 'submessages') == null | ||
? null | ||
: Poll.fromJson(Message._readPoll(json, 'submessages')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ow, this generated code is inefficient. (Since _readPoll
includes all the work of actually parsing the JSON.)
If there's not a quick and clean way to rearrange things to avoid this double parsing, that's fine and it shouldn't block merging this — polls aren't that common so it's not a major performance issue. But in that case let's leave a TODO comment about it at Message.poll
.
test/api/model/submessage_test.dart
Outdated
eg.submessage( | ||
content: event, | ||
senderId: senderId, | ||
), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: fold onto one line
test/api/model/submessage_test.dart
Outdated
events: [ | ||
(eg.otherUser.userId, PollNewOptionEventSubmessage(idx: 0, option: 'D')), | ||
(eg.otherUser.userId, PollVoteEventSubmessage(key: '${eg.otherUser.userId},0', op: PollVoteOp.add)), | ||
(eg.otherUser.userId, PollVoteEventSubmessage(key: '${eg.selfUser.userId},0', op: PollVoteOp.add)), | ||
(eg.otherUser.userId, PollVoteEventSubmessage(key: '${eg.selfUser.userId},0', op: PollVoteOp.remove)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this latter key correspond to an option? It looks like there's the initial options, then one created by eg.otherUser, and none by eg.selfUser.
test/api/model/submessage_test.dart
Outdated
test('applyEvent: adding repeated option', () { | ||
final poll = messageWithSubmessageContents(pollWidgetData).poll!; | ||
check(poll.options).deepEquals(defaultOptions); | ||
poll.applyEvent(eg.otherUser.userId, PollNewOptionEventSubmessage( | ||
option: defaultOptions[0].text, idx: 0)); | ||
check(poll.options).deepEquals(defaultOptions); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In either the name or a one-line comment within the test, say explicitly how the story ends: IIUC, the answer is that the event gets ignored.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would do it:
test('applyEvent: adding repeated option', () { | |
final poll = messageWithSubmessageContents(pollWidgetData).poll!; | |
check(poll.options).deepEquals(defaultOptions); | |
poll.applyEvent(eg.otherUser.userId, PollNewOptionEventSubmessage( | |
option: defaultOptions[0].text, idx: 0)); | |
check(poll.options).deepEquals(defaultOptions); | |
test('applyEvent: option with duplicate text ignored', () { | |
final poll = messageWithSubmessageContents(pollWidgetData).poll!; | |
check(poll.options).deepEquals(defaultOptions); | |
poll.applyEvent(eg.otherUser.userId, PollNewOptionEventSubmessage( | |
option: defaultOptions[0].text, idx: 0)); | |
check(poll.options).deepEquals(defaultOptions); |
test/api/model/submessage_test.dart
Outdated
]); | ||
}); | ||
|
||
test('applyEvent: adding repeated option', () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having these test cases for error paths is great. Let's also have test cases for the normal happy paths, though. As a bonus, those can make the error tests clearer because they serve as a baseline for the reader to compare to.
As is, if I'm reading right it looks like these tests would all pass if applyEvent
just always did nothing 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I guess the tests in lib/model/message_test.dart
end up covering those happy paths.
I think it's clearest if the success cases and error cases have their tests next to each other — as I said above, the success tests make a helpful baseline for reading the error tests, and also the reader of the success tests may get nervous if it looks like there are no tests for the error cases.
I like the layer that the lib/model/message_test.dart
tests are expressed at: calling store.handleEvent
with a Zulip server event, just as if the event had come in from the server in the polling loop. That's how we try to write most of our model tests, following this principle:
https://zulip.readthedocs.io/en/latest/testing/philosophy.html#integration-testing-or-unit-testing
So let's try to move these error tests to that file and convert them to that file's style.
In fact let's go a step farther: let's move even the "parse poll …" test cases the same way. Then this whole "Message.poll" test group gets replaced by a comment saying the tests for deserializing [Message.poll] can be found over there.
Really what's going on here is that for this data structure, we've fused the job of deserializing it with the job of setting up a nontrivial data structure, including applying some significant model logic to apply all those poll-event submessages. So the deserialization code has a lot of the nature of model code, and it's fitting if its tests therefore wind up in the model code's test files. And then the tests for later events coming in definitely belong on the model side (as discussed above), and they're exercising most of the same code as the deserialization… so the deserialization tests belong next to the later-events tests, in test/model/
.
lib/api/model/submessage.dart
Outdated
final Object? decodedContent; | ||
try { | ||
decodedContent = jsonDecode(event.content); | ||
} catch (e) { | ||
assert(debugLog('Invalid JSON from submessage event data for poll: $e\n${jsonEncode(event)}')); // TODO(log) | ||
return; | ||
} | ||
|
||
final PollEventSubmessage? pollEventSubmessage; | ||
try { | ||
pollEventSubmessage = PollEventSubmessage.fromJson(decodedContent as Map<String, Object?>); | ||
} catch (e) { | ||
assert(debugLog('Malformed submessage event data for poll: $e\n${jsonEncode(event)}')); // TODO(log) | ||
return; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh one more comment: we can simplify this a bit by making this just one try/catch that doesn't distinguish between whether it was the jsonDecode
or the fromJson
call that threw. Conceptually those are two phases of a single operation anyway: deserialize a string into a PollEventSubmessage.
Similarly if you walk through the logic in ApiConnection.send, I'm pretty sure the behavior there is that both kinds of failures throw very similar exceptions (instances of MalformedServerResponseException). So it's not worth getting more fine-grained here than we are there, the main place we deserialize JSON from the server.
(following up on #885 (comment) )
Partially updated. The tests reorganization is WIP. |
The tests from "Message.poll" are rewritten in two destinations. Here's a broad overview:
Extra note: dropped the "no poll if submessages is empty" test because it is a duplicate of the existing "message has no submessages" test. |
9b4b999
to
bf599f2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the revision! Here's a next round, covering all the first N-1 commits:
2bfbff0 message test [nfc]: Move handleDeleteMessageEvent
ed5f68e submessage: Extract OptionKey.
2d8f11e submessage [nfc]: Minor wording change to a comment.
ae9f87c submessage test [nfc]: Extract pollWidgetDataJson.
a85dbe4 api: Construct polls data store from submessages.
727b9d8 event: Handle submessage event for polls.
b00a64f (optional) test: Add a test helper for inspecting logs.
and leaving only the last commit for a future round:
bf599f2 poll: Support read-only poll widget UI.
lib/api/model/model.dart
Outdated
static Map<String, Object?> _readPoll(Map<Object?, Object?> json, String key) { | ||
return {'data': json[key], 'sender_id': json['sender_id']}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This allocates a new Map
, for every message we read. How about just returning json
?
Or having this call Submessage.parseSubmessagesJson
, like in the revision as of my previous review.
test/example_data.dart
Outdated
|
||
Submessage submessage({ | ||
SubmessageType? msgType, | ||
required Object? content, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bump
lib/api/model/submessage.dart
Outdated
// It will start returning `Poll` as we support more Zulip widgets. | ||
static Poll? parseSubmessagesJson(List<Object?> json, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment doesn't seem right — this is on Submessage
so won't remain specific to Poll
in that future.
Perhaps this was intended for Poll.fromJson
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Poll
in the comment was unintended. Made it a TODO instead:
// TODO: Use a generalized return type when supporting other Zulip widgets.
lib/api/model/submessage.dart
Outdated
@@ -205,6 +239,8 @@ enum PollEventSubmessageType { | |||
.map((key, value) => MapEntry(value, key)); | |||
} | |||
|
|||
typedef OptionKey = String; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to #823 (comment) :
typedef OptionKey = String; | |
typedef PollOptionKey = String; |
'options': ['A', 'B', 'C'], | ||
} | ||
}; | ||
PollWidgetData pollWidgetData({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: commit message doesn't match:
submessage test [nfc]: Extract pollWidgetDataJson.
test/model/message_test.dart
Outdated
|
||
await prepare(); | ||
await store.handleEvent(MessageEvent(id: 0, message: message)); | ||
return check(store.messages[message.id]).isNotNull(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return check(store.messages[message.id]).isNotNull(); |
The callers don't use this; and it makes sense to leave that to the checkPoll
helper.
test/model/message_test.dart
Outdated
final defaultOptionConditions = [ | ||
conditionPollOption('foo'), | ||
conditionPollOption('bar'), | ||
]; | ||
|
||
final defaultPollWidgetData = eg.pollWidgetData( | ||
question: 'example question', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: swap order, for a more logical order
test/model/message_test.dart
Outdated
/// Perform a single-message initial message fetch for [messageList] with | ||
/// submessages. | ||
/// | ||
/// The test case must have already called [prepare] to initialize the state. | ||
Future<void> prepareMessageWithSubmessages( | ||
Message message, List<Submessage> submessages, { | ||
bool foundOldest = false, | ||
}) async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just like we did in the lib/api/ code, I'd like to avoid having poll-related or submessage-related code scattered through this test file. That will get in the way when trying to read and maintain this code for everything not related to submessages (because one usually doesn't want or need to think about them); and it also makes the poll/submessages test code itself harder to read, because one has to skip back and forth between definitions far apart in the file.
Instead, they can all go in a group
call at the end. That won't strictly follow the ordering of the code under test, but I think the better grouping will be worth it.
test/model/message_test.dart
Outdated
checkPoll(message).question.equals('Old question'); | ||
}); | ||
|
||
test('ignore submessage event with malformed content', () async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: move out of "question event" group, to go after "message has no submessages" above — this happens to say 'type': 'question'
but isn't really about a particular type of poll event
test/model/message_test.dart
Outdated
}))))..length.equals(2) | ||
..last.contains('Malformed submessage event data for poll'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, thinking about this checkLogs
sort of test check now that I've reached this commit:
This feels to me like the sort of thing that's going to be quite brittle. (E.g. why length 2 — I guess the first is the one in PerAccountStore.handleEvent
?) If we start widely adopting this pattern, then any time we add or remove a debugLog
call there's going to be a bunch of random tests all over that need to be updated for it.
Also when we change the content of a debugLog
call, we'll have to update the corresponding test. That's OK for a lot of things (if it's really the test aimed at the thing one is changing), but it makes changes more heavyweight. And these log messages are internal details, in the sense that they're aimed only at us the developers — they're not something our users, or any external system, ever see.
If we find a place where there's a debugLog
call that we feel is really critical to ensure doesn't accidentally get broken, then that could be a good use case for something like this despite those costs. But I think this debugLog
call is typical in that it isn't critical to that degree. Let's therefore leave this check out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(As for the server tests:
This is inspired by `TestCase.assertLogs`, which is widely adapted
in the Zulip server tests.
I have the sense that a lot of those uses are motivated mostly by the need to suppress the logs so they don't spew into the output of running the tests. (That's especially clear when they don't end up asserting anything about the logs emitted; and I suspect it's the only important motivation in many tests that do assert something about them too.) In this codebase we just handle that systematically using debugLogEnabled
.
The other motivation for assertLogs
checks in the server is that in the server, those log messages go into the production server log, which is an important operational tool for Zulip server operators including ourselves. That has somewhat the same shape as our debugLog
calls for debugging… with the crucial difference that when we see debugLog
output, the app is a debug build, and so we're presumably sitting there debugging it and can easily add more log lines as we wish and then hot-reload and repeat whatever we did. That's very different from the scenario where something went wrong an hour ago on one's live production server and you're frantically trying to pin down what happened.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation! Dropping the commit.
Another motivation for adding checkLogs
was to have a convenient way of verifying we expect to happen in our code -- too convenient that it relies heavily on implementation details. I agree that testing the externally visible interface is preferred.
I have updated the PR. In this update:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the revision! I haven't yet read the final commit closely:
42c9c10 poll: Support read-only poll widget UI.
but the rest all generally look good. A few comments below.
I'll also push a revision that I think demonstrates the main comment below, about the tests — please take a look and see if I'm missing something.
lib/widgets/poll.dart
Outdated
String getNameByUserId(int userId) => | ||
store.users[userId]?.fullName ?? zulipLocalizations.unknownUserName; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, when at #885 (comment) I mentioned a local variable instead of a method:
It can probably just be a local variable in that build method, not a method or anything.
I was thinking of the result of map
and join
as a local variable.
I think that'd be a good further tweak — the widget expression below has a lot of details of layout, so it'd be good to move the more interesting computations out of it to happen above.
/// See also: | ||
/// - https://zulip.com/help/create-a-poll | ||
/// - https://github.com/zulip/zulip/blob/304d948416465c1a085122af5d752f03d6797003/web/shared/src/poll_data.ts | ||
class Poll { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, in the new revision this earlier commit adds tests in test/api/model/submessage_test.dart
, and then the later commit moves them and adapts them to test/model/message_test.dart
.
I don't really understand why the tests can't be in the latter form already in the earlier commit. The polls are there on the messages in the message store, right? So the tests that find them in the message store should work fine.
/// See also: | ||
/// - https://zulip.com/help/create-a-poll | ||
/// - https://github.com/zulip/zulip/blob/304d948416465c1a085122af5d752f03d6797003/web/shared/src/poll_data.ts | ||
class Poll { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we really would have to move around and rewrite the tests like that, then it'd be better to squash the commits.
test/model/message_test.dart
Outdated
@@ -576,4 +584,346 @@ void main() { | |||
.reactions.isNotNull().jsonEquals([eg.unicodeEmojiReaction]); | |||
}); | |||
}); | |||
|
|||
group('handleSubmessageEvent', () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, one other nit:
This group isn't specific to handleSubmessageEvent
— it's about interpreting Message objects that come in the first place with submessages, too, where SubmessageEvent
may never be involved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gave the outer group a different name: "handle Poll related events". The name "handleSubmessageEvent" should be applicable to the subgroup of tests that do take care of SubmessageEvent
. To parallel that, I renamed the other subgroup to "handleMessageEvent with initial submessages"
213b802
to
3a216b2
Compare
Pushed a minor formatting update. |
Thanks for the revision! Those changes all look good. All the commits before the last one are now ready, so I'm going to go ahead and merge them. I won't make it to a review of the last commit today, though, before I go on vacation: So please send that as a fresh PR. Let's also have @chrisbobbe do the first review on that — I know a similar commit was in #823 which he reviewed before, but it's been a bit of a marathon series of changes 🙂 (which is natural because this is a complex feature), so he may have new comments to make when looking at it with fresh eyes. |
to match the order the events are defined. Signed-off-by: Zixuan James Li <[email protected]>
Semantically, we expect strings of a specific shape to be the key for options. This make it clearer to the reader without complicating things with extra validation. Signed-off-by: Zixuan James Li <[email protected]>
Signed-off-by: Zixuan James Li <[email protected]>
The goal is to make the relevant context of the test data local to the tests, and keep the example data as boring as possible. We also combine the smoke test into the test for invalid widget types, contrasting it with the failing cases. Signed-off-by: Zixuan James Li <[email protected]>
For now, we only consider the case when the submessages describe a poll, and disgard them otherwise. It will be a simple refactor later to support other Zulip widgets like todo lists, by extracting a common ancestors for such widget data structures. The `Poll` data structure will become useful when we support submessage events, where updates to a poll can happen. Some more comments are added here instead of earlier because of their references to `SubmessageData`. Signed-off-by: Zixuan James Li <[email protected]>
We could potentially avoid notifying listeners in some cases when handling submessage event, but it wouldn't be critically necessary. Signed-off-by: Zixuan James Li <[email protected]>
Stacked on top of #823.
Fixes: #165