-
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: Add start attribute support for ordered list #1329
base: main
Are you sure you want to change the base?
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. |
Fixes: #59 and #1356
Screenshot