-
Notifications
You must be signed in to change notification settings - Fork 270
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
content test: Deduplicate ad hoc content-preparing code #659
content test: Deduplicate ad hoc content-preparing code #659
Conversation
cc8c9fe
to
7050c3c
Compare
7050c3c
to
e151e61
Compare
I'm not sure if |
e151e61
to
b011d2f
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.
Yeah, "bare" is definitely no longer an accurate word for this function :-)
Comments below. Then here's an idea I had after writing most of those comments and coming back to this:
Instead of having this method take a String html
, it can take Widget child
, intended to be the BlockContentList
or MessageContent
. Then have helpers like
Widget plainContent(String html) {
return BlockContentList(nodes: parseContent(html).nodes);
}
Widget messageContent(String html) {
return MessageContent(message: eg.streamMessage(content: html),
content: parseContent(html));
}
Then call sites can look like
void testContentSmoke(ContentExample example) {
testWidgets('smoke: ${example.description}', (tester) async {
await prepare(tester, plainContent(example.html));
where the prepare method itself might as well just be called prepare
.
That makes defaultTextStyleOverride
(and wrapWithScaffold
) entirely unnecessary too — the caller can easily wrap the result of plainContent
or messageContent
in whatever other widgets it needs.
test/widgets/content_test.dart
Outdated
Widget widget = BlockContentList(nodes: parseContent(html).nodes); | ||
|
||
if (wrapWithMessageContent) { | ||
widget = MessageContent( | ||
message: eg.streamMessage(content: html), | ||
content: parseContent(html)); | ||
} |
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.
Unlike the other conditionals, this one is replacing widget
rather than wrapping it. Probably clearest to group the two cases explicitly together, as one stanza and as an if/else.
test/widgets/content_test.dart
Outdated
if (wrapWithScaffold) { | ||
widget = Scaffold(body: widget); |
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.
Since we don't know of a case where we need this, let's just cut it — we can easily add it back later if we do find a use for it.
test/widgets/content_test.dart
Outdated
if (defaultTextStyleOverride != null) { | ||
widget = DefaultTextStyle(style: defaultTextStyleOverride, | ||
child: widget); |
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 is used in just one place, right?
I'd rather handle it with something generic — so the specifics like DefaultTextStyle
live at the caller instead. A good way to do that is probably for this to look like
if (builder != null) {
widget = builder(widget);
}
test/widgets/content_test.dart
Outdated
final pushedRoutes = <Route<dynamic>>[]; | ||
final testNavObserver = TestNavigatorObserver() | ||
..onPushed = (route, prevRoute) => pushedRoutes.add(route); |
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 particular feature makes me uneasy to have here in the generic shared helper because there's no easy way to search for the particular test cases that rely on it. One has to look for call sites that use the return value, and potentially trace that data flow onward.
The industrial-strength way to solve that would be to refactor this helper from a function prepareContentBare
into being a class instead. Then pushedRoutes
would be data on that class — either a public field, or a private one with a method like takePushedRoutes
to consume it — and one could look for references to the relevant field or method.
But rather than undertake that, I think a good solution would be to just make this feature a bit more generic, with just enough of a hook for the caller to be able to integrate in the needed way with the MaterialApp
: have this prepare method take something like List<NavigatorObserver> navObservers = const []
. Then the three call sites stay a few lines longer than in this version, but I think that's OK. Those lines are a tad verbose, but aren't all wasted either — they provide some explicitness which is nice.
If we wanted to simplify the TestNavigatorObserver
/ pushedRoutes
stuff in those call sites, a good direction would be to extend TestNavigatorObserver
itself. It's a class in our own test code; we use it in a number of other test files too, and it looks like in every case we do the exact same thing with it.
Are you also thinking that callers would also provide their own A |
I see. From a #488 perspective, I think the only particularly problematic dependency is Using So when we go to handle #488, we'll need to invent a new abstraction that allows getting a realm URL without a full-blown The |
Yeah, that's probably true.
Makes sense. When we render links that are narrow links, we depend on other data too—about streams, the self-user ID, maybe data on other users (for DM links), maybe message-move history (not sure). We'll want to make sure all that data gets used when we have it, while not crashing in the realm-description context, where we won't have it. A nice extra touch in that context would be an error dialog specific to narrow links, saying that they can't be opened in the normal way. When we render custom image emoji, and some image previews and (with #656) some video previews, we expect the user's auth credential, so we can request a protected resource on the org's server. So for these elements, we'll want the auth credential to get used when we have it, but we shouldn't crash when we don't have it. This might not change our calculations, but I wanted to point out that we have more to do about the |
True. I think those will also be pretty straightforward to deal with when we get to #488, though. In particular they'll both come up when we search
(Only when we try to open them, not render them in the first place, right? Though it doesn't change the overall point here.) |
b011d2f
to
f657827
Compare
Thanks for the review! Revision pushed. |
f657827
to
ef77c25
Compare
(Just updated to add a few small code comments.) |
None of our content widgets currently depend on a Scaffold.
We'll use this soon.
Callers can use this to check pushed routes.
It should now be easy to drop in natural dependencies of our content widgets, such as a ThemeExtension for custom light- and dark-theme colors, for all the tests of our Zulip content widgets. To do that, we can just add unconditional code to this function. After this change, some ad-hoc `tester.pumpWidget` calls do remain in this file; see tests of RealmContentNetworkImage and AvatarImage. But those aren't "Zulip content widgets" in the same sense as most of the widgets in lib/widgets/content.dart, which are used exclusively to render parsed Zulip Markdown. (There isn't even any Zulip Markdown that would produce an AvatarImage.) So, leave them be. Perhaps these widgets belong in some other file. Related: zulip#95
ef77c25
to
19ed919
Compare
Thanks! Looks good; merging. |
This PR makes it so all our tests of Zulip content widgets go through
prepareContentBare
, which has grown some explicit params that let callers opt in to extra dependencies, such as aPerAccountStoreWidget
ancestor. With this, I have two benefits in mind:Related: #95
Related: #488