blob: 59ccd3ca56b8403794764235bb69dc9fb81fb58f [file] [log] [blame]
// 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 'dart:typed_data';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart' hide window;
import 'package:ui/ui.dart' as ui;
import 'semantics_tester.dart';
final InputConfiguration singlelineConfig = InputConfiguration();
final InputConfiguration multilineConfig = InputConfiguration(
inputType: EngineInputType.multiline,
inputAction: 'TextInputAction.newline',
);
EngineSemanticsOwner semantics() => EngineSemanticsOwner.instance;
const MethodCodec codec = JSONMethodCodec();
DateTime _testTime = DateTime(2021, 4, 16);
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
ensureFlutterViewEmbedderInitialized();
setUp(() {
EngineSemanticsOwner.debugResetSemantics();
});
group('$SemanticsTextEditingStrategy pre-initialization tests', () {
setUp(() {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
});
tearDown(() {
semantics().semanticsEnabled = false;
});
test('Calling dispose() pre-initialization will not throw an error', () {
final SemanticsObject textFieldSemantics = createTextFieldSemantics(
value: 'hi',
isFocused: true,
);
final TextField textField =
textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField;
// ensureInitialized() isn't called prior to calling dispose() here.
// Since we are conditionally calling dispose() on our
// SemanticsTextEditingStrategy._instance, we shouldn't expect an error.
// ref: https://github.com/flutter/engine/pull/40146
expect(() => textField.dispose(), returnsNormally);
});
});
group('$SemanticsTextEditingStrategy', () {
late HybridTextEditing testTextEditing;
late SemanticsTextEditingStrategy strategy;
setUp(() {
testTextEditing = HybridTextEditing();
SemanticsTextEditingStrategy.ensureInitialized(testTextEditing);
strategy = SemanticsTextEditingStrategy.instance;
testTextEditing.debugTextEditingStrategyOverride = strategy;
testTextEditing.configuration = singlelineConfig;
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
});
tearDown(() {
semantics().semanticsEnabled = false;
});
test('renders a text field', () {
createTextFieldSemantics(value: 'hello');
expectSemanticsTree('''
<sem style="$rootSemanticStyle">
<input value="hello" />
</sem>''');
});
// TODO(yjbanov): this test will need to be adjusted for Safari when we add
// Safari testing.
test('sends a tap action when browser requests focus', () async {
final SemanticsActionLogger logger = SemanticsActionLogger();
createTextFieldSemantics(value: 'hello');
final DomElement textField = appHostNode
.querySelector('input[data-semantics-role="text-field"]')!;
expect(appHostNode.ownerDocument?.activeElement, isNot(textField));
textField.focus();
expect(appHostNode.ownerDocument?.activeElement, textField);
expect(await logger.idLog.first, 0);
expect(await logger.actionLog.first, ui.SemanticsAction.tap);
}, // TODO(yjbanov): https://github.com/flutter/flutter/issues/46638
// TODO(yjbanov): https://github.com/flutter/flutter/issues/50590
// TODO(yjbanov): https://github.com/flutter/flutter/issues/50754
skip: browserEngine != BrowserEngine.blink);
test('Syncs semantic state from framework', () {
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
int changeCount = 0;
int actionCount = 0;
strategy.enable(
singlelineConfig,
onChange: (_, __) {
changeCount++;
},
onAction: (_) {
actionCount++;
},
);
// Create
final SemanticsObject textFieldSemantics = createTextFieldSemantics(
value: 'hello',
label: 'greeting',
isFocused: true,
rect: const ui.Rect.fromLTWH(0, 0, 10, 15),
);
final TextField textField =
textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField;
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
expect(textField.editableElement, strategy.domElement);
expect(textField.activeEditableElement.getAttribute('aria-label'), 'greeting');
expect(textField.activeEditableElement.style.width, '10px');
expect(textField.activeEditableElement.style.height, '15px');
// Update
createTextFieldSemantics(
value: 'bye',
label: 'farewell',
rect: const ui.Rect.fromLTWH(0, 0, 12, 17),
);
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
expect(strategy.domElement, null);
expect(textField.activeEditableElement.getAttribute('aria-label'), 'farewell');
expect(textField.activeEditableElement.style.width, '12px');
expect(textField.activeEditableElement.style.height, '17px');
strategy.disable();
// There was no user interaction with the <input> element,
// so we should expect no engine-to-framework feedback.
expect(changeCount, 0);
expect(actionCount, 0);
});
test(
'Does not overwrite text value and selection editing state on semantic updates',
() {
strategy.enable(
singlelineConfig,
onChange: (_, __) {},
onAction: (_) {},
);
final SemanticsObject textFieldSemantics = createTextFieldSemantics(
value: 'hello',
textSelectionBase: 1,
textSelectionExtent: 3,
isFocused: true,
rect: const ui.Rect.fromLTWH(0, 0, 10, 15));
final TextField textField =
textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField;
final DomHTMLInputElement editableElement =
textField.activeEditableElement as DomHTMLInputElement;
expect(editableElement, strategy.domElement);
expect(editableElement.value, '');
expect(editableElement.selectionStart, 0);
expect(editableElement.selectionEnd, 0);
strategy.disable();
});
test(
'Updates editing state when receiving framework messages from the text input channel',
() {
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
strategy.enable(
singlelineConfig,
onChange: (_, __) {},
onAction: (_) {},
);
final SemanticsObject textFieldSemantics = createTextFieldSemantics(
value: 'hello',
textSelectionBase: 1,
textSelectionExtent: 3,
isFocused: true,
rect: const ui.Rect.fromLTWH(0, 0, 10, 15));
final TextField textField =
textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField;
final DomHTMLInputElement editableElement =
textField.activeEditableElement as DomHTMLInputElement;
// No updates expected on semantic updates
expect(editableElement, strategy.domElement);
expect(editableElement.value, '');
expect(editableElement.selectionStart, 0);
expect(editableElement.selectionEnd, 0);
// Update from framework
const MethodCall setEditingState =
MethodCall('TextInput.setEditingState', <String, dynamic>{
'text': 'updated',
'selectionBase': 2,
'selectionExtent': 3,
});
sendFrameworkMessage(codec.encodeMethodCall(setEditingState), testTextEditing);
// Editing state should now be updated
expect(editableElement.value, 'updated');
expect(editableElement.selectionStart, 2);
expect(editableElement.selectionEnd, 3);
strategy.disable();
});
test('Gives up focus after DOM blur', () {
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
strategy.enable(
singlelineConfig,
onChange: (_, __) {},
onAction: (_) {},
);
final SemanticsObject textFieldSemantics = createTextFieldSemantics(
value: 'hello',
isFocused: true,
);
final TextField textField =
textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField;
expect(textField.editableElement, strategy.domElement);
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
// The input should not refocus after blur.
textField.activeEditableElement.blur();
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
strategy.disable();
});
test('Does not dispose and recreate dom elements in persistent mode', () {
strategy.enable(
singlelineConfig,
onChange: (_, __) {},
onAction: (_) {},
);
// It doesn't create a new DOM element.
expect(strategy.domElement, isNull);
// During the semantics update the DOM element is created and is focused on.
final SemanticsObject textFieldSemantics = createTextFieldSemantics(
value: 'hello',
isFocused: true,
);
expect(strategy.domElement, isNotNull);
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
strategy.disable();
expect(strategy.domElement, isNull);
// It doesn't remove the DOM element.
final TextField textField =
textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField;
expect(appHostNode.contains(textField.editableElement), isTrue);
// Editing element is not enabled.
expect(strategy.isEnabled, isFalse);
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
});
test('Refocuses when setting editing state', () {
strategy.enable(
singlelineConfig,
onChange: (_, __) {},
onAction: (_) {},
);
createTextFieldSemantics(
value: 'hello',
isFocused: true,
);
expect(strategy.domElement, isNotNull);
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
// Blur the element without telling the framework.
strategy.activeDomElement.blur();
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
// The input will have focus after editing state is set and semantics updated.
strategy.setEditingState(EditingState(text: 'foo'));
// NOTE: at this point some browsers, e.g. some versions of Safari will
// have set the focus on the editing element as a result of setting
// the test selection range. Other browsers require an explicit call
// to `element.focus()` for the element to acquire focus. So far,
// this discrepancy hasn't caused issues, so we're not checking for
// any particular focus state between setEditingState and
// createTextFieldSemantics. However, this is something for us to
// keep in mind in case this causes issues in the future.
createTextFieldSemantics(
value: 'hello',
isFocused: true,
);
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
strategy.disable();
});
test('Works in multi-line mode', () {
strategy.enable(
multilineConfig,
onChange: (_, __) {},
onAction: (_) {},
);
createTextFieldSemantics(
value: 'hello',
isFocused: true,
isMultiline: true,
);
final DomHTMLTextAreaElement textArea =
strategy.domElement! as DomHTMLTextAreaElement;
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
strategy.enable(
singlelineConfig,
onChange: (_, __) {},
onAction: (_) {},
);
textArea.blur();
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
strategy.disable();
// It doesn't remove the textarea from the DOM.
expect(appHostNode.contains(textArea), isTrue);
// Editing element is not enabled.
expect(strategy.isEnabled, isFalse);
});
test('Does not position or size its DOM element', () {
strategy.enable(
singlelineConfig,
onChange: (_, __) {},
onAction: (_) {},
);
// Send width and height that are different from semantics values on
// purpose.
final EditableTextGeometry geometry = EditableTextGeometry(
height: 12,
width: 13,
globalTransform: Matrix4.translationValues(14, 15, 0).storage,
);
testTextEditing.acceptCommand(
TextInputSetEditableSizeAndTransform(geometry: geometry),
() {},
);
createTextFieldSemantics(
value: 'hello',
isFocused: true,
);
// Checks that the placement attributes come from semantics and not from
// EditableTextGeometry.
void checkPlacementIsSetBySemantics() {
expect(strategy.activeDomElement.style.transform, '');
expect(strategy.activeDomElement.style.width, '100px');
expect(strategy.activeDomElement.style.height, '50px');
}
checkPlacementIsSetBySemantics();
strategy.placeElement();
checkPlacementIsSetBySemantics();
});
Map<int, SemanticsObject> createTwoFieldSemantics(SemanticsTester builder,
{int? focusFieldId}) {
builder.updateNode(
id: 0,
children: <SemanticsNodeUpdate>[
builder.updateNode(
id: 1,
isTextField: true,
value: 'Hello',
isFocused: focusFieldId == 1,
rect: const ui.Rect.fromLTRB(0, 0, 50, 10),
),
builder.updateNode(
id: 2,
isTextField: true,
value: 'World',
isFocused: focusFieldId == 2,
rect: const ui.Rect.fromLTRB(0, 20, 50, 10),
),
],
);
return builder.apply();
}
test('Changes focus from one text field to another through a semantics update', () {
strategy.enable(
singlelineConfig,
onChange: (_, __) {},
onAction: (_) {},
);
// Switch between the two fields a few times.
for (int i = 0; i < 5; i++) {
final SemanticsTester tester = SemanticsTester(semantics());
createTwoFieldSemantics(tester, focusFieldId: 1);
expect(tester.apply().length, 3);
expect(appHostNode.ownerDocument?.activeElement,
tester.getTextField(1).editableElement);
expect(strategy.domElement, tester.getTextField(1).editableElement);
createTwoFieldSemantics(tester, focusFieldId: 2);
expect(tester.apply().length, 3);
expect(appHostNode.ownerDocument?.activeElement,
tester.getTextField(2).editableElement);
expect(strategy.domElement, tester.getTextField(2).editableElement);
}
});
}, skip: isIosSafari);
group('$SemanticsTextEditingStrategy in iOS', () {
late HybridTextEditing testTextEditing;
late SemanticsTextEditingStrategy strategy;
setUp(() {
testTextEditing = HybridTextEditing();
SemanticsTextEditingStrategy.ensureInitialized(testTextEditing);
strategy = SemanticsTextEditingStrategy.instance;
testTextEditing.debugTextEditingStrategyOverride = strategy;
testTextEditing.configuration = singlelineConfig;
debugBrowserEngineOverride = BrowserEngine.webkit;
debugOperatingSystemOverride = OperatingSystem.iOs;
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
});
tearDown(() {
debugBrowserEngineOverride = null;
debugOperatingSystemOverride = null;
semantics().semanticsEnabled = false;
});
test('does not render a text field', () {
expect(appHostNode.querySelector('flt-semantics[role="textbox"]'), isNull);
createTextFieldSemanticsForIos(value: 'hello');
expect(appHostNode.querySelector('flt-semantics[role="textbox"]'), isNotNull);
});
test('tap detection works', () async {
final SemanticsActionLogger logger = SemanticsActionLogger();
createTextFieldSemanticsForIos(value: 'hello');
final DomElement textField = appHostNode
.querySelector('flt-semantics[role="textbox"]')!;
simulateTap(textField);
expect(await logger.idLog.first, 0);
expect(await logger.actionLog.first, ui.SemanticsAction.tap);
});
test('Syncs semantic state from framework', () {
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
int changeCount = 0;
int actionCount = 0;
strategy.enable(
singlelineConfig,
onChange: (_, __) {
changeCount++;
},
onAction: (_) {
actionCount++;
},
);
// Create
final SemanticsObject textFieldSemantics = createTextFieldSemanticsForIos(
value: 'hello',
label: 'greeting',
isFocused: true,
rect: const ui.Rect.fromLTWH(0, 0, 10, 15),
);
final TextField textField =
textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField;
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
expect(textField.editableElement, strategy.domElement);
expect(textField.activeEditableElement.getAttribute('aria-label'), 'greeting');
expect(textField.activeEditableElement.style.width, '10px');
expect(textField.activeEditableElement.style.height, '15px');
// Update
createTextFieldSemanticsForIos(
value: 'bye',
label: 'farewell',
rect: const ui.Rect.fromLTWH(0, 0, 12, 17),
);
final DomElement textBox =
appHostNode.querySelector('flt-semantics[role="textbox"]')!;
expect(strategy.domElement, null);
expect(appHostNode.ownerDocument?.activeElement, textBox);
expect(textBox.getAttribute('aria-label'), 'farewell');
strategy.disable();
// There was no user interaction with the <input> element,
// so we should expect no engine-to-framework feedback.
expect(changeCount, 0);
expect(actionCount, 0);
});
test(
'Does not overwrite text value and selection editing state on semantic updates',
() {
strategy.enable(
singlelineConfig,
onChange: (_, __) {},
onAction: (_) {},
);
final SemanticsObject textFieldSemantics = createTextFieldSemanticsForIos(
value: 'hello',
textSelectionBase: 1,
textSelectionExtent: 3,
isFocused: true,
rect: const ui.Rect.fromLTWH(0, 0, 10, 15));
final TextField textField =
textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField;
final DomHTMLInputElement editableElement =
textField.activeEditableElement as DomHTMLInputElement;
expect(editableElement, strategy.domElement);
expect(editableElement.value, '');
expect(editableElement.selectionStart, 0);
expect(editableElement.selectionEnd, 0);
strategy.disable();
});
test(
'Updates editing state when receiving framework messages from the text input channel',
() {
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
strategy.enable(
singlelineConfig,
onChange: (_, __) {},
onAction: (_) {},
);
final SemanticsObject textFieldSemantics = createTextFieldSemanticsForIos(
value: 'hello',
textSelectionBase: 1,
textSelectionExtent: 3,
isFocused: true,
rect: const ui.Rect.fromLTWH(0, 0, 10, 15));
final TextField textField =
textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField;
final DomHTMLInputElement editableElement =
textField.activeEditableElement as DomHTMLInputElement;
// No updates expected on semantic updates
expect(editableElement, strategy.domElement);
expect(editableElement.value, '');
expect(editableElement.selectionStart, 0);
expect(editableElement.selectionEnd, 0);
// Update from framework
const MethodCall setEditingState =
MethodCall('TextInput.setEditingState', <String, dynamic>{
'text': 'updated',
'selectionBase': 2,
'selectionExtent': 3,
});
sendFrameworkMessage(codec.encodeMethodCall(setEditingState), testTextEditing);
// Editing state should now be updated
// expect(editableElement.value, 'updated');
expect(editableElement.selectionStart, 2);
expect(editableElement.selectionEnd, 3);
strategy.disable();
});
test('Gives up focus after DOM blur', () {
expect(appHostNode.ownerDocument?.activeElement, domDocument.body);
strategy.enable(
singlelineConfig,
onChange: (_, __) {},
onAction: (_) {},
);
final SemanticsObject textFieldSemantics = createTextFieldSemanticsForIos(
value: 'hello',
isFocused: true,
);
final TextField textField =
textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField;
expect(textField.editableElement, strategy.domElement);
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
// The input should not refocus after blur.
textField.activeEditableElement.blur();
final DomElement textBox =
appHostNode.querySelector('flt-semantics[role="textbox"]')!;
expect(appHostNode.ownerDocument?.activeElement, textBox);
strategy.disable();
});
test('Disposes and recreates dom elements in persistent mode', () {
strategy.enable(
singlelineConfig,
onChange: (_, __) {},
onAction: (_) {},
);
// It doesn't create a new DOM element.
expect(strategy.domElement, isNull);
// During the semantics update the DOM element is created and is focused on.
final SemanticsObject textFieldSemantics = createTextFieldSemanticsForIos(
value: 'hello',
isFocused: true,
);
expect(strategy.domElement, isNotNull);
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
strategy.disable();
expect(strategy.domElement, isNull);
// It removes the DOM element.
final TextField textField = textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField;
expect(appHostNode.contains(textField.editableElement), isFalse);
// Editing element is not enabled.
expect(strategy.isEnabled, isFalse);
// Focus is on the semantic object
final DomElement textBox =
appHostNode.querySelector('flt-semantics[role="textbox"]')!;
expect(appHostNode.ownerDocument?.activeElement, textBox);
});
test('Refocuses when setting editing state', () {
strategy.enable(
singlelineConfig,
onChange: (_, __) {},
onAction: (_) {},
);
createTextFieldSemanticsForIos(
value: 'hello',
isFocused: true,
);
expect(strategy.domElement, isNotNull);
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
// Blur the element without telling the framework.
strategy.activeDomElement.blur();
final DomElement textBox =
appHostNode.querySelector('flt-semantics[role="textbox"]')!;
expect(appHostNode.ownerDocument?.activeElement, textBox);
// The input will have focus after editing state is set and semantics updated.
strategy.setEditingState(EditingState(text: 'foo'));
// NOTE: at this point some browsers, e.g. some versions of Safari will
// have set the focus on the editing element as a result of setting
// the test selection range. Other browsers require an explicit call
// to `element.focus()` for the element to acquire focus. So far,
// this discrepancy hasn't caused issues, so we're not checking for
// any particular focus state between setEditingState and
// createTextFieldSemantics. However, this is something for us to
// keep in mind in case this causes issues in the future.
createTextFieldSemanticsForIos(
value: 'hello',
isFocused: true,
);
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
strategy.disable();
});
test('Works in multi-line mode', () {
strategy.enable(
multilineConfig,
onChange: (_, __) {},
onAction: (_) {},
);
createTextFieldSemanticsForIos(
value: 'hello',
isFocused: true,
isMultiline: true,
);
final DomHTMLTextAreaElement textArea =
strategy.domElement! as DomHTMLTextAreaElement;
expect(appHostNode.ownerDocument?.activeElement, strategy.domElement);
strategy.enable(
singlelineConfig,
onChange: (_, __) {},
onAction: (_) {},
);
expect(appHostNode.contains(textArea), isTrue);
textArea.blur();
final DomElement textBox =
appHostNode.querySelector('flt-semantics[role="textbox"]')!;
expect(appHostNode.ownerDocument?.activeElement, textBox);
strategy.disable();
// It removes the textarea from the DOM.
expect(appHostNode.contains(textArea), isFalse);
// Editing element is not enabled.
expect(strategy.isEnabled, isFalse);
});
test('Does not position or size its DOM element', () {
strategy.enable(
singlelineConfig,
onChange: (_, __) {},
onAction: (_) {},
);
// Send width and height that are different from semantics values on
// purpose.
final Matrix4 transform = Matrix4.translationValues(14, 15, 0);
final EditableTextGeometry geometry = EditableTextGeometry(
height: 12,
width: 13,
globalTransform: transform.storage,
);
const ui.Rect semanticsRect = ui.Rect.fromLTRB(0, 0, 100, 50);
testTextEditing.acceptCommand(
TextInputSetEditableSizeAndTransform(geometry: geometry),
() {},
);
createTextFieldSemanticsForIos(
value: 'hello',
isFocused: true,
);
// Checks that the placement attributes come from semantics and not from
// EditableTextGeometry.
void checkPlacementIsSetBySemantics() {
expect(strategy.activeDomElement.style.transform,
isNot(equals(transform.toString())));
expect(strategy.activeDomElement.style.width, '${semanticsRect.width}px');
expect(strategy.activeDomElement.style.height, '${semanticsRect.height}px');
}
checkPlacementIsSetBySemantics();
strategy.placeElement();
checkPlacementIsSetBySemantics();
});
test('Changes focus from one text field to another through a semantics update', () {
strategy.enable(
singlelineConfig,
onChange: (_, __) {},
onAction: (_) {},
);
// Switch between the two fields a few times.
for (int i = 0; i < 1; i++) {
final SemanticsTester tester = SemanticsTester(semantics());
createTwoFieldSemanticsForIos(tester, focusFieldId: 1);
expect(tester.apply().length, 3);
expect(appHostNode.ownerDocument?.activeElement,
tester.getTextField(1).editableElement);
expect(strategy.domElement, tester.getTextField(1).editableElement);
createTwoFieldSemanticsForIos(tester, focusFieldId: 2);
expect(tester.apply().length, 3);
expect(appHostNode.ownerDocument?.activeElement,
tester.getTextField(2).editableElement);
expect(strategy.domElement, tester.getTextField(2).editableElement);
}
});
test('input transform is correct', () async {
strategy.enable(
singlelineConfig,
onChange: (_, __) {},
onAction: (_) {},
);
createTextFieldSemanticsForIos(
value: 'hello',
isFocused: true,
);
expect(strategy.activeDomElement.style.transform, 'translate(${offScreenOffset}px, ${offScreenOffset}px)');
// See [_delayBeforePlacement].
await Future<void>.delayed(const Duration(milliseconds: 120) , (){});
expect(strategy.activeDomElement.style.transform, '');
});
test('disposes the editable element, if there is one', () {
strategy.enable(
singlelineConfig,
onChange: (_, __) {},
onAction: (_) {},
);
SemanticsObject textFieldSemantics = createTextFieldSemanticsForIos(
value: 'hello',
);
TextField textField =
textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField;
expect(textField.editableElement, isNull);
textField.dispose();
expect(textField.editableElement, isNull);
textFieldSemantics = createTextFieldSemanticsForIos(
value: 'hi',
isFocused: true,
);
textField =
textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField;
expect(textField.editableElement, isNotNull);
textField.dispose();
expect(textField.editableElement, isNull);
});
}, skip: !isSafari);
}
SemanticsObject createTextFieldSemantics({
required String value,
String label = '',
bool isFocused = false,
bool isMultiline = false,
ui.Rect rect = const ui.Rect.fromLTRB(0, 0, 100, 50),
int textSelectionBase = 0,
int textSelectionExtent = 0,
}) {
final SemanticsTester tester = SemanticsTester(semantics());
tester.updateNode(
id: 0,
label: label,
value: value,
isTextField: true,
isFocused: isFocused,
isMultiline: isMultiline,
hasTap: true,
rect: rect,
textDirection: ui.TextDirection.ltr,
textSelectionBase: textSelectionBase,
textSelectionExtent: textSelectionExtent
);
tester.apply();
return tester.getSemanticsObject(0);
}
void simulateTap(DomElement element) {
element.dispatchEvent(createDomPointerEvent(
'pointerdown',
<Object?, Object?>{
'clientX': 125,
'clientY': 248,
},
));
element.dispatchEvent(createDomPointerEvent(
'pointerup',
<Object?, Object?>{
'clientX': 126,
'clientY': 248,
},
));
}
/// An editable DOM element won't be created on iOS unless a tap is detected.
/// This function mimics the workflow by simulating a tap and sending a second
/// semantic update.
SemanticsObject createTextFieldSemanticsForIos({
required String value,
String label = '',
bool isFocused = false,
bool isMultiline = false,
ui.Rect rect = const ui.Rect.fromLTRB(0, 0, 100, 50),
int textSelectionBase = 0,
int textSelectionExtent = 0,
}) {
final SemanticsObject textFieldSemantics = createTextFieldSemantics(
value: value,
isFocused: isFocused,
label: label,
isMultiline: isMultiline,
rect: rect,
textSelectionBase: textSelectionBase,
textSelectionExtent: textSelectionExtent,
);
if (isFocused) {
final TextField textField =
textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField;
simulateTap(textField.semanticsObject.element);
return createTextFieldSemantics(
value: value,
isFocused: isFocused,
label: label,
isMultiline: isMultiline,
rect: rect,
textSelectionBase: textSelectionBase,
textSelectionExtent: textSelectionExtent,
);
}
return textFieldSemantics;
}
/// See [createTextFieldSemanticsForIos].
Map<int, SemanticsObject> createTwoFieldSemanticsForIos(SemanticsTester builder,
{int? focusFieldId}) {
builder.updateNode(
id: 0,
children: <SemanticsNodeUpdate>[
builder.updateNode(
id: 1,
isTextField: true,
value: 'Hello',
label: 'Hello',
isFocused: false,
rect: const ui.Rect.fromLTWH(0, 0, 10, 10),
),
builder.updateNode(
id: 2,
isTextField: true,
value: 'World',
label: 'World',
isFocused: false,
rect: const ui.Rect.fromLTWH(20, 20, 10, 10),
),
],
);
builder.apply();
final String label = focusFieldId == 1 ? 'Hello' : 'World';
final DomElement textBox =
appHostNode.querySelector('flt-semantics[aria-label="$label"]')!;
simulateTap(textBox);
builder.updateNode(
id: 0,
children: <SemanticsNodeUpdate>[
builder.updateNode(
id: 1,
isTextField: true,
value: 'Hello',
label: 'Hello',
isFocused: focusFieldId == 1,
rect: const ui.Rect.fromLTWH(0, 0, 10, 10),
),
builder.updateNode(
id: 2,
isTextField: true,
value: 'World',
label: 'World',
isFocused: focusFieldId == 2,
rect: const ui.Rect.fromLTWH(20, 20, 10, 10),
),
],
);
return builder.apply();
}
/// Emulates sending of a message by the framework to the engine.
void sendFrameworkMessage(ByteData? message, HybridTextEditing testTextEditing) {
testTextEditing.channel.handleTextInput(message, (ByteData? data) {});
}