| @TestOn('chrome') |
| library; |
| |
| import 'dart:js_interop'; |
| |
| import 'package:html/dom.dart' as html; |
| import 'package:html/parser.dart' as html; |
| import 'package:test/test.dart'; |
| import 'package:web/web.dart' as web; |
| |
| /// A collection of HTML test cases for differential testing. |
| /// |
| /// This suite evaluates `package:html` against the browser's native `web.DOMParser` |
| /// (via `package:web`). For each test case, it parses the HTML payload with both |
| /// `web.DOMParser` and `package:html` and compares the structure of both tags |
| /// and attributes, with the assumption that discrepancies are likely |
| /// bugs that should be fixed. |
| final _testCases = [ |
| ( |
| name: 'sanity check: simple nesting', |
| html: '<div><p>Hello <b>World</b></p></div>', |
| ), |
| ( |
| name: 'sanity check: attributes', |
| html: '<a href="https://dart.dev" class="link">link</a>', |
| ), |
| ( |
| name: 'implicit tbody template foster parenting', |
| html: '<table><template><tr>', |
| ), |
| ( |
| name: 'adoption agency algorithm nested formatters limit', |
| html: |
| '<b><p><b style=""><b style="font-weight:600"><b style=""><b style="font-weight:600"><b><b></b></b><b style="font-weight:600"><b style="font-weight:600"></b></b></b></b></b></b>', |
| ), |
| ( |
| name: 'noscript contents style dropped', |
| html: '<noscript><style type="a">', |
| ), |
| ( |
| name: 'isindex form expansion (prompt)', |
| html: '<isindex prompt><hr><hr><hr><hr><input>', |
| ), |
| ( |
| name: 'frameset disallowed in template', |
| html: '<ins><template><frameset>', |
| ), |
| ( |
| name: 'anchor active formatting overlapping menu', |
| html: '<a><menu><a>', |
| ), |
| ( |
| name: 'noscript contents dropped (paragraphs)', |
| html: '<noscript><p>', |
| ), |
| ( |
| name: 'noscript contents dropped (anchors)', |
| html: '<noscript><a></noscript>', |
| ), |
| ( |
| name: 'template implicit tbody edge case', |
| html: '<table><thead><template><td>', |
| ), |
| ( |
| name: 'noscript contents dropped (images)', |
| html: '<noscript><img src alt width height title style></noscript>', |
| ), |
| ( |
| name: 'noscript layout dropped (yahoo core)', |
| html: '<noscript><a><img></a></noscript>', |
| ), |
| ( |
| name: 'adoption agency algorithm nested loop', |
| html: '<b><em><x><x><x><p></b></em>', |
| ), |
| ( |
| name: 'isindex form expansion (action)', |
| html: '<isindex action="foo" name="bar">', |
| ), |
| ( |
| name: 'template attribute mutation leak', |
| html: '<template><html a="b"></template>', |
| ), |
| ]; |
| |
| final _knownIssues = { |
| 'implicit tbody template foster parenting', |
| 'adoption agency algorithm nested formatters limit', |
| 'noscript contents style dropped', |
| 'isindex form expansion (prompt)', |
| 'frameset disallowed in template', |
| 'anchor active formatting overlapping menu', |
| 'noscript contents dropped (paragraphs)', |
| 'noscript contents dropped (anchors)', |
| 'template implicit tbody edge case', |
| 'noscript contents dropped (images)', |
| 'noscript layout dropped (yahoo core)', |
| 'adoption agency algorithm nested loop', |
| 'isindex form expansion (action)', |
| 'template attribute mutation leak', |
| }; |
| |
| void main() { |
| for (final c in _testCases) { |
| test(c.name, () { |
| final parser = web.DOMParser(); |
| final browserDoc = parser.parseFromString(c.html.toJS, 'text/html'); |
| final pkgDoc = html.parse(c.html); |
| |
| final webCounts = _countWebTags(browserDoc); |
| final pkgCounts = _countPkgTags(pkgDoc); |
| |
| if (_knownIssues.contains(c.name)) { |
| expect(webCounts, isNot(equals(pkgCounts)), |
| reason: 'Expected to fail per known issues list.'); |
| } else { |
| expect(webCounts, equals(pkgCounts), |
| reason: 'Should parse identically'); |
| } |
| }); |
| } |
| } |
| |
| /// Counts the occurrence of every HTML tag and its attributes recursively in a Chrome [web.Document]. |
| Map<String, int> _countWebTags(web.Document doc) { |
| final counts = <String, int>{}; |
| final elements = doc.querySelectorAll('*'); |
| for (var i = 0; i < elements.length; i++) { |
| final element = elements.item(i) as web.Element; |
| final tag = element.tagName.toLowerCase(); |
| counts.update(tag, (v) => v + 1, ifAbsent: () => 1); |
| |
| final attrs = element.attributes; |
| for (var j = 0; j < attrs.length; j++) { |
| final attrName = attrs.item(j)!.name.toLowerCase(); |
| final key = '$tag.$attrName'; |
| counts.update(key, (v) => v + 1, ifAbsent: () => 1); |
| } |
| } |
| return counts; |
| } |
| |
| /// Counts the occurrence of every HTML tag and its attributes recursively in a Dart [html.Document]. |
| Map<String, int> _countPkgTags(html.Document doc) { |
| final counts = <String, int>{}; |
| void walk(html.Element node) { |
| final tag = node.localName!.toLowerCase(); |
| counts.update(tag, (v) => v + 1, ifAbsent: () => 1); |
| |
| for (final attr in node.attributes.keys) { |
| final attrName = attr.toString().toLowerCase(); |
| final key = '$tag.$attrName'; |
| counts.update(key, (v) => v + 1, ifAbsent: () => 1); |
| } |
| for (final child in node.children) { |
| walk(child); |
| } |
| } |
| |
| walk(doc.documentElement!); |
| return counts; |
| } |