| // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:html'; | |
| import 'dart:typed_data'; | |
| import 'package:expect/legacy/minitest.dart'; // ignore: deprecated_member_use_from_same_package | |
| /** | |
| * Verifies that [actual] has the same graph structure as [expected]. | |
| * Detects cycles and DAG structure in Maps and Lists. | |
| */ | |
| verifyGraph(expected, actual) { | |
| var eItems = []; | |
| var aItems = []; | |
| message(path, reason) => path == '' | |
| ? reason | |
| : reason == null | |
| ? "path: $path" | |
| : "path: $path, $reason"; | |
| walk(path, expected, actual) { | |
| if (expected is String || expected is num || expected == null) { | |
| expect(actual, equals(expected), reason: message(path, 'not equal')); | |
| return; | |
| } | |
| // Cycle or DAG? | |
| for (int i = 0; i < eItems.length; i++) { | |
| if (identical(expected, eItems[i])) { | |
| expect( | |
| actual, | |
| same(aItems[i]), | |
| reason: message(path, 'missing back or side edge'), | |
| ); | |
| return; | |
| } | |
| } | |
| for (int i = 0; i < aItems.length; i++) { | |
| if (identical(actual, aItems[i])) { | |
| expect( | |
| expected, | |
| same(eItems[i]), | |
| reason: message(path, 'extra back or side edge'), | |
| ); | |
| return; | |
| } | |
| } | |
| eItems.add(expected); | |
| aItems.add(actual); | |
| if (expected is Blob) { | |
| expect(actual is Blob, isTrue, reason: '$actual is Blob'); | |
| expect( | |
| expected.type, | |
| equals(actual.type), | |
| reason: message(path, '.type'), | |
| ); | |
| expect( | |
| expected.size, | |
| equals(actual.size), | |
| reason: message(path, '.size'), | |
| ); | |
| return; | |
| } | |
| if (expected is ByteBuffer) { | |
| expect(actual is ByteBuffer, isTrue, reason: '$actual is ByteBuffer'); | |
| expect( | |
| expected.lengthInBytes, | |
| equals(actual.lengthInBytes), | |
| reason: message(path, '.lengthInBytes'), | |
| ); | |
| // TODO(antonm): one can create a view on top of those | |
| // and check if contents identical. Let's do it later. | |
| return; | |
| } | |
| if (expected is DateTime) { | |
| expect(actual is DateTime, isTrue, reason: '$actual is DateTime'); | |
| expect( | |
| expected.millisecondsSinceEpoch, | |
| equals(actual.millisecondsSinceEpoch), | |
| reason: message(path, '.millisecondsSinceEpoch'), | |
| ); | |
| return; | |
| } | |
| if (expected is ImageData) { | |
| expect(actual is ImageData, isTrue, reason: '$actual is ImageData'); | |
| expect( | |
| expected.width, | |
| equals(actual.width), | |
| reason: message(path, '.width'), | |
| ); | |
| expect( | |
| expected.height, | |
| equals(actual.height), | |
| reason: message(path, '.height'), | |
| ); | |
| walk('$path.data', expected.data, actual.data); | |
| return; | |
| } | |
| if (expected is TypedData) { | |
| expect(actual is TypedData, isTrue, reason: '$actual is TypedData'); | |
| walk('$path/.buffer', expected.buffer, actual.buffer); | |
| expect( | |
| expected.offsetInBytes, | |
| equals(actual.offsetInBytes), | |
| reason: message(path, '.offsetInBytes'), | |
| ); | |
| expect( | |
| expected.lengthInBytes, | |
| equals(actual.lengthInBytes), | |
| reason: message(path, '.lengthInBytes'), | |
| ); | |
| // And also fallback to elements check below. | |
| } | |
| if (expected is List) { | |
| expect( | |
| actual, | |
| predicate((v) => v is List), | |
| reason: message(path, '$actual is List'), | |
| ); | |
| expect( | |
| actual.length, | |
| expected.length, | |
| reason: message(path, 'different list lengths'), | |
| ); | |
| for (var i = 0; i < expected.length; i++) { | |
| walk('$path[$i]', expected[i], actual[i]); | |
| } | |
| return; | |
| } | |
| if (expected is Map) { | |
| expect( | |
| actual, | |
| predicate((v) => v is Map), | |
| reason: message(path, '$actual is Map'), | |
| ); | |
| for (var key in expected.keys) { | |
| if (!actual.containsKey(key)) { | |
| expect(false, isTrue, reason: message(path, 'missing key "$key"')); | |
| } | |
| walk('$path["$key"]', expected[key], actual[key]); | |
| } | |
| for (var key in actual.keys) { | |
| if (!expected.containsKey(key)) { | |
| expect(false, isTrue, reason: message(path, 'extra key "$key"')); | |
| } | |
| } | |
| return; | |
| } | |
| expect(false, isTrue, reason: 'Unhandled type: $expected'); | |
| } | |
| walk('', expected, actual); | |
| } | |
| /** | |
| * Sanitizer which does nothing. | |
| */ | |
| class NullTreeSanitizer implements NodeTreeSanitizer { | |
| void sanitizeTree(Node node) {} | |
| } | |
| /** | |
| * Validate that two DOM trees are equivalent. | |
| */ | |
| void validateNodeTree(Node a, Node b, [String path = '']) { | |
| path = '${path}${a.runtimeType}'; | |
| expect(a.nodeType, b.nodeType, reason: '$path nodeTypes differ'); | |
| expect(a.nodeValue, b.nodeValue, reason: '$path nodeValues differ'); | |
| expect(a.text, b.text, reason: '$path texts differ'); | |
| expect(a.nodes.length, b.nodes.length, reason: '$path nodes.lengths differ'); | |
| if (a is Element) { | |
| Element bE = b as Element; | |
| Element aE = a; | |
| expect(aE.tagName, bE.tagName, reason: '$path tagNames differ'); | |
| expect( | |
| aE.attributes.length, | |
| bE.attributes.length, | |
| reason: '$path attributes.lengths differ', | |
| ); | |
| for (var key in aE.attributes.keys) { | |
| expect( | |
| aE.attributes[key], | |
| bE.attributes[key], | |
| reason: '$path attribute [$key] values differ', | |
| ); | |
| } | |
| } | |
| for (var i = 0; i < a.nodes.length; ++i) { | |
| validateNodeTree(a.nodes[i], b.nodes[i], '$path[$i].'); | |
| } | |
| } |