diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index 8dfa57b2a9420..465d66703a631 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -597,9 +597,9 @@ extension DomElementExtension on DomElement { external set _id(JSString id); set id(String id) => _id = id.toJS; - @JS('innerHtml') - external set _innerHtml(JSString? html); - set innerHtml(String? html) => _innerHtml = html?.toJS; + @JS('innerHTML') + external set _innerHTML(JSString? html); + set innerHTML(String? html) => _innerHTML = html?.toJS; @JS('outerHTML') external JSString? get _outerHTML; diff --git a/lib/web_ui/test/common/matchers.dart b/lib/web_ui/test/common/matchers.dart index 51698843d66a8..a94f5b480ba59 100644 --- a/lib/web_ui/test/common/matchers.dart +++ b/lib/web_ui/test/common/matchers.dart @@ -7,8 +7,8 @@ library matchers; import 'dart:math' as math; -import 'package:html/dom.dart' as html_package; -import 'package:html/parser.dart' as html_package; +import 'package:html/dom.dart' as html; +import 'package:html/parser.dart' as html; import 'package:test/test.dart'; @@ -198,259 +198,244 @@ class _IsWithinDistance extends Matcher { } } -/// Controls how test HTML is canonicalized by [canonicalizeHtml] function. +/// A matcher for functions that throw [AssertionError]. /// -/// In all cases whitespace between elements is stripped. -enum HtmlComparisonMode { - /// Retains all attributes. - /// - /// Useful when very precise HTML comparison is needed that includes both - /// layout and non-layout style attributes. This mode is rarely needed. Most - /// tests should use [layoutOnly] or [nonLayoutOnly]. - everything, - - /// Retains only layout style attributes, such as "width". - /// - /// Useful when testing layout because it filters out all the noise that does - /// not affect layout. - layoutOnly, - - /// Retains only non-layout style attributes, such as "color". - /// - /// Useful when testing styling because it filters out all the noise from the - /// layout attributes. - nonLayoutOnly, - - /// Do not consider attributes when comparing HTML. - noAttributes, -} +/// This is equivalent to `throwsA(isInstanceOf())`. +/// +/// If you are trying to test whether a call to [WidgetTester.pumpWidget] +/// results in an [AssertionError], see +/// [TestWidgetsFlutterBinding.takeException]. +/// +/// See also: +/// +/// * [throwsFlutterError], to test if a function throws a [FlutterError]. +/// * [throwsArgumentError], to test if a functions throws an [ArgumentError]. +/// * [isAssertionError], to test if any object is any kind of [AssertionError]. +final Matcher throwsAssertionError = throwsA(isAssertionError); -/// Rewrites [htmlContent] by removing irrelevant style attributes. +/// A matcher for [AssertionError]. /// -/// If [throwOnUnusedStyleProperties] is `true`, throws instead of rewriting. Set -/// [throwOnUnusedStyleProperties] to `true` to check that expected HTML strings do -/// not contain irrelevant attributes. It is ok for actual HTML to contain all -/// kinds of attributes. They only need to be filtered out before testing. -String canonicalizeHtml( - String htmlContent, { - HtmlComparisonMode mode = HtmlComparisonMode.nonLayoutOnly, - bool throwOnUnusedStyleProperties = false, - List? ignoredStyleProperties, -}) { - if (htmlContent.trim().isEmpty) { - return ''; +/// This is equivalent to `isInstanceOf()`. +/// +/// See also: +/// +/// * [throwsAssertionError], to test if a function throws any [AssertionError]. +/// * [isFlutterError], to test if any object is a [FlutterError]. +const Matcher isAssertionError = TypeMatcher(); + +/// Matches a [DomElement] against an HTML pattern. +/// +/// An HTML pattern is a piece of valid HTML. The expectation is that the DOM +/// element has the exact element structure as the provided [htmlPattern]. The +/// DOM element is expected to have the exact element and style attributes +/// specified in the pattern. +/// +/// The DOM element may have additional attributes not specified in the pattern. +/// This allows testing specific features relevant to the test. +/// +/// The DOM structure may not have additional elements that are not specified in +/// the pattern. +Matcher hasHtml(String htmlPattern) { + final html.DocumentFragment originalDom = html.parseFragment(htmlPattern); + if (originalDom.children.isEmpty) { + fail( + 'Test HTML pattern is empty.\n' + 'The pattern must contain exacly one top-level element, but was: $htmlPattern'); + } + if (originalDom.children.length > 1) { + fail( + 'Test HTML pattern has more than one top-level element.\n' + 'The pattern must contain exacly one top-level element, but was: $htmlPattern'); } + return HtmlPatternMatcher(originalDom.children.single); +} + +enum _Breadcrumb { root, element, attribute, styleProperty } + +class _Breadcrumbs { + const _Breadcrumbs._(this.parent, this.kind, this.name); + + final _Breadcrumbs? parent; + final _Breadcrumb kind; + final String name; + + static const _Breadcrumbs root = _Breadcrumbs._(null, _Breadcrumb.root, ''); + + _Breadcrumbs element(String tagName) { + return _Breadcrumbs._(this, _Breadcrumb.element, tagName); + } + + _Breadcrumbs attribute(String attributeName) { + return _Breadcrumbs._(this, _Breadcrumb.attribute, attributeName); + } + + _Breadcrumbs styleProperty(String propertyName) { + return _Breadcrumbs._(this, _Breadcrumb.styleProperty, propertyName); + } + + @override + String toString() { + return switch (kind) { + _Breadcrumb.root => '', + _Breadcrumb.element => parent!.kind == _Breadcrumb.root ? '@$name' : '$parent > $name', + _Breadcrumb.attribute => '$parent#$name', + _Breadcrumb.styleProperty => '$parent#style($name)', + }; + } +} + +class HtmlPatternMatcher extends Matcher { + const HtmlPatternMatcher(this.pattern); - String? unusedStyleProperty(String name) { - if (throwOnUnusedStyleProperties) { - fail('Provided HTML contains style property "$name" which ' - 'is not used for comparison in the test. The HTML was:\n\n$htmlContent'); + final html.Element pattern; + + @override + bool matches(final Object? object, Map matchState) { + if (object is! DomElement) { + return false; } - return null; + final List mismatches = []; + matchState['mismatches'] = mismatches; + + final html.Element element = html.parseFragment(object.outerHTML).children.single; + matchElements(_Breadcrumbs.root, mismatches, element, pattern); + return mismatches.isEmpty; } - html_package.Element cleanup(html_package.Element original) { - String replacementTag = original.localName!; - switch (replacementTag) { - case 'flt-scene': - replacementTag = 's'; - case 'flt-transform': - replacementTag = 't'; - case 'flt-opacity': - replacementTag = 'o'; - case 'flt-clip': - final String? clipType = original.attributes['clip-type']; - switch (clipType) { - case 'rect': - replacementTag = 'clip'; - case 'rrect': - replacementTag = 'rclip'; - case 'physical-shape': - replacementTag = 'pshape'; - default: - throw Exception('Unknown clip type: $clipType'); - } - case 'flt-clip-interior': - replacementTag = 'clip-i'; - case 'flt-picture': - replacementTag = 'pic'; - case 'flt-canvas': - replacementTag = 'c'; - case 'flt-dom-canvas': - replacementTag = 'd'; - case 'flt-semantics': - replacementTag = 'sem'; - case 'flt-semantics-container': - replacementTag = 'sem-c'; - case 'flt-semantics-img': - replacementTag = 'sem-img'; - case 'flt-semantics-text-field': - replacementTag = 'sem-tf'; + static bool _areTagsEqual(html.Element a, html.Element b) { + const Map synonyms = { + 'sem': 'flt-semantics', + 'sem-c': 'flt-semantics-container', + 'sem-img': 'flt-semantics-img', + 'sem-tf': 'flt-semantics-text-field', + }; + + String aName = a.localName!.toLowerCase(); + String bName = b.localName!.toLowerCase(); + + if (synonyms.containsKey(aName)) { + aName = synonyms[aName]!; } - final html_package.Element replacement = - html_package.Element.tag(replacementTag); - - if (mode != HtmlComparisonMode.noAttributes) { - // Sort the attributes so tests are not sensitive to their order, which - // does not matter in terms of functionality. - final List attributeNames = original.attributes.keys.cast().toList(); - attributeNames.sort(); - for (final String name in attributeNames) { - final String value = original.attributes[name]!; - if (name == 'style') { - // The style attribute is handled separately because it contains substructure. - continue; - } + if (synonyms.containsKey(bName)) { + bName = synonyms[bName]!; + } - // These are the only attributes we're interested in testing. This list - // can change over time. - if (name.startsWith('aria-') || name.startsWith('flt-') || name == 'role') { - replacement.attributes[name] = value; - } - } + return aName == bName; + } - if (original.attributes.containsKey('style')) { - final String styleValue = original.attributes['style']!; - - int attrCount = 0; - final String processedAttributes = styleValue - .split(';') - .map((String attr) { - attr = attr.trim(); - if (attr.isEmpty) { - return null; - } - - if (mode != HtmlComparisonMode.everything) { - final bool forLayout = mode == HtmlComparisonMode.layoutOnly; - final List parts = attr.split(':'); - if (parts.length == 2) { - final String name = parts.first; - - if (ignoredStyleProperties != null && ignoredStyleProperties.contains(name)) { - return null; - } - - // Whether the attribute is one that's set to the same value and - // never changes. Such attributes are usually not interesting to - // test. - final bool isStaticAttribute = const [ - 'top', - 'left', - 'position', - ].contains(name); - - if (isStaticAttribute) { - return unusedStyleProperty(name); - } - - // Whether the attribute is set by the layout system. - final bool isLayoutAttribute = const [ - 'top', - 'left', - 'bottom', - 'right', - 'position', - 'width', - 'height', - 'font-size', - 'transform', - 'transform-origin', - 'white-space', - ].contains(name); - - if (forLayout && !isLayoutAttribute || - !forLayout && isLayoutAttribute) { - return unusedStyleProperty(name); - } - } - } - - attrCount++; - return attr.trim(); - }) - .where((String? attr) => attr != null && attr.isNotEmpty) - .join('; '); - - if (attrCount > 0) { - replacement.attributes['style'] = processedAttributes; - } - } - } else if (throwOnUnusedStyleProperties && original.attributes.isNotEmpty) { - fail('Provided HTML contains attributes. However, the comparison mode ' - 'is $mode. The HTML was:\n\n$htmlContent'); + void matchElements(_Breadcrumbs parent, List mismatches, html.Element element, html.Element pattern) { + final _Breadcrumbs breadcrumb = parent.element(pattern.localName!); + + if (!_areTagsEqual(element, pattern)) { + mismatches.add( + '$breadcrumb: unexpected tag name <${element.localName}> (expected <${pattern.localName}>).' + ); + // Don't bother matching anything else. If tags are different, it's likely + // we're comparing apples to oranges at this point. + return; } - for (final html_package.Node child in original.nodes) { - if (child is html_package.Text && child.text.trim().isEmpty) { - continue; - } + matchAttributes(breadcrumb, mismatches, element, pattern); + matchChildren(breadcrumb, mismatches, element, pattern); + } + + void matchAttributes(_Breadcrumbs parent, List mismatches, html.Element element, html.Element pattern) { + for (final MapEntry attribute in pattern.attributes.entries) { + final String expectedName = attribute.key as String; + final String expectedValue = attribute.value; + final _Breadcrumbs breadcrumb = parent.attribute(expectedName); - if (child is html_package.Element) { - replacement.append(cleanup(child)); + if (expectedName == 'style') { + // Style is a complex attribute that deserves a special comparison algorithm. + matchStyle(parent, mismatches, element, pattern); } else { - replacement.append(child.clone(true)); + if (!element.attributes.containsKey(expectedName)) { + mismatches.add('$breadcrumb: attribute $expectedName="$expectedValue" missing.'); + } else { + final String? actualValue = element.attributes[expectedName]; + if (actualValue != expectedValue) { + mismatches.add( + '$breadcrumb: expected attribute value $expectedName="$expectedValue", ' + 'but found $expectedName="$actualValue".' + ); + } + } } } - - return replacement; } - final html_package.DocumentFragment originalDom = - html_package.parseFragment(htmlContent); + static Map parseStyle(html.Element element) { + final Map result = {}; - final html_package.DocumentFragment cleanDom = - html_package.DocumentFragment(); - for (final html_package.Element child in originalDom.children) { - cleanDom.append(cleanup(child)); - } + final String rawStyle = element.attributes['style']!; + for (final String attribute in rawStyle.split(';')) { + final List parts = attribute.split(':'); + final String name = parts[0].trim(); + final String value = parts.skip(1).join(':').trim(); + result[name] = value; + } - return cleanDom.outerHtml; -} + return result; + } -/// Tests that [element] has the HTML structure described by [expectedHtml]. -void expectHtml(DomElement element, String expectedHtml, - {HtmlComparisonMode mode = HtmlComparisonMode.nonLayoutOnly}) { - expectedHtml = - canonicalizeHtml(expectedHtml, mode: mode, throwOnUnusedStyleProperties: true); - final String actualHtml = canonicalizeHtml(element.outerHTML!, mode: mode); - expect(actualHtml, expectedHtml); -} + void matchStyle(_Breadcrumbs parent, List mismatches, html.Element element, html.Element pattern) { + final Map expected = parseStyle(pattern); + final Map actual = parseStyle(element); + for (final MapEntry entry in expected.entries) { + final _Breadcrumbs breadcrumb = parent.styleProperty(entry.key); + if (!actual.containsKey(entry.key)) { + mismatches.add( + '$breadcrumb: style property ${entry.key}="${entry.value}" missing.' + ); + } else if (actual[entry.key] != entry.value) { + mismatches.add( + '$breadcrumb: expected style property ${entry.key}="${entry.value}", ' + 'but found ${entry.key}="${actual[entry.key]}".' + ); + } + } + } -class SceneTester { - SceneTester(this.scene); + void matchChildren(_Breadcrumbs parent, List mismatches, html.Element element, html.Element pattern) { + if (element.children.length != pattern.children.length) { + mismatches.add( + '$parent: expected ${pattern.children.length} children, but found ${element.children.length}.' + ); + return; + } - final SurfaceScene scene; + for (int i = 0; i < pattern.children.length; i++) { + final html.Element expectedChild = pattern.children[i]; + final html.Element actualChild = element.children[i]; + matchElements(parent, mismatches, actualChild, expectedChild); + } + } - void expectSceneHtml(String expectedHtml) { - expectHtml(scene.webOnlyRootElement!, expectedHtml, - mode: HtmlComparisonMode.noAttributes); + @override + Description describe(Description description) { + description.add('the element to have the following pattern:\n'); + description.add(pattern.outerHtml); + return description; } -} -/// A matcher for functions that throw [AssertionError]. -/// -/// This is equivalent to `throwsA(isInstanceOf())`. -/// -/// If you are trying to test whether a call to [WidgetTester.pumpWidget] -/// results in an [AssertionError], see -/// [TestWidgetsFlutterBinding.takeException]. -/// -/// See also: -/// -/// * [throwsFlutterError], to test if a function throws a [FlutterError]. -/// * [throwsArgumentError], to test if a functions throws an [ArgumentError]. -/// * [isAssertionError], to test if any object is any kind of [AssertionError]. -final Matcher throwsAssertionError = throwsA(isAssertionError); + @override + Description describeMismatch( + Object? object, + Description mismatchDescription, + Map matchState, + bool verbose, + ) { + mismatchDescription.add('The following DOM structure did not match the expected pattern:\n'); + mismatchDescription.add('${(object! as DomElement).outerHTML!}\n\n'); + mismatchDescription.add('Specifically:\n'); -/// A matcher for [AssertionError]. -/// -/// This is equivalent to `isInstanceOf()`. -/// -/// See also: -/// -/// * [throwsAssertionError], to test if a function throws any [AssertionError]. -/// * [isFlutterError], to test if any object is a [FlutterError]. -const Matcher isAssertionError = TypeMatcher(); + final List mismatches = matchState['mismatches']! as List; + for (final String mismatch in mismatches) { + mismatchDescription.add(' - $mismatch\n'); + } + + return mismatchDescription; + } +} diff --git a/lib/web_ui/test/engine/matchers_test.dart b/lib/web_ui/test/engine/matchers_test.dart new file mode 100644 index 0000000000000..4c2ce65ac6e8b --- /dev/null +++ b/lib/web_ui/test/engine/matchers_test.dart @@ -0,0 +1,322 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@TestOn('chrome || safari || firefox') +library; + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine/dom.dart'; + +import '../common/matchers.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + group('expectDom', () { + _expectDomTests(); + }); +} + +void _expectDomTests() { + test('trivial equal elements', () { + expectDom( + '
', + hasHtml('
'), + ); + }); + + test('trivial unequal elements', () { + expectDom( + '
', + expectMismatch( + hasHtml(''), + ''' +The following DOM structure did not match the expected pattern: +
+ +Specifically: + - @span: unexpected tag name
(expected ).''', + ), + ); + }); + + test('trivial equal attributes', () { + expectDom( + '
', + hasHtml('
'), + ); + }); + + test('trivial out-of-order equal attributes', () { + expectDom( + '
', + hasHtml('
'), + ); + }); + + test('trivial unequal attributes', () { + expectDom( + '
', + expectMismatch( + hasHtml('
'), + ''' +The following DOM structure did not match the expected pattern: +
+ +Specifically: + - @div#id: expected attribute value id="world", but found id="hello".''', + ), + ); + }); + + test('trivial missing attributes', () { + expectDom( + '
', + expectMismatch( + hasHtml('
'), + ''' +The following DOM structure did not match the expected pattern: +
+ +Specifically: + - @div#id: attribute id="hello" missing.''', + ), + ); + }); + + test('trivial additional attributes', () { + expectDom( + '
', + hasHtml('
'), + ); + + expectDom( + '
', + hasHtml('
'), + ); + }); + + test('trivial equal style', () { + expectDom( + '
', + hasHtml('
'), + ); + }); + + test('trivial additional style attribute', () { + expectDom( + '
', + hasHtml('
'), + ); + }); + + test('out of order equal style', () { + expectDom( + '
', + hasHtml('
'), + ); + }); + + test('trivial unequal style attributes', () { + expectDom( + '
', + expectMismatch( + hasHtml('
'), + ''' +The following DOM structure did not match the expected pattern: +
+ +Specifically: + - @div#style(width): expected style property width="12px", but found width="10px".''', + ), + ); + }); + + test('trivial missing style attribute', () { + expectDom( + '
', + expectMismatch( + hasHtml('
'), + ''' +The following DOM structure did not match the expected pattern: +
+ +Specifically: + - @div#style(height): style property height="20px" missing.''', + ), + ); + }); + + test('multiple attribute mismatches', () { + expectDom( + '
', + expectMismatch( + hasHtml('
'), + ''' +The following DOM structure did not match the expected pattern: +
+ +Specifically: + - @div#id: expected attribute value id="this", but found id="other". + - @div#foo: attribute foo="bar" missing. + - @div#style(height): style property height="20px" missing.''', + ), + ); + }); + + test('trivial child elements', () { + expectDom( + '

