blob: 3b2366c6b2063fd79e1e1d5784e2fa0d0a88f207 [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')
import 'dart:html' as html;
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(
inputType: EngineInputType.text,
);
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() {
setUp(() {
EngineSemanticsOwner.debugResetSemantics();
});
group('$SemanticsTextEditingStrategy', () {
late HybridTextEditing testTextEditing;
late SemanticsTextEditingStrategy strategy;
setUp(() {
testTextEditing = HybridTextEditing();
SemanticsTextEditingStrategy.ensureInitialized(testTextEditing);
strategy = SemanticsTextEditingStrategy.instance;
testTextEditing.debugTextEditingStrategyOverride = strategy;
testTextEditing.configuration = singlelineConfig;
});
test('renders a text field', () async {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
createTextFieldSemantics(value: 'hello');
expectSemanticsTree('''
<sem style="$rootSemanticStyle">
<input value="hello" />
</sem>''');
semantics().semanticsEnabled = false;
});
// 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();
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
createTextFieldSemantics(value: 'hello');
final html.Element textField = appHostNode
.querySelector('input[data-semantics-role="text-field"]')!;
expect(appHostNode.activeElement, isNot(textField));
textField.focus();
expect(appHostNode.activeElement, textField);
expect(await logger.idLog.first, 0);
expect(await logger.actionLog.first, ui.SemanticsAction.tap);
semantics().semanticsEnabled = false;
}, // TODO(nurhan): https://github.com/flutter/flutter/issues/46638
// TODO(nurhan): https://github.com/flutter/flutter/issues/50590
// TODO(nurhan): https://github.com/flutter/flutter/issues/50754
skip: browserEngine != BrowserEngine.blink);
test('Syncs editing state from framework', () async {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
expect(html.document.activeElement, html.document.body);
expect(appHostNode.activeElement, null);
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(html.document.activeElement, domRenderer.glassPaneElement);
expect(appHostNode.activeElement, strategy.domElement);
expect(textField.editableElement, strategy.domElement);
expect((textField.editableElement as dynamic).value, 'hello');
expect(textField.editableElement.getAttribute('aria-label'), 'greeting');
expect(textField.editableElement.style.width, '10px');
expect(textField.editableElement.style.height, '15px');
// Update
createTextFieldSemantics(
value: 'bye',
label: 'farewell',
isFocused: false,
rect: const ui.Rect.fromLTWH(0, 0, 12, 17),
);
expect(html.document.activeElement, html.document.body);
expect(appHostNode.activeElement, null);
expect(strategy.domElement, null);
expect((textField.editableElement as dynamic).value, 'bye');
expect(textField.editableElement.getAttribute('aria-label'), 'farewell');
expect(textField.editableElement.style.width, '12px');
expect(textField.editableElement.style.height, '17px');
strategy.disable();
semantics().semanticsEnabled = false;
// 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('Gives up focus after DOM blur', () async {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
expect(html.document.activeElement, html.document.body);
expect(appHostNode.activeElement, null);
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(html.document.activeElement, domRenderer.glassPaneElement);
expect(appHostNode.activeElement, strategy.domElement);
// The input should not refocus after blur.
textField.editableElement.blur();
expect(html.document.activeElement, html.document.body);
expect(appHostNode.activeElement, null);
strategy.disable();
semantics().semanticsEnabled = false;
});
test('Does not dispose and recreate dom elements in persistent mode', () {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
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(html.document.activeElement, domRenderer.glassPaneElement);
expect(appHostNode.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(html.document.activeElement, html.document.body);
expect(appHostNode.activeElement, null);
semantics().semanticsEnabled = false;
});
test('Refocuses when setting editing state', () {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
strategy.enable(
singlelineConfig,
onChange: (_) {},
onAction: (_) {},
);
createTextFieldSemantics(
value: 'hello',
isFocused: true,
);
expect(strategy.domElement, isNotNull);
expect(html.document.activeElement, domRenderer.glassPaneElement);
expect(appHostNode.activeElement, strategy.domElement);
// Blur the element without telling the framework.
strategy.activeDomElement.blur();
expect(html.document.activeElement, html.document.body);
expect(appHostNode.activeElement, null);
// 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(html.document.activeElement, domRenderer.glassPaneElement);
expect(appHostNode.activeElement, strategy.domElement);
strategy.disable();
semantics().semanticsEnabled = false;
});
test('Works in multi-line mode', () {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
strategy.enable(
multilineConfig,
onChange: (_) {},
onAction: (_) {},
);
createTextFieldSemantics(
value: 'hello',
isFocused: true,
isMultiline: true,
);
final html.TextAreaElement textArea = strategy.domElement! as html.TextAreaElement;
expect(html.document.activeElement, domRenderer.glassPaneElement);
expect(appHostNode.activeElement, strategy.domElement);
strategy.enable(
singlelineConfig,
onChange: (_) {},
onAction: (_) {},
);
textArea.blur();
expect(html.document.activeElement, html.document.body);
expect(appHostNode.activeElement, null);
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);
semantics().semanticsEnabled = false;
});
test('Does not position or size its DOM element', () {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
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,
);
const ui.Rect semanticsRect = ui.Rect.fromLTRB(0, 0, 100, 50);
testTextEditing.acceptCommand(
TextInputSetEditableSizeAndTransform(geometry: geometry),
() {},
);
createTextFieldSemantics(
value: 'hello',
isFocused: true,
rect: semanticsRect,
);
// Checks that the placement attributes come from semantics and not from
// EditableTextGeometry.
void checkPlacementIsSetBySemantics() {
expect(strategy.activeDomElement.style.transform, '');
expect(strategy.activeDomElement.style.width, '${semanticsRect.width}px');
expect(strategy.activeDomElement.style.height, '${semanticsRect.height}px');
}
checkPlacementIsSetBySemantics();
strategy.placeElement();
checkPlacementIsSetBySemantics();
semantics().semanticsEnabled = false;
});
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', () async {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
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(html.document.activeElement, domRenderer.glassPaneElement);
expect(appHostNode.activeElement, tester.getTextField(1).editableElement);
expect(strategy.domElement, tester.getTextField(1).editableElement);
createTwoFieldSemantics(tester, focusFieldId: 2);
expect(tester.apply().length, 3);
expect(appHostNode.activeElement, tester.getTextField(2).editableElement);
expect(strategy.domElement, tester.getTextField(2).editableElement);
}
semantics().semanticsEnabled = false;
});
},
// TODO(nurhan): https://github.com/flutter/flutter/issues/50769
skip: browserEngine == BrowserEngine.edge);
}
SemanticsObject createTextFieldSemantics({
required String value,
String label = '',
bool isFocused = false,
bool isMultiline = false,
ui.Rect rect = const ui.Rect.fromLTRB(0, 0, 100, 50),
}) {
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,
);
tester.apply();
return tester.getSemanticsObject(0);
}