/// Additional feature tests that aren't based on test data. library; import 'package:html/dom.dart'; import 'package:html/parser.dart'; import 'package:html/src/constants.dart'; import 'package:html/src/encoding_parser.dart'; import 'package:html/src/treebuilder.dart'; import 'package:source_span/source_span.dart'; import 'package:test/test.dart'; void main() { _testElementSpans(); test('doctype is cloneable', () { final doc = parse(''); final doctype = doc.nodes[0] as DocumentType; expect(doctype.clone(false).toString(), ''); }); test('line counter', () { // http://groups.google.com/group/html5lib-discuss/browse_frm/thread/f4f00e4a2f26d5c0 final doc = parse('
\nx\n>\n
'); expect(doc.body!.innerHtml, '
x\n>\n
'); }); test('namespace html elements on', () { final doc = HtmlParser('', tree: TreeBuilder(true)).parse(); expect((doc.nodes[0] as Element).namespaceUri, Namespaces.html); }); test('namespace html elements off', () { final doc = HtmlParser('', tree: TreeBuilder(false)).parse(); expect((doc.nodes[0] as Element).namespaceUri, null); }); test('parse error spans - full', () { final parser = HtmlParser(''' ''', generateSpans: true, sourceUrl: 'ParseError'); final doc = parser.parse(); expect(doc.body!.outerHtml, '\n \n \n\n'); expect(parser.errors.length, 1); final error = parser.errors[0]; expect(error.errorCode, 'unexpected-doctype'); // Note: these values are 0-based, but the printed format is 1-based. expect(error.span!.start.line, 3); expect(error.span!.end.line, 3); expect(error.span!.start.column, 2); expect(error.span!.end.column, 17); expect(error.span!.text, ''); expect(error.toString(), ''' On line 4, column 3 of ParseError: Unexpected DOCTYPE. Ignored. ╷ 4 │ │ ^^^^^^^^^^^^^^^ ╵'''); }); test('parse error spans - minimal', () { final parser = HtmlParser(''' '''); final doc = parser.parse(); expect(doc.body!.outerHtml, '\n \n \n\n'); expect(parser.errors.length, 1); final error = parser.errors[0]; expect(error.errorCode, 'unexpected-doctype'); }); test('text spans should have the correct length', () { final textContent = '\n hello {{name}}'; final html = '
$textContent
'; final doc = parse(html, generateSpans: true); final text = doc.body!.nodes[0].nodes[0] as Text; expect(text, const TypeMatcher()); expect(text.data, textContent); expect(text.sourceSpan!.start.offset, html.indexOf(textContent)); expect(text.sourceSpan!.length, textContent.length); }); test('attribute spans', () { final text = ''; final doc = parse(text, generateSpans: true); final elem = doc.querySelector('element')!; expect(elem.sourceSpan!.start.offset, 0); expect(elem.sourceSpan!.end.offset, text.length); expect(elem.sourceSpan!.text, text); expect(elem.attributeSpans!['quux'], null); final span = elem.attributeSpans!['extends']!; expect(span.start.offset, text.indexOf('extends')); expect(span.text, 'extends="x-bar"'); }); test('attribute value spans', () { final text = ''; final doc = parse(text, generateSpans: true); final elem = doc.querySelector('element')!; expect(elem.attributeValueSpans!['quux'], null); final span = elem.attributeValueSpans!['extends']!; expect(span.start.offset, text.indexOf('x-bar')); expect(span.text, 'x-bar'); }); test('attribute spans if no attributes', () { final text = ''; final doc = parse(text, generateSpans: true); final elem = doc.querySelector('element')!; expect(elem.attributeSpans!['quux'], null); expect(elem.attributeValueSpans!['quux'], null); }); test('attribute spans if no attribute value', () { final text = ''; final doc = parse(text, generateSpans: true); final elem = doc.querySelector('foo')!; expect(elem.attributeSpans!['template']!.start.offset, text.indexOf('template')); expect(elem.attributeValueSpans!.containsKey('template'), false); }); test('attribute spans null if code parsed without spans', () { final text = ''; final doc = parse(text); final elem = doc.querySelector('element')!; expect(elem.sourceSpan, null); expect(elem.attributeSpans!['quux'], null); expect(elem.attributeSpans!['extends'], null); }); test('attribute spans if value contains & (ambiguous ampersand)', () { final expectedUrl = 'foo?key=value&key2=value2'; final text = ''; final doc = parse(text, generateSpans: true); final elem = doc.querySelector('script')!; final span = elem.attributeValueSpans!['src']!; expect(span.start.offset, text.indexOf('foo')); expect(span.text, expectedUrl); }); test('void element innerHTML', () { var doc = parse('
'); expect(doc.body!.innerHtml, '
'); doc = parse(''); expect(doc.body!.innerHtml, ''); doc = parse('
'); expect(doc.body!.innerHtml, '
'); doc = parse('
'); expect(doc.body!.innerHtml, '
'); }); test('empty document has html, body, and head', () { final doc = parse(''); final html = ''; expect(doc.outerHtml, html); expect(doc.documentElement!.outerHtml, html); expect(doc.head!.outerHtml, ''); expect(doc.body!.outerHtml, ''); }); test('strange table case', () { final doc = parse('').body!; expect(doc.innerHtml, '
'); }); group('html serialization', () { test('attribute order', () { // Note: the spec only requires a stable order. // However, we preserve the input order via LinkedHashMap final body = parse('').body!; expect(body.innerHtml, ''); expect(body.querySelector('foo')!.attributes.remove('a'), '2'); expect(body.innerHtml, ''); body.querySelector('foo')!.attributes['a'] = '0'; expect(body.innerHtml, ''); }); test('escaping Text node in ').firstChild as Element; expect(e.outerHtml, ''); }); test('escaping Text node in ', () { final e = parseFragment('a && b').firstChild as Element; expect(e.outerHtml, 'a && b'); }); test('Escaping attributes', () { var e = parseFragment('
').firstChild as Element; expect(e.outerHtml, '
'); e = parseFragment('
').firstChild as Element; expect(e.outerHtml, '
'); }); test('Escaping non-breaking space', () { final text = 'foO\u00A0bar'; expect(text.codeUnitAt(text.indexOf('O') + 1), 0xA0); final e = parseFragment(text).firstChild as Element; expect(e.outerHtml, 'foO bar'); }); test('Newline after
', () {
      var e = parseFragment('
\n\nsome text').firstChild as Element;
      expect((e.firstChild as Text).data, '\nsome text');
      expect(e.outerHtml, '
\n\nsome text
'); e = parseFragment('
\nsome text').firstChild as Element;
      expect((e.firstChild as Text).data, 'some text');
      expect(e.outerHtml, '
some text
'); }); test('xml namespaces', () { // Note: this is a nonsensical example, but it triggers the behavior // we're looking for with attribute names in foreign content. final doc = parse(''' '''); final n = doc.querySelector('desc')!; final keys = n.attributes.keys.toList(); expect( keys.first, isA() .having((n) => n.prefix, 'prefix', 'xlink') .having((n) => n.namespace, 'namespace', 'http://www.w3.org/1999/xlink') .having((n) => n.name, 'name', 'type')); expect( n.outerHtml, ''); }); }); test('error printing without spans', () { final parser = HtmlParser('foo'); final doc = parser.parse(); expect(doc.body!.innerHtml, 'foo'); expect(parser.errors.length, 1); expect(parser.errors[0].errorCode, 'expected-doctype-but-got-chars'); expect(parser.errors[0].message, 'Unexpected non-space characters. Expected DOCTYPE.'); expect(parser.errors[0].toString(), 'Unexpected non-space characters. Expected DOCTYPE.'); }); test('Element.text', () { final doc = parseFragment('
foo
bar
baz
'); final e = doc.firstChild!; final text = e.firstChild!; expect((text as Text).data, 'foo'); expect(e.text, 'foobarbaz'); e.text = 'FOO'; expect(e.nodes.length, 1); expect(e.firstChild, isNot(text), reason: 'should create a new tree'); expect((e.firstChild as Text).data, 'FOO'); expect(e.text, 'FOO'); }); test('Text.text', () { final doc = parseFragment('
foo
bar
baz
'); final e = doc.firstChild!; final text = e.firstChild as Text; expect(text.data, 'foo'); expect(text.text, 'foo'); text.text = 'FOO'; expect(text.data, 'FOO'); expect(e.text, 'FOObarbaz'); expect(text.text, 'FOO'); }); test('Comment.text', () { final doc = parseFragment('
bar
'); final e = doc.firstChild!; final c = e.firstChild!; expect((c as Comment).data, 'foo'); expect(c.text, 'foo'); expect(e.text, 'bar'); c.text = 'qux'; expect(c.data, 'qux'); expect(c.text, 'qux'); expect(e.text, 'bar'); }); test('foreignObject end tag', () { final p = HtmlParser(''' '''); final doc = p.parseFragment(); expect(p.errors, isEmpty); final svg = doc.querySelector('svg')!; expect(svg.children[0].children[0].localName, 'x-flow'); }); group('Encoding pre-parser', () { String? getEncoding(String s) => EncodingParser(s.codeUnits).getEncoding(); test('gets encoding from meta charset', () { expect(getEncoding(''), 'utf-16'); }); test('gets encoding from meta in head', () { expect(getEncoding(''), 'utf-16'); }); test('skips comments', () { expect(getEncoding(''), 'utf-16'); }); test('stops if no match', () { // missing closing tag expect(getEncoding(''), 'utf-16'); }); test('parses content attr', () { expect( getEncoding( ''), null); }); }); } void _testElementSpans() { void assertSpan(SourceSpan span, int offset, int end, String text) { expect(span, isNotNull); expect(span.start.offset, offset); expect(span.end.offset, end); expect(span.text, text); } group('element spans', () { test('html and body', () { final text = '123'; final doc = parse(text, generateSpans: true); { final elem = doc.querySelector('html')!; assertSpan(elem.sourceSpan!, 0, 6, ''); assertSpan(elem.endSourceSpan!, 22, 29, ''); } { final elem = doc.querySelector('body')!; assertSpan(elem.sourceSpan!, 6, 12, ''); assertSpan(elem.endSourceSpan!, 15, 22, ''); } }); test('normal', () { final text = '
'; final doc = parse(text, generateSpans: true); final elem = doc.querySelector('element')!; assertSpan(elem.sourceSpan!, 5, 14, ''); assertSpan(elem.endSourceSpan!, 27, 37, ''); }); test('block', () { final text = '
123
'; final doc = parse(text, generateSpans: true); final elem = doc.querySelector('div')!; assertSpan(elem.sourceSpan!, 0, 5, '
'); assertSpan(elem.endSourceSpan!, 8, 14, '
'); }); test('form', () { final text = '
123
'; final doc = parse(text, generateSpans: true); final elem = doc.querySelector('form')!; assertSpan(elem.sourceSpan!, 0, 6, '
'); assertSpan(elem.endSourceSpan!, 9, 16, '
'); }); test('p explicit end', () { final text = '

123

'; final doc = parse(text, generateSpans: true); final elem = doc.querySelector('p')!; assertSpan(elem.sourceSpan!, 0, 3, '

'); assertSpan(elem.endSourceSpan!, 6, 10, '

'); }); test('p implicit end', () { final text = '

123

456

'; final doc = parse(text, generateSpans: true); final elem = doc.querySelector('p')!; assertSpan(elem.sourceSpan!, 5, 8, '

'); expect(elem.endSourceSpan, isNull); }); test('li', () { final text = '

  • 123
  • '; final doc = parse(text, generateSpans: true); final elem = doc.querySelector('li')!; assertSpan(elem.sourceSpan!, 0, 4, '
  • '); assertSpan(elem.endSourceSpan!, 7, 12, '
  • '); }); test('heading', () { final text = '

    123

    '; final doc = parse(text, generateSpans: true); final elem = doc.querySelector('h1')!; assertSpan(elem.sourceSpan!, 0, 4, '

    '); assertSpan(elem.endSourceSpan!, 7, 12, '

    '); }); test('formatting', () { final text = '123'; final doc = parse(text, generateSpans: true); final elem = doc.querySelector('b')!; assertSpan(elem.sourceSpan!, 0, 3, ''); assertSpan(elem.endSourceSpan!, 6, 10, ''); }); test('table tbody', () { final text = '
    '; final doc = parse(text, generateSpans: true); { final elem = doc.querySelector('tbody')!; assertSpan(elem.sourceSpan!, 7, 14, ''); assertSpan(elem.endSourceSpan!, 16, 24, ''); } }); test('table tr td', () { final text = '
    123
    '; final doc = parse(text, generateSpans: true); { final elem = doc.querySelector('table')!; assertSpan(elem.sourceSpan!, 0, 7, ''); assertSpan(elem.endSourceSpan!, 28, 36, '
    '); } { final elem = doc.querySelector('tr')!; assertSpan(elem.sourceSpan!, 7, 11, ''); assertSpan(elem.endSourceSpan!, 23, 28, ''); } { final elem = doc.querySelector('td')!; assertSpan(elem.sourceSpan!, 11, 15, ''); assertSpan(elem.endSourceSpan!, 18, 23, ''); } }); test('select optgroup option', () { final text = ''; final doc = parse(text, generateSpans: true); { final elem = doc.querySelector('select')!; assertSpan(elem.sourceSpan!, 0, 8, ''); } { final elem = doc.querySelector('optgroup')!; assertSpan(elem.sourceSpan!, 8, 18, ''); assertSpan(elem.endSourceSpan!, 38, 49, ''); } { final elem = doc.querySelector('option')!; assertSpan(elem.sourceSpan!, 18, 26, ''); } }); }); }