blob: f4368dbc866f17aa016361f554c30f9377bf01ac [file]
// Copyright (c) project authors. All rights reserved.
// Licensed under the MIT license.
// See LICENSE file in the project root for details.
@TestOn('vm')
library;
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:html/dom.dart';
import 'package:html/parser.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
const int _testQsaBaseline = 0x01;
const String _knownFailingSubstring = 'matching custom data-* attribute with';
typedef _SelectorCase = ({
String name,
String selector,
List<String> expectedIds,
Set<String> excludedContexts,
int testType,
});
typedef _WptSelectorsSuite = ({
String html,
List<_SelectorCase> validCases,
});
_WptSelectorsSuite? _loadWptSelectorsSuite([Directory? directory]) {
final dir = directory ??
Directory.fromUri(
Isolate.resolvePackageUriSync(Uri.parse('package:html/dom.dart'))!
.resolve('../../../third_party/wpt-selectors/'),
);
if (!dir.existsSync()) {
return null;
}
final html = File(p.join(dir.path, 'level1-content.html')).readAsStringSync();
final rawJson = jsonDecode(
File(p.join(dir.path, 'selectors.json')).readAsStringSync(),
) as Map<String, Object?>;
final rawValid = rawJson['validSelectors'] as List<Object?>;
final validCases = <_SelectorCase>[
for (final entry in rawValid.cast<Map<String, Object?>>())
(
name: entry['name'] as String,
selector: entry['selector'] as String,
expectedIds: (entry['expect'] as List<Object?>).cast<String>(),
excludedContexts: {
if (entry['exclude'] case final List<Object?> excluded)
...excluded.cast<String>(),
},
testType: entry['testType'] as int,
),
];
if (validCases.isEmpty) {
throw StateError('Expected non-empty validSelectors in ${dir.path}');
}
return (html: html, validCases: validCases);
}
void main() {
test('returns null when wpt-selectors directory is absent', () {
final tempDir = Directory.systemTemp.createTempSync('wpt_selectors_test_');
addTearDown(() => tempDir.deleteSync(recursive: true));
expect(
_loadWptSelectorsSuite(Directory(p.join(tempDir.path, 'missing'))),
isNull,
);
});
final suite = _loadWptSelectorsSuite();
if (suite == null) {
return;
}
final doc = parse(suite.html);
final rootElement = doc.getElementById('root')!;
_appendNamespaceFixtures(doc, rootElement);
final outOfScope = rootElement.clone(true);
_forEachElement(outOfScope, (el) {
el.attributes['data-clone'] = '';
});
final detached = rootElement.clone(true);
final fragment = doc.createDocumentFragment()
..append(rootElement.clone(true));
final documentTarget = _DocumentTarget(doc);
final detachedTarget = _ElementTarget(detached);
final fragmentTarget = _FragmentTarget(fragment);
final elementTarget = _ElementTarget(rootElement);
_registerSpecialSelectorTests('Document', documentTarget);
_registerSpecialSelectorTests('Detached Element', detachedTarget);
_registerSpecialSelectorTests('Fragment', fragmentTarget);
_registerSpecialSelectorTests('In-document Element', elementTarget);
_registerStaticListTests('Document', doc, documentTarget);
_registerStaticListTests('Detached Element', doc, detachedTarget);
_registerStaticListTests('Fragment', doc, fragmentTarget);
_registerStaticListTests('In-document Element', doc, elementTarget);
_registerValidSelectorTests('Document', documentTarget, suite.validCases);
_registerValidSelectorTests(
'Detached Element',
detachedTarget,
suite.validCases,
);
_registerValidSelectorTests('Fragment', fragmentTarget, suite.validCases);
group('out of scope', () {
setUp(() {
doc.body!.append(outOfScope);
});
tearDown(outOfScope.remove);
_registerValidSelectorTests(
'In-document Element',
elementTarget,
suite.validCases,
);
});
}
void _appendNamespaceFixtures(Document doc, Element parent) {
parent
..append(doc.createElement('null'))
..append(doc.createElement('undefined'))
..append(_buildNamespaceGroup(doc, 'any-namespace'))
..append(_buildNamespaceGroup(doc, 'no-namespace'));
}
Element _buildNamespaceGroup(Document doc, String prefix) {
final container = doc.createElement('div')..id = prefix;
final divs = [
doc.createElement('div')..id = '$prefix-div1',
doc.createElementNS('http://www.w3.org/1999/xhtml', 'div')
..id = '$prefix-div2',
doc.createElementNS('', 'div')..attributes['id'] = '$prefix-div3',
doc.createElementNS('http://www.example.org/ns', 'div')
..attributes['id'] = '$prefix-div4',
];
for (final div in divs) {
container.append(div);
}
return container;
}
void _registerStaticListTests(
String label,
Document doc,
_QueryTarget target,
) {
late List<Element> pre;
late int preLength;
test('$label: static NodeList', () {
pre = target.querySelectorAll('div');
preLength = pre.length;
target.appendDiv(doc.createElement('div'));
expect(pre, hasLength(preLength));
});
test('$label: new NodeList', () {
final post = target.querySelectorAll('div');
expect(post, hasLength(preLength + 1));
});
}
void _registerSpecialSelectorTests(String label, _QueryTarget target) {
for (final tag in ['null', 'undefined']) {
final upper = tag.toUpperCase();
test('$label.querySelectorAll $tag', () {
expect(target.querySelectorAll(tag), hasLength(1));
});
test('$label.querySelector $tag', () {
final match = target.querySelector(tag);
expect(match, isNotNull);
expect(match!.localName?.toUpperCase(), upper);
});
}
test('$label.querySelectorAll tree order', () {
final result = target.querySelectorAll('*');
final expected = <Element>[];
_forEachElement(target.node, (el) {
if (!identical(el, target.node)) {
expected.add(el);
}
});
expect(result, expected);
});
}
void _registerValidSelectorTests(
String label,
_QueryTarget target,
List<_SelectorCase> cases,
) {
final contextKind = target.contextKind;
for (final c in cases) {
if ((c.testType & _testQsaBaseline) == 0 ||
c.excludedContexts.contains(contextKind) ||
c.excludedContexts.contains('html')) {
continue;
}
final skipReason = c.name.contains(_knownFailingSubstring)
? 'Tests related to `$_knownFailingSubstring` fail for an unknown '
'reason.'
: null;
late List<Element> foundAll;
test('$label.querySelectorAll: ${c.name}:${c.selector}', () {
foundAll = target.querySelectorAll(c.selector);
expect(foundAll, hasLength(c.expectedIds.length));
for (var i = 0; i < c.expectedIds.length; i++) {
expect(foundAll[i].attributes['id'], c.expectedIds[i]);
expect(foundAll[i].attributes.containsKey('data-clone'), isFalse);
}
}, skip: skipReason);
test('$label.querySelector: ${c.name} : ${c.selector}', () {
final found = target.querySelector(c.selector);
if (c.expectedIds.isEmpty) {
expect(found, isNull);
return;
}
expect(found, isNotNull);
expect(found!.attributes['id'], c.expectedIds.first);
expect(found, same(foundAll.first));
expect(found.attributes.containsKey('data-clone'), isFalse);
}, skip: skipReason);
}
}
void _forEachElement(Node node, void Function(Element) action) {
if (node is Element) {
action(node);
}
for (final child in node.nodes) {
_forEachElement(child, action);
}
}
sealed class _QueryTarget {
Node get node;
String get contextKind;
Element? querySelector(String selector);
List<Element> querySelectorAll(String selector);
void appendDiv(Element div);
}
final class _DocumentTarget extends _QueryTarget {
final Document _document;
_DocumentTarget(this._document);
@override
Node get node => _document;
@override
String get contextKind => 'document';
@override
Element? querySelector(String selector) => _document.querySelector(selector);
@override
List<Element> querySelectorAll(String selector) =>
_document.querySelectorAll(selector);
@override
void appendDiv(Element div) => _document.body!.append(div);
}
final class _ElementTarget extends _QueryTarget {
final Element _element;
_ElementTarget(this._element);
@override
Node get node => _element;
@override
String get contextKind =>
_element.parentNode != null ? 'element' : 'detached';
@override
Element? querySelector(String selector) => _element.querySelector(selector);
@override
List<Element> querySelectorAll(String selector) =>
_element.querySelectorAll(selector);
@override
void appendDiv(Element div) => _element.append(div);
}
final class _FragmentTarget extends _QueryTarget {
final DocumentFragment _fragment;
_FragmentTarget(this._fragment);
@override
Node get node => _fragment;
@override
String get contextKind => 'fragment';
@override
Element? querySelector(String selector) => _fragment.querySelector(selector);
@override
List<Element> querySelectorAll(String selector) =>
_fragment.querySelectorAll(selector);
@override
void appendDiv(Element div) => _fragment.append(div);
}