-
Notifications
You must be signed in to change notification settings - Fork 306
content: Add start attribute support for ordered list #1329
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
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 working on this @lakshya1goel! Some small comments below, otherwise looks good.
c61baff
to
348edbc
Compare
Thanks for the review @rajveermalviya, pushed the revision. PTAL. |
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 @lakshya1goel! Left some small comments, mostly nits.
a04f584
to
4cd6d1b
Compare
Pushed the revision, PTAL. Thanks! |
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 @lakshya1goel!
Apart from one comment, this LGTM! Marking for Greg's review.
4cd6d1b
to
c71b346
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 @lakshya1goel, and thanks @rajveermalviya for the previous reviews!
Generally this looks good. Comments below, most of them small.
lib/widgets/content.dart
Outdated
child: Table( | ||
textBaseline: localizedTextBaseline(context), | ||
defaultVerticalAlignment: TableCellVerticalAlignment.baseline, |
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.
Do we have an issue in the tracker for the problem this is fixing? Let's file one if not, and then the commit message (and PR) can be marked as fixing 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.
I think we didn't have one previously so I have filed #F1356 for it.
7fb35b3
to
0c6fda4
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! Mostly small comments now.
switch (element.localName) { | ||
case 'ol': listStyle = ListStyle.ordered; break; | ||
case 'ul': listStyle = ListStyle.unordered; break; | ||
} | ||
assert(listStyle != null); |
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 should still have an assert for this method's expectations on element.localName
.
If you look at other methods on this class, you can see that that's the pattern they follow: any facts they assume (which their callers are expected to ensure), they assert at the top. And then down at the if/else below, this assumption is essential for understanding why the logic makes sense, so it's important to make explicit within the method.
(See also #1329 (comment) — this is why I said there that the references to ListStyle could switch to referring to element.localName
directly, instead of saying they'd be deleted.)
lib/model/content.dart
Outdated
} else { | ||
return UnorderedListNode(items: items, debugHtmlNode: debugHtmlNode); |
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.
(Specifically, this only makes sense because we know that element.localName
is "ol" at this point, not some arbitrary other value like "p" or "span" or "div".)
test/model/content_test.dart
Outdated
@@ -1155,6 +1158,32 @@ class ContentExample { | |||
], isHeader: false), | |||
]), | |||
]); | |||
|
|||
static const orderedListLargeStart = ContentExample( |
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: in general, try to keep test code in the same order as the code it's testing
Here, that means let's order these examples to match the parsing code — so these list examples go above the spoiler examples, just after the many inline examples.
test/model/content_test.dart
Outdated
OrderedListNode( | ||
start: 1, | ||
items: [[ | ||
QuotationNode([ |
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: keep the existing formatting:
OrderedListNode( | |
start: 1, | |
items: [[ | |
QuotationNode([ | |
OrderedListNode(start: 1, items: [[ | |
QuotationNode([ |
In particular the version in the current revision of the PR doesn't have correct indentation: the QuotationNode inside items
needs to be indented more deeply than the line items
starts on.
test/model/content_test.dart
Outdated
], | ||
); | ||
|
||
static const orderedListCustomStart = ContentExample( |
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: put this "custom" example before "large" — after all, the "large" value is a custom value too, so it's a particular case of being "custom"
expect(find.text('fourth'), findsOneWidget); | ||
}); | ||
|
||
testWidgets('list uses correct text baseline alignment', (tester) 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.
Hmm, I guess this is good to check too but it's not the thing I was hoping for in #1329 (comment) — it doesn't check that #1356 was (and remains) fixed.
In particular if I make this edit:
--- lib/widgets/content.dart
+++ lib/widgets/content.dart
@@ -507,7 +507,7 @@ class ListNodeWidget extends StatelessWidget {
defaultVerticalAlignment: TableCellVerticalAlignment.baseline,
textBaseline: localizedTextBaseline(context),
columnWidths: const <int, TableColumnWidth>{
- 0: IntrinsicColumnWidth(),
+ 0: FixedColumnWidth(20),
1: FlexColumnWidth(),
},
then that should reintroduce #1356 — it'd make the new code behave pretty much just like the old code before your fix — but there isn't currently a test that would detect that and fail.
Can you find a way to write a test that checks that #1356 is fixed?
test/widgets/content_test.dart
Outdated
await prepareContent(tester, Directionality( | ||
textDirection: TextDirection.rtl, | ||
child: plainContent(ContentExample.orderedListLargeStart.html))); | ||
|
||
final tableRtl = tester.widget<Table>(find.byType(Table)); | ||
check(tableRtl.defaultVerticalAlignment).equals(TableCellVerticalAlignment.baseline); | ||
check(tableRtl.textBaseline).equals(localizedTextBaseline(tester.element(find.byType(Table)))); |
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 the baseline logic interact with the text direction?
I think it doesn't, and it doesn't seem likely that it would. So let's leave this part of the test out, as being redundant.
53616a7
to
306efe6
Compare
Pushed the revision @gnprice, PTAL. Thanks! |
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.
test/model/content_test.dart
Outdated
[OrderedListNode(start: 5, items: [ | ||
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('fifth')])], | ||
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('sixth')])], | ||
]) | ||
], |
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 indent; and the two [
-enclosed lists start on the same line, so the closing ]
can go on the same line:
[OrderedListNode(start: 5, items: [ | |
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('fifth')])], | |
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('sixth')])], | |
]) | |
], | |
[OrderedListNode(start: 5, items: [ | |
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('fifth')])], | |
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('sixth')])], | |
])], |
test/model/content_test.dart
Outdated
testParseExample(ContentExample.orderedListCustomStart); | ||
testParseExample(ContentExample.orderedListLargeStart); | ||
testParseExample(ContentExample.spoilerDefaultHeader); |
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: use blank lines to group things logically:
testParseExample(ContentExample.orderedListCustomStart); | |
testParseExample(ContentExample.orderedListLargeStart); | |
testParseExample(ContentExample.spoilerDefaultHeader); | |
testParseExample(ContentExample.orderedListCustomStart); | |
testParseExample(ContentExample.orderedListLargeStart); | |
testParseExample(ContentExample.spoilerDefaultHeader); |
(including the existing blank line that was there between the multi-line group
block and these function calls)
test/model/content_test.dart
Outdated
testParseExample(ContentExample.orderedListCustomStart); | ||
testParseExample(ContentExample.orderedListLargeStart); |
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 and speaking of grouping logically: these should go inside the "parse lists" group (just above)
test/widgets/content_test.dart
Outdated
find.descendant(of: find.byType(Align), matching: find.byType(Text))); | ||
|
||
for (final text in markerTexts) { |
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: avoid trailing whitespace at end of line
find.descendant(of: find.byType(Align), matching: find.byType(Text))); | |
for (final text in markerTexts) { | |
find.descendant(of: find.byType(Align), matching: find.byType(Text))); | |
for (final text in markerTexts) { |
If you read your changes with git log -p
, it should highlight this for you. In my terminal it looks like this:
(I recommend regularly reading Git commits — yours and other people's — with git log --stat -p
, and using this "secret" for convenient navigation between commits.)
test/widgets/content_test.dart
Outdated
final renderParagraph = tester.renderObject(find.text(text.data!)) as RenderParagraph; | ||
final textHeight = renderParagraph.size.height; | ||
final lineHeight = renderParagraph.text.style!.height! * renderParagraph.text.style!.fontSize!; | ||
check(textHeight).equals(lineHeight); |
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 a good strategy. Let's add one other check:
final renderParagraph = tester.renderObject(find.text(text.data!)) as RenderParagraph; | |
final textHeight = renderParagraph.size.height; | |
final lineHeight = renderParagraph.text.style!.height! * renderParagraph.text.style!.fontSize!; | |
check(textHeight).equals(lineHeight); | |
final renderParagraph = tester.renderObject(find.text(text.data!)) as RenderParagraph; | |
final textHeight = renderParagraph.size.height; | |
final lineHeight = renderParagraph.text.style!.height! * renderParagraph.text.style!.fontSize!; | |
check(textHeight).equals(lineHeight); | |
check(renderParagraph.didExceedMaxLines).isFalse(); |
That way if we had a maxLines: 1
on that Text widget (very similar to how web behaved for years until the recent fix), that would break the test too.
test/widgets/content_test.dart
Outdated
final markerTexts = tester.widgetList<Text>( | ||
find.descendant(of: find.byType(Align), matching: find.byType(Text))); |
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.
Looking for Align
here makes this tied into some fairly internal details of how ListNodeWidget is implemented.
Instead, let's use find.textContaining('9999')
. That should find the same Text
widget in a way that's more focused on what the user sees, rather than how the implementation works. See:
https://zulip.readthedocs.io/en/latest/testing/philosophy.html#integration-testing-or-unit-testing
(It does make this test dependent on the details of the test data it's using, from ContentExample.orderedListLargeStart
. That's OK — that test data exists basically for the sake of this test and the related parsing test.)
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 we only need to check one marker, not both.
test/widgets/content_test.dart
Outdated
final textWidth = renderParagraph.size.width; | ||
final renderBox = tester.renderObject(find.ancestor( | ||
of: find.text(text.data!), | ||
matching: find.byType(Align))) as RenderBox; | ||
check(renderBox.size.width >= textWidth).isTrue(); |
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 think this check can't really fail — it's checking that the parent is at least as big as the child, and that's always true unless one uses some fairly special widgets that aren't likely to get involved here. We can leave it out.
test/widgets/content_test.dart
Outdated
final align = tester.widget<Align>( | ||
find.ancestor(of: find.text(text.data!), matching: find.byType(Align))); | ||
check(align.alignment).equals(AlignmentDirectional.topEnd); |
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.
Let's adapt this so that it's in terms of what the user sees, rather than how the implementation code works. (Per https://zulip.readthedocs.io/en/latest/testing/philosophy.html#integration-testing-or-unit-testing again.)
Concretely: the content in this test has two markers. They aren't going to be the same width. So if the alignment is correct, they'll have their right edges at the same x-coordinate, and if the alignment is wrong they'll almost surely have their right edges at different x-coordinates. So to write a test, get the locations of the two marker Text
s, confirm the widths are different, and then check the right edges match.
test/widgets/content_test.dart
Outdated
check(table.textBaseline).equals(localizedTextBaseline(tester.element(find.byType(Table)))); | ||
}); | ||
|
||
testWidgets('long ordered list markers render completely and are right-aligned', (tester) 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: this should be two test cases (two testWidgets
calls) — one for having enough room, and one for being end-aligned
(also it's not right-aligned in general — it's end-aligned, which means the left side when using an RTL language, like Persian or Arabic or Hebrew)
(it's fine not to add a test for the RTL case, just adjust this test name so it's accurate)
lib/model/content.dart
Outdated
@@ -251,21 +251,11 @@ class HeadingNode extends BlockInlineContainerNode { | |||
} | |||
} | |||
|
|||
enum ListStyle { ordered, unordered } | |||
sealed class ListNode extends BlockContentNode { | |||
const ListNode({required this.items, super.debugHtmlNode}); |
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 I missed this earlier — let's keep the constructor calls a bit simpler:
const ListNode({required this.items, super.debugHtmlNode}); | |
const ListNode(this.items, {super.debugHtmlNode}); |
(Just like in the existing version, and like in some other classes in this file: QuotationNode, CodeBlockNode, TextNode, and others where it's clear what a positional argument should mean.)
306efe6
to
e71ca83
Compare
Thanks for the detailed review @gnprice , I have pushed the revision, PTAL. |
(I've just edited the PR description so that it's marked as fixing #1356 too — the GitHub "fixes" syntax doesn't understand "and", and needs the word "fixes" to be right next to the issue number.) |
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! Just a couple of small comments now.
test/widgets/content_test.dart
Outdated
final marker9999 = tester.renderObject(find.textContaining('9999.')) as RenderParagraph; | ||
final marker10000 = tester.renderObject(find.textContaining('10000.')) as RenderParagraph; | ||
check(marker9999.size.width != marker10000.size.width).isTrue(); | ||
|
||
final marker9999Pos = marker9999.localToGlobal(Offset.zero); | ||
final marker10000Pos = marker10000.localToGlobal(Offset.zero); | ||
check(marker9999Pos.dx + marker9999.size.width) | ||
.equals(marker10000Pos.dx + marker10000.size.width); |
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, this test is accurate, but can be simplified using getRect
:
final marker9999 = tester.getRect(find.textContaining('9999.'));
then marker9999.width
and marker9999.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.
(If you dig into the implementation of getRect
, you'll see it's doing something similar to this test's logic.)
test/model/content_test.dart
Outdated
])] | ||
])], | ||
])], | ||
], start: 1)], |
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: keep start: 1
as the first argument, before the positional items
argument
Dart has the handy feature of allowing named and positional arguments to be interleaved in any order, including named first. That's useful to do in a case like this where there's a named argument that's short and fits easily on the first line and then a longer multi-line argument that's positional.
e71ca83
to
564047c
Compare
Pushed the revision, Please have a look @gnprice . Thanks! |
…in a bit more A couple of the extension members added here don't actually end up getting used (because the "height of one line" check feels like it benefits from describing the calculation more explicitly). Still the fact we're referring to those properties in a test makes a good prompt to add the extension so future tests can use it.
Thanks! Looks good; merging. I made a few tweaks in the first commit along the lines of #1329 (comment), for two-space indentation and to avoid other formatting changes. Also added one commit on top which I recommend reading: since it points to an important bit of test-writing style which I'd missed in previous reviews. In particular, a change like this: - check(marker9999.right).equals(marker10000.right);
+ check(marker9999).right.equals(marker10000.right); means that if that check fails, the resulting error will be more informative because it will have the whole Rect value |
Fixes: #59
Fixes: #1356
Screenshot