blob: d77aac796cef0c5117ef74d0e7928796f555dc68 [file] [log] [blame]
/// Test for the Selectors API ported from
/// <https://github.com/w3c/web-platform-tests/tree/master/selectors-api>
///
/// Note: tried to make minimal changes possible here. Hence some oddities such
/// as [runTest] arguments having a different order, long lines, etc.
///
/// As usual with ports: being faithful to the original style is more important
/// than other style goals, as it reduces friction to integrating changes
/// from upstream.
library;
import 'package:html/dom.dart';
import 'package:test/test.dart' as unittest;
late Document doc;
// Create and append special elements that cannot be created correctly with HTML
// markup alone.
void setupSpecialElements(Element parent) {
// Setup null and undefined tests
parent.append(doc.createElement('null'));
parent.append(doc.createElement('undefined'));
// Setup namespace tests
final anyNS = doc.createElement('div');
final noNS = doc.createElement('div');
anyNS.id = 'any-namespace';
noNS.id = 'no-namespace';
var div = [
doc.createElement('div'),
doc.createElementNS('http://www.w3.org/1999/xhtml', 'div'),
doc.createElementNS('', 'div'),
doc.createElementNS('http://www.example.org/ns', 'div')
];
div[0].id = 'any-namespace-div1';
div[1].id = 'any-namespace-div2';
div[2].attributes['id'] =
'any-namespace-div3'; // Non-HTML elements can't use .id property
div[3].attributes['id'] = 'any-namespace-div4';
for (var i = 0; i < div.length; i++) {
anyNS.append(div[i]);
}
div = [
doc.createElement('div'),
doc.createElementNS('http://www.w3.org/1999/xhtml', 'div'),
doc.createElementNS('', 'div'),
doc.createElementNS('http://www.example.org/ns', 'div')
];
div[0].id = 'no-namespace-div1';
div[1].id = 'no-namespace-div2';
div[2].attributes['id'] =
'no-namespace-div3'; // Non-HTML elements can't use .id property
div[3].attributes['id'] = 'no-namespace-div4';
for (var i = 0; i < div.length; i++) {
noNS.append(div[i]);
}
parent.append(anyNS);
parent.append(noNS);
}
/// Verify that the NodeList returned by querySelectorAll is static and and that
/// a new list is created after each call. A static list should not be affected
/// by subsequent changes to the DOM.
void verifyStaticList(String type, SelectorAdaptor root) {
List<Element> pre;
List<Element> post;
late int preLength;
runTest(() {
pre = root.querySelectorAll('div');
preLength = pre.length;
final div = doc.createElement('div');
root.isDocument ? root.body!.append(div) : root.append(div);
assertEquals(
pre.length, preLength, 'The length of the NodeList should not change.');
}, '$type: static NodeList');
runTest(() {
post = root.querySelectorAll('div');
assertEquals(post.length, preLength + 1,
'The length of the new NodeList should be 1 more than the previous list.');
}, '$type: new NodeList');
}
/// Verify handling of special values for the selector parameter, including
/// stringification of null and undefined, and the handling of the empty string.
void runSpecialSelectorTests(String type, SelectorAdaptor root) {
// Dart note: changed these tests because we don't have auto conversion to
// String like JavaScript does.
runTest(() {
// 1
assertEquals(root.querySelectorAll('null').length, 1,
"This should find one element with the tag name 'NULL'.");
}, '$type.querySelectorAll null');
runTest(() {
// 2
assertEquals(root.querySelectorAll('undefined').length, 1,
"This should find one element with the tag name 'UNDEFINED'.");
}, '$type.querySelectorAll undefined');
// runTest(() {
// // 3
// assertThrows((e) => e is NoSuchMethodError, () {
// root.querySelectorAll();
// }, 'This should throw a TypeError.');
// }, '$type.querySelectorAll no parameter');
runTest(() {
// 4
final elm = root.querySelector('null');
assertNotEquals(elm, null, 'This should find an element.');
// TODO(jmesserly): change "localName" back to "tagName" once implemented.
assertEquals(elm!.localName?.toUpperCase(), 'NULL',
"The tag name should be 'NULL'.");
}, '$type.querySelector null');
runTest(() {
// 5
final elm = root.querySelector('undefined');
assertNotEquals(elm, 'undefined', 'This should find an element.');
// TODO(jmesserly): change "localName" back to "tagName" once implemented.
assertEquals(elm!.localName?.toUpperCase(), 'UNDEFINED',
"The tag name should be 'UNDEFINED'.");
}, '$type.querySelector undefined');
// runTest(() {
// // 6
// assertThrows((e) => e is NoSuchMethodError, () {
// root.querySelector();
// }, 'This should throw a TypeError.');
// }, '$type.querySelector no parameter');
runTest(() {
// 7
final result = root.querySelectorAll('*');
var i = 0;
traverse(root.asNode!, (elem) {
if (!identical(elem, root.asNode)) {
assertEquals(
elem, result[i], 'The result in index $i should be in tree order.');
i++;
}
});
}, '$type.querySelectorAll tree order');
}
/// Tests containing this string fail for an unknown reason
final _failureName = 'matching custom data-* attribute with';
String? _getSkip(String name) {
if (name.contains(_failureName)) {
return 'Tests related to `$_failureName` fail for an unknown reason.';
}
return null;
}
/*
* Execute queries with the specified valid selectors for both querySelector() and querySelectorAll()
* Only run these tests when results are expected. Don't run for syntax error tests.
*/
void runValidSelectorTest(String type, SelectorAdaptor root,
List<Map<String, dynamic>> selectors, int testType, String docType) {
var nodeType = '';
switch (root.nodeType) {
case Node.DOCUMENT_NODE:
nodeType = 'document';
break;
case Node.ELEMENT_NODE:
nodeType = root.parentNode != null ? 'element' : 'detached';
break;
case Node.DOCUMENT_FRAGMENT_NODE:
nodeType = 'fragment';
break;
default:
throw StateError('Reached unreachable code path.');
}
for (var i = 0; i < selectors.length; i++) {
final s = selectors[i];
final n = s['name'] as String;
final skip = _getSkip(n);
final q = s['selector'] as String;
final e = s['expect'] as List?;
final exclude = s['exclude'];
if ((exclude is! List ||
(!exclude.contains(nodeType) && !exclude.contains(docType))) &&
((s['testType'] as int) & testType != 0)) {
//console.log("Running tests " + nodeType + ": " + s["testType"] + "&" + testType + "=" + (s["testType"] & testType) + ": " + JSON.stringify(s))
late List<Element> foundall;
Element? found;
runTest(() {
foundall = root.querySelectorAll(q);
assertNotEquals(foundall, null, 'The method should not return null.');
assertEquals(foundall.length, e!.length,
'The method should return the expected number of matches.');
for (var i = 0; i < e.length; i++) {
assertNotEquals(
foundall[i], null, 'The item in index $i should not be null.');
assertEquals(foundall[i].attributes['id'], e[i],
'The item in index $i should have the expected ID.');
assertFalse(foundall[i].attributes.containsKey('data-clone'),
'This should not be a cloned element.');
}
}, '$type.querySelectorAll: $n:$q', skip: skip);
runTest(() {
found = root.querySelector(q);
if (e!.isNotEmpty) {
assertNotEquals(found, null, 'The method should return a match.');
assertEquals(found!.attributes['id'], e[0],
'The method should return the first match.');
assertEquals(found, foundall[0],
'The result should match the first item from querySelectorAll.');
assertFalse(found!.attributes.containsKey('data-clone'),
'This should not be annotated as a cloned element.');
} else {
assertEquals(found, null, 'The method should not match anything.');
}
}, '$type.querySelector: $n : $q', skip: skip);
} else {
//console.log("Excluding for " + nodeType + ": " + s["testType"] + "&" + testType + "=" + (s["testType"] & testType) + ": " + JSON.stringify(s))
}
}
}
/*
* Execute queries with the specified invalid selectors for both querySelector() and querySelectorAll()
* Only run these tests when errors are expected. Don't run for valid selector tests.
*/
void runInvalidSelectorTest(
String type, SelectorAdaptor root, List<Map<String, dynamic>> selectors) {
for (var i = 0; i < selectors.length; i++) {
final s = selectors[i];
final n = s['name'] as String;
final q = s['selector'] as String;
// Dart note: FormatException seems a reasonable mapping of SyntaxError
runTest(() {
assertThrows((Object e) => e is FormatException, () {
root.querySelector(q);
});
}, '$type.querySelector: $n:$q');
runTest(() {
assertThrows((Object e) => e is FormatException, () {
root.querySelectorAll(q);
});
}, '$type.querySelectorAll: $n:$q');
}
}
void traverse(Node elem, void Function(Node) fn) {
if (elem.nodeType == Node.ELEMENT_NODE) {
fn(elem);
}
// Dart note: changed this since our DOM API doesn't support nextNode yet.
for (var node in elem.nodes) {
traverse(node, fn);
}
}
void runTest(dynamic Function() body, String name, {String? skip}) =>
unittest.test(name, body, skip: skip);
void assertTrue(bool value, String reason) =>
unittest.expect(value, unittest.isTrue, reason: reason);
void assertFalse(bool value, String reason) =>
unittest.expect(value, unittest.isFalse, reason: reason);
void assertEquals(dynamic x, dynamic y, String reason) =>
unittest.expect(x, y, reason: reason);
void assertNotEquals(dynamic x, dynamic y, String reason) =>
unittest.expect(x, unittest.isNot(y), reason: reason);
void assertThrows(Object exception, void Function() body, [String? reason]) =>
unittest.expect(body, unittest.throwsA(exception), reason: reason);
/// Used for testing.
///
/// This class delegates to one of three different kinds of objects. They share
/// methods with similar signatures but do not share a type hierarchy.
/// Previously these methods were invoked through `dynamic`.
class SelectorAdaptor {
final Document? document;
final Element? element;
final DocumentFragment? fragment;
SelectorAdaptor.document(this.document)
: element = null,
fragment = null;
SelectorAdaptor.element(this.element)
: document = null,
fragment = null;
SelectorAdaptor.fragment(this.fragment)
: document = null,
element = null;
bool get isDocument => document != null;
Element? get body => document?.body;
Node? get asNode => document ?? element ?? fragment;
int get nodeType => asNode!.nodeType;
Node? get parentNode => asNode!.parentNode;
void append(Node node) {
asNode!.append(node);
}
Element? querySelector(String selector) {
if (document != null) return document!.querySelector(selector);
if (element != null) return element!.querySelector(selector);
if (fragment != null) return fragment!.querySelector(selector);
throw StateError('unsupported');
}
List<Element> querySelectorAll(String selector) {
if (document != null) return document!.querySelectorAll(selector);
if (element != null) return element!.querySelectorAll(selector);
if (fragment != null) return fragment!.querySelectorAll(selector);
throw StateError('unsupported');
}
}