', + hasHtml('

'), + ); + }); + + test('trivial nested child elements', () { + expectDom( + '

', + hasHtml('

'), + ); + }); + + test('missing child elements', () { + expectDom( + '

', + expectMismatch( + hasHtml('

'), + ''' +The following DOM structure did not match the expected pattern: +

+ +Specifically: + - @div: expected 3 children, but found 2.''', + ), + ); + }); + + test('additional child elements', () { + expectDom( + '

', + expectMismatch( + hasHtml('

'), + ''' +The following DOM structure did not match the expected pattern: +

+ +Specifically: + - @div: expected 2 children, but found 3.''', + ), + ); + }); + + test('deep breadcrumbs', () { + expectDom( + '', + expectMismatch( + hasHtml(''), + ''' +The following DOM structure did not match the expected pattern: + + +Specifically: + - @a > b > c > d#style(width): expected style property width="2px", but found width="1px".''', + ), + ); + }); +} + +void expectDom(String domHtml, Matcher matcher) { + final DomElement root = createDomElement('div'); + root.innerHTML = domHtml; + expect(root.children.single, matcher); +} + +Matcher expectMismatch(Matcher matcher, String expectedMismatchDescription) { + return _ExpectMismatch(matcher, expectedMismatchDescription); +} + +class _ExpectMismatch extends Matcher { + const _ExpectMismatch(this._matcher, this.expectedMismatchDescription); + + final Matcher _matcher; + final String expectedMismatchDescription; + + @override + bool matches(Object? item, Map matchState) { + if (_matcher.matches(item, matchState)) { + matchState['matched'] = true; + return false; + } + + final _TestDescription description = _TestDescription(); + _matcher.describeMismatch( + item, + description, + matchState, + false, + ); + final String mismatchDescription = description.items.join(); + + if (mismatchDescription.trim() != expectedMismatchDescription.trim()) { + matchState['mismatchDescription'] = mismatchDescription; + matchState['expectedMismatchDescription'] = expectedMismatchDescription; + return false; + } + + return true; + } + + @override + Description describe(Description description) => + description.add('not ').addDescriptionOf(_matcher); + + @override + Description describeMismatch( + Object? object, + Description mismatchDescription, + Map matchState, + bool verbose, + ) { + if (matchState.containsKey('matched')) { + mismatchDescription.add('Expected a mismatch, but the HTML pattern matched.'); + } + if (matchState.containsKey('mismatchDescription')) { + mismatchDescription.add('Mismatch description was wrong.\n'); + mismatchDescription.add(' Expected: ${matchState['expectedMismatchDescription']}\n'); + mismatchDescription.add(' Actual : ${matchState['mismatchDescription']}\n'); + } + + return mismatchDescription; + } +} + +class _TestDescription implements Description { + final List items = []; + + @override + int get length => items.length; + + @override + Description add(String text) { + items.add(text); + return this; + } + + @override + Description addAll(String start, String separator, String end, Iterable list) { + throw UnimplementedError(); + } + + @override + Description addDescriptionOf(Object? value) { + throw UnimplementedError(); + } + + @override + Description replace(String text) { + throw UnimplementedError(); + } +} diff --git a/lib/web_ui/test/engine/semantics/semantics_test.dart b/lib/web_ui/test/engine/semantics/semantics_test.dart index d48ec5f37852e..d07cc1d4474ef 100644 --- a/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -1768,12 +1768,24 @@ void _testTextField() { ); owner().updateSemantics(builder.build()); + expectSemanticsTree(owner(), ''' - + '''); + final SemanticsObject node = owner().debugSemanticsTree![0]!; + + // TODO(yjbanov): this used to attempt to test that value="hello" but the + // test was a false positive. We should revise this test and + // make sure it tests the right things: + // https://github.com/flutter/flutter/issues/147200 + expect( + (node.element as DomHTMLInputElement).value, + isNull, + ); + expect(node.primaryRole?.role, PrimaryRole.textField); expect( reason: 'Text fields use custom focus management', diff --git a/lib/web_ui/test/engine/semantics/semantics_tester.dart b/lib/web_ui/test/engine/semantics/semantics_tester.dart index ff49a461e7ef1..6c2fcdcb6ccc6 100644 --- a/lib/web_ui/test/engine/semantics/semantics_tester.dart +++ b/lib/web_ui/test/engine/semantics/semantics_tester.dart @@ -347,10 +347,9 @@ class SemanticsTester { /// Verifies the HTML structure of the current semantics tree. void expectSemanticsTree(EngineSemanticsOwner owner, String semanticsHtml) { - const List ignoredStyleProperties = ['pointer-events']; expect( - canonicalizeHtml(owner.semanticsHost.querySelector('flt-semantics')!.outerHTML!, ignoredStyleProperties: ignoredStyleProperties), - canonicalizeHtml(semanticsHtml), + owner.semanticsHost.querySelector('flt-semantics'), + hasHtml(semanticsHtml), ); } diff --git a/lib/web_ui/test/engine/semantics/text_field_test.dart b/lib/web_ui/test/engine/semantics/text_field_test.dart index 52a41b1ae197d..37777769e67ea 100644 --- a/lib/web_ui/test/engine/semantics/text_field_test.dart +++ b/lib/web_ui/test/engine/semantics/text_field_test.dart @@ -93,8 +93,18 @@ void testMain() { expectSemanticsTree(owner(), ''' - + '''); + + // TODO(yjbanov): this used to attempt to test that value="hello" but the + // test was a false positive. We should revise this test and + // make sure it tests the right things: + // https://github.com/flutter/flutter/issues/147200 + final SemanticsObject node = owner().debugSemanticsTree![0]!; + expect( + (node.element as DomHTMLInputElement).value, + isNull, + ); }); // TODO(yjbanov): this test will need to be adjusted for Safari when we add diff --git a/lib/web_ui/test/engine/surface/scene_builder_test.dart b/lib/web_ui/test/engine/surface/scene_builder_test.dart index 494c4019a56aa..53806432a3cdc 100644 --- a/lib/web_ui/test/engine/surface/scene_builder_test.dart +++ b/lib/web_ui/test/engine/surface/scene_builder_test.dart @@ -33,7 +33,7 @@ void testMain() { testLayerLifeCycle((ui.SceneBuilder sceneBuilder, ui.EngineLayer? oldLayer) { return sceneBuilder.pushOffset(10, 20, oldLayer: oldLayer as ui.OffsetEngineLayer?); }, () { - return ''''''; + return ''''''; }); }); @@ -42,7 +42,7 @@ void testMain() { return sceneBuilder.pushTransform( (Matrix4.identity()..scale(EngineFlutterDisplay.instance.browserDevicePixelRatio)).toFloat64()); }, () { - return ''''''; + return ''''''; }); }); @@ -52,9 +52,9 @@ void testMain() { oldLayer: oldLayer as ui.ClipRectEngineLayer?); }, () { return ''' - - - + + + '''; }); }); @@ -67,9 +67,11 @@ void testMain() { clipBehavior: ui.Clip.none); }, () { return ''' - - - + + + + + '''; }); }); @@ -80,11 +82,11 @@ void testMain() { return sceneBuilder.pushClipPath(path, oldLayer: oldLayer as ui.ClipPathEngineLayer?); }, () { return ''' - + - + '''; }); }); @@ -93,7 +95,7 @@ void testMain() { testLayerLifeCycle((ui.SceneBuilder sceneBuilder, ui.EngineLayer? oldLayer) { return sceneBuilder.pushOpacity(10, oldLayer: oldLayer as ui.OpacityEngineLayer?); }, () { - return ''''''; + return ''''''; }); }); test('pushBackdropFilter implements surface lifecycle', () { @@ -103,10 +105,13 @@ void testMain() { oldLayer: oldLayer as ui.BackdropFilterEngineLayer?, ); }, () { - return '' - '' - '' - ''; + return ''' + + + + + +'''; }); }); }); @@ -733,7 +738,7 @@ void testLayerLifeCycle( // Recycle: discards all the layers. sceneBuilder = SurfaceSceneBuilder(); tester = SceneTester(sceneBuilder.build()); - tester.expectSceneHtml(''); + tester.expectSceneHtml(''); expect(surface3.rootElement, isNull); // offset3 should be recycled. @@ -916,3 +921,16 @@ HtmlImage createTestImage({int width = 100, int height = 50}) { imageElement.src = js_util.callMethod(canvas, 'toDataURL', []); return HtmlImage(imageElement, width, height); } + +class SceneTester { + SceneTester(this.scene); + + final SurfaceScene scene; + + void expectSceneHtml(String expectedHtml) { + expect( + scene.webOnlyRootElement, + hasHtml(expectedHtml), + ); + } +}