-
Notifications
You must be signed in to change notification settings - Fork 173
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: Support spoilers! #503
Conversation
c296615
to
f1f111a
Compare
Thanks for the review! Revision pushed, this time with content-parsing tests; PTAL. Widget tests still TODO. |
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! The tests generally look good too; a few comments below.
lib/model/content.dart
Outdated
List<DiagnosticsNode> debugDescribeChildren() { | ||
return [ | ||
_SpoilerHeaderDiagnosticableNode(header).toDiagnosticsNode(), | ||
_SpoilerContentDiagnosticableNode(content).toDiagnosticsNode(), |
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 bit didn't feel satisfying — it feels like we should be able to express directly here a label "header" for the header and "content" for the content, rather than just list them positionally — so I looked at the debugDescribeChildren
doc to see what alternatives I could find.
That led me to [RenderTable.debugDescribeChildren] , which pointed me at the name
parameter to toDiagnosticsNode
. I just tried that out and it works nicely.
I'll push a commit to the tip of this PR branch that makes that change. Probably the ideal branch structure is
- that commit first, applying to ListNode;
- then this SpoilerNode commit, with the rest of my new commit squashed into it.
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 commit is:
cd0ed9c content [nfc]: Use toDiagnosticsNode(name: ...)
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.
Awesome; glad to have this more satisfying way. Thanks!
test/model/content_test.dart
Outdated
header: [ParagraphNode( | ||
links: [LinkNode(url: 'https://chat.zulip.org/', nodes: [TextNode('czo')])], | ||
nodes: [ |
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.
header: [ParagraphNode( | |
links: [LinkNode(url: 'https://chat.zulip.org/', nodes: [TextNode('czo')])], | |
nodes: [ | |
header: [ | |
ParagraphNode(links: null, nodes: [ |
This convenience is in a comment on equalsNode
, which is the engine of these parsing tests. It should probably be in model/content_test.dart
itself so as to be more visible:
// In [expected], for the `links` field of [ParagraphNode] or
// any other [BlockInlineContainerNode] subclass, use `null`.
// This field will be ignored in [expected], and instead the
// field's value in [actual] will be checked for accuracy against
// the [BlockInlineContainerNode.nodes] field on the same node.
void equalsNode(ContentNode expected) {
test/model/content_test.dart
Outdated
testParse('with rich header and content', | ||
// ```spoiler **bold** [czo](https://chat.zulip.org/)\n*italic* [zulip](https://zulip.com/)\n``` |
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.
Did you find a way to provoke the header into being a list of more than one BlockContentNode? That'd be fun to also test, if so.
If not, then a test case where the header is like 1. * ## details
, showing that it's not necessarily a paragraph, would be good. (I guess more generally: it'd be good to demonstrate not-a-paragraph, either as the only BlockContentNode or not.)
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.
Did you find a way to provoke the header into being a list of more than one BlockContentNode? That'd be fun to also test, if so.
No, try as I might, sadly I did not manage to do this. 😛
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.
No wait! I did! Here: https://chat.zulip.org/#narrow/stream/7-test-here/topic/content/near/1735802
Here's the HTML (formatted for easy reading, with an online HTML-formatting tool):
<div class="spoiler-block">
<div class="spoiler-header">
<p><a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3">image</a></p>
<div class="message_inline_image"><a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3" title="image"><img src="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3"></a></div>
</div>
<div class="spoiler-content" aria-hidden="true">
<p>hello world</p>
</div>
</div>
The div.spoiler-header
has a p
child and a div
child.
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.
But that includes an image, and I don't think we can build our content widget for image previews, MessageImage
, in a testContentSmoke
call. That's because MessageImage
needs PerAccountStoreWidget.of
and InheritedMessage.of
, and testContentSmoke
doesn't give support for those. When we think about those dependencies, we should have this issue in mind:
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, it's fine that testContentSmoke
won't handle that; it's just a shortcut to enable a lot of those tests to be one line each. Instead you can write a test that's similar to what you'd get by inlining testContentSmoke
and prepareContentBare
, but that provides a few more enclosing widgets in the pumpWidget
call.
(And if we find ourselves with a lot of tests like that doing similar things, we can abstract those into further helpers similar to prepareContentBare
and/or testContentSmoke
, or perhaps have the latter take an optional flag.)
I recommend rebasing atop #511, which I just sent, and converting the content-parsing tests into the new You'll also want some interaction tests, though (for showing and hiding on tap), and those might not benefit from (Hmm I guess if there's a link in the header, it'd be nice to check that tapping the link works and isn't pre-empted by the header's gesture detector. But that can still be pretty concise, along the lines of the HTML examples in the "LinkNode interactions" tests.) |
8bad174
to
62fffc5
Compare
OK, revision pushed! The feature is still imperfect:
but we can leave that for later work. I can file issues, unless one or both of these falls under an umbrella issue that you have a vision for. |
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! All looks good, with just small comments below.
@@ -226,33 +241,35 @@ class ListNode extends BlockContentNode { | |||
@override | |||
List<DiagnosticsNode> debugDescribeChildren() { | |||
return items | |||
.map((nodes) => _ListItemDiagnosticableNode(nodes).toDiagnosticsNode()) | |||
.mapIndexed((i, nodes) => | |||
_BlockContentListNode(nodes).toDiagnosticsNode(name: 'item $i')) |
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.
content [nfc]: Use toDiagnosticsNode(name: ...)
This commit should probably still have me as the author :-)
(you can use git commit --amend --author=…
to adjust)
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 indeed; thanks for catching this!
test/widgets/content_test.dart
Outdated
testWidgets('tap image', (tester) async { | ||
final pushedRoutes = await prepareContent(tester, example.html); | ||
|
||
await tester.tapAt(tester.getCenter(find.byType(RealmContentNetworkImage))); |
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: can simplify to:
await tester.tapAt(tester.getCenter(find.byType(RealmContentNetworkImage))); | |
await tester.tap(find.byType(RealmContentNetworkImage)); |
test/widgets/page_checks.dart
Outdated
@@ -9,3 +9,7 @@ extension WidgetRouteChecks on Subject<WidgetRoute> { | |||
extension AccountPageRouteMixinChecks on Subject<AccountPageRouteMixin> { | |||
Subject<int> get accountId => has((x) => x.accountId, 'accountId'); | |||
} | |||
|
|||
extension PageRouteChecks on Subject<PageRoute> { |
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: let's keep the checks extensions for Flutter upstream classes gathered in flutter_checks.dart
test/model/content_test.dart
Outdated
header: [ListNode(ListStyle.ordered, [ | ||
[ListNode(ListStyle.unordered, [ | ||
[HeadingNode(level: HeadingLevel.h2, links: null, nodes: [ | ||
TextNode('hello'), | ||
])] | ||
])], | ||
])], |
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: two-space indents, here and below
header: [ListNode(ListStyle.ordered, [ | |
[ListNode(ListStyle.unordered, [ | |
[HeadingNode(level: HeadingLevel.h2, links: null, nodes: [ | |
TextNode('hello'), | |
])] | |
])], | |
])], | |
header: [ListNode(ListStyle.ordered, [ | |
[ListNode(ListStyle.unordered, [ | |
[HeadingNode(level: HeadingLevel.h2, links: null, nodes: [ | |
TextNode('hello'), | |
])] | |
])], | |
])], |
test/model/content_test.dart
Outdated
'```spoiler [image](https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3)\nhello world\n```', | ||
'<div class="spoiler-block"><div class="spoiler-header">\n' | ||
'<p><a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3">image</a></p>\n' | ||
'<div class="message_inline_image"><a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3" title="image"><img src="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3"></a></div></div><div class="spoiler-content" aria-hidden="true">\n' |
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: this line finishes an element it didn't begin (the div.spoiler-header
), so should have a newline before starting the next:
'<div class="message_inline_image"><a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3" title="image"><img src="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3"></a></div></div><div class="spoiler-content" aria-hidden="true">\n' | |
'<div class="message_inline_image"><a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3" title="image"><img src="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3"></a></div></div>' | |
'<div class="spoiler-content" aria-hidden="true">\n' |
(Ideally the indentation would help show the nesting of elements, but this part is easy and an improvement regardless)
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, I wasn't sure how to compromise between being helpful about element nesting and not stretching out the code too much over a lot of lines. Like if it were OK to take a lot of lines, we could do
'<div class="spoiler-block">'
'<div class="spoiler-header">\n'
'<p>'
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3">image</a>'
'</p>\n'
'<div class="message_inline_image">'
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3" title="image">'
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=3">'
'</a>'
'</div>'
'</div>'
'<div class="spoiler-content" aria-hidden="true">\n'
'<p>hello world</p>\n'
'</div>'
'</div>\n',
or something like that, right?
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. And like you suggested, going all the way there would be a lot more lines and probably not worth the trade-off in making it hard to see several of these on the screen at once.
Oh, and forgot that I'd meant to reply to these:
Yeah, please go ahead and file an issue for the second one. The first one is probably best handled as part of a future sweep through our semantics and screen-reader behavior; I'll go file an issue for that and include this as an item to remember to check when doing that. |
Sure: #536 |
62fffc5
to
17c8ccc
Compare
Thanks for the review! Revision pushed. |
Thanks! Looks good; merging. |
This produces slightly nicer output, by indicating the index for each item. For example, after introducing an error in one test, the expected and actual values it prints look like this: Actual: <ZulipContent └─ListNode │ unordered │ ├─item 0: BlockContentNode list │ └─ParagraphNode │ │ was implicit │ │ │ └─TextNode │ "something" │ └─item 1: BlockContentNode list └─ParagraphNode │ was implicit │ └─TextNode "another"> instead of like this: Actual: <ZulipContent └─ListNode │ unordered │ ├─list item │ └─ParagraphNode │ │ was implicit │ │ │ └─TextNode │ "something" │ └─list item └─ParagraphNode │ was implicit │ └─TextNode "another"> This also means less code as we start adding more nodes that have this kind of structure; it avoids having to make a new subclass of DiagnosticableTree for each one.
When we support spoilers, soon, we'll want to translate the default header text ("Spoiler") into the user's language. It seems safe to assume that Zulip content will always have `ZulipLocalizations.of` support wherever it appears in the widget tree. The language will be a client setting with a default value. There are certain other assumptions that `prepareContentBare` will probably want to avoid making, though, including: - that the content is rendered in a particular message - that the content is rendered in a message at all (zulip#488) - that the content is rendered in a per-account context (zulip#488) But we'll think more about that later, when we start using testContentSmoke in more places.
17c8ccc
to
0be474a
Compare
TODO tests, but here's my code so far.
Fixes: #358