blob: 5d4b9fb6a89d680c3f1bcd4eb248904f27d36cd0 [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.
import 'dart:async';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine/browser_detection.dart';
import 'package:ui/src/engine/dom.dart';
import 'package:ui/src/engine/initialization.dart';
import 'package:ui/src/engine/text_editing/composition_aware_mixin.dart';
import 'package:ui/src/engine/text_editing/text_editing.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
class _MockWithCompositionAwareMixin with CompositionAwareMixin {
// These variables should be equal to their counterparts in CompositionAwareMixin.
// Separate so the counterparts in CompositionAwareMixin can be private.
static const String _kCompositionUpdate = 'compositionupdate';
static const String _kCompositionStart = 'compositionstart';
static const String _kCompositionEnd = 'compositionend';
}
DomHTMLInputElement get _inputElement {
return defaultTextEditingRoot.querySelectorAll('input').first as DomHTMLInputElement;
}
GloballyPositionedTextEditingStrategy _enableEditingStrategy({
required bool deltaModel,
void Function(EditingState?, TextEditingDeltaState?)? onChange,
}) {
final HybridTextEditing owner = HybridTextEditing();
owner.configuration = InputConfiguration(enableDeltaModel: deltaModel);
final GloballyPositionedTextEditingStrategy editingStrategy =
GloballyPositionedTextEditingStrategy(owner);
owner.debugTextEditingStrategyOverride = editingStrategy;
editingStrategy.enable(owner.configuration!, onChange: onChange ?? (_, __) {}, onAction: (_) {});
return editingStrategy;
}
Future<void> testMain() async {
await initializeEngine();
const String fakeComposingText = 'ImComposingText';
group('$CompositionAwareMixin', () {
late TextEditingStrategy editingStrategy;
setUp(() {
editingStrategy = _enableEditingStrategy(deltaModel: false);
});
tearDown(() {
editingStrategy.disable();
});
group('composition end', () {
test('should reset composing text on handle composition end', () {
final _MockWithCompositionAwareMixin mockWithCompositionAwareMixin =
_MockWithCompositionAwareMixin();
mockWithCompositionAwareMixin.composingText = fakeComposingText;
mockWithCompositionAwareMixin.addCompositionEventHandlers(_inputElement);
_inputElement.dispatchEvent(createDomEvent('Event', _MockWithCompositionAwareMixin._kCompositionEnd));
expect(mockWithCompositionAwareMixin.composingText, null);
});
});
group('composition start', () {
test('should reset composing text on handle composition start', () {
final _MockWithCompositionAwareMixin mockWithCompositionAwareMixin =
_MockWithCompositionAwareMixin();
mockWithCompositionAwareMixin.composingText = fakeComposingText;
mockWithCompositionAwareMixin.addCompositionEventHandlers(_inputElement);
_inputElement.dispatchEvent(createDomEvent('Event', _MockWithCompositionAwareMixin._kCompositionStart));
expect(mockWithCompositionAwareMixin.composingText, null);
});
});
group('composition update', () {
test('should set composing text to event composing text', () {
const String fakeEventText = 'IAmComposingThis';
final _MockWithCompositionAwareMixin mockWithCompositionAwareMixin =
_MockWithCompositionAwareMixin();
mockWithCompositionAwareMixin.composingText = fakeComposingText;
mockWithCompositionAwareMixin.addCompositionEventHandlers(_inputElement);
_inputElement.dispatchEvent(createDomCompositionEvent(
_MockWithCompositionAwareMixin._kCompositionUpdate,
<Object?, Object?>{ 'data': fakeEventText }
));
expect(mockWithCompositionAwareMixin.composingText, fakeEventText);
});
});
group('determine composition state', () {
test('should return new composition state if valid new composition', () {
const int baseOffset = 100;
const String composingText = 'composeMe';
final EditingState editingState = EditingState(
extentOffset: baseOffset,
text: 'testing',
baseOffset: baseOffset,
);
final _MockWithCompositionAwareMixin mockWithCompositionAwareMixin =
_MockWithCompositionAwareMixin();
mockWithCompositionAwareMixin.composingText = composingText;
const int expectedComposingBase = baseOffset - composingText.length;
expect(
mockWithCompositionAwareMixin.determineCompositionState(editingState),
editingState.copyWith(
composingBaseOffset: expectedComposingBase,
composingExtentOffset: expectedComposingBase + composingText.length));
});
});
});
group('composing range', () {
late GloballyPositionedTextEditingStrategy editingStrategy;
setUp(() {
editingStrategy = _enableEditingStrategy(deltaModel: false);
});
tearDown(() {
editingStrategy.disable();
});
test('should be [0, compostionStrLength] on new composition', () {
const String composingText = 'hi';
_inputElement.dispatchEvent(createDomCompositionEvent(
_MockWithCompositionAwareMixin._kCompositionUpdate,
<Object?, Object?>{'data': composingText}));
// Set the selection text.
_inputElement.value = composingText;
_inputElement.dispatchEvent(createDomEvent('Event', 'input'));
expect(
editingStrategy.lastEditingState,
isA<EditingState>()
.having((EditingState editingState) => editingState.composingBaseOffset,
'composingBaseOffset', 0)
.having((EditingState editingState) => editingState.composingExtentOffset,
'composingExtentOffset', composingText.length));
});
test(
'should be [beforeComposingText - composingText, compostionStrLength] on composition in the middle of text',
() {
const String composingText = 'hi';
const String beforeComposingText = 'beforeComposingText';
const String afterComposingText = 'afterComposingText';
// Type in the text box, then move cursor to the middle.
_inputElement.value = '$beforeComposingText$afterComposingText';
_inputElement.setSelectionRange(beforeComposingText.length, beforeComposingText.length);
_inputElement.dispatchEvent(createDomCompositionEvent(
_MockWithCompositionAwareMixin._kCompositionUpdate,
<Object?, Object?>{ 'data': composingText }
));
// Flush editing state (since we did not compositionend).
_inputElement.dispatchEvent(createDomEvent('Event', 'input'));
expect(
editingStrategy.lastEditingState,
isA<EditingState>()
.having((EditingState editingState) => editingState.composingBaseOffset,
'composingBaseOffset', beforeComposingText.length - composingText.length)
.having((EditingState editingState) => editingState.composingExtentOffset,
'composingExtentOffset', beforeComposingText.length));
});
});
group('Text Editing Delta Model', () {
late GloballyPositionedTextEditingStrategy editingStrategy;
final StreamController<TextEditingDeltaState?> deltaStream =
StreamController<TextEditingDeltaState?>.broadcast();
setUp(() {
editingStrategy = _enableEditingStrategy(
deltaModel: true,
onChange: (_, TextEditingDeltaState? deltaState) => deltaStream.add(deltaState)
);
});
tearDown(() {
editingStrategy.disable();
});
test('should have newly entered composing characters', () async {
const String newComposingText = 'n';
editingStrategy.setEditingState(EditingState(text: newComposingText, baseOffset: 1, extentOffset: 1));
final Future<dynamic> containExpect = expectLater(
deltaStream.stream.first,
completion(isA<TextEditingDeltaState>()
.having((TextEditingDeltaState deltaState) => deltaState.composingOffset, 'composingOffset', 0)
.having((TextEditingDeltaState deltaState) => deltaState.composingExtent, 'composingExtent', newComposingText.length)
));
_inputElement.dispatchEvent(createDomCompositionEvent(
_MockWithCompositionAwareMixin._kCompositionUpdate,
<Object?, Object?>{ 'data': newComposingText }));
await containExpect;
});
test('should emit changed composition', () async {
const String newComposingCharsInOrder = 'hiCompose';
for (int currCharIndex = 0; currCharIndex < newComposingCharsInOrder.length; currCharIndex++) {
final String currComposingSubstr = newComposingCharsInOrder.substring(0, currCharIndex + 1);
editingStrategy.setEditingState(
EditingState(text: currComposingSubstr, baseOffset: currCharIndex + 1, extentOffset: currCharIndex + 1)
);
final Future<dynamic> containExpect = expectLater(
deltaStream.stream.first,
completion(isA<TextEditingDeltaState>()
.having((TextEditingDeltaState deltaState) => deltaState.composingOffset, 'composingOffset', 0)
.having((TextEditingDeltaState deltaState) => deltaState.composingExtent, 'composingExtent', currCharIndex + 1)
));
_inputElement.dispatchEvent(createDomCompositionEvent(
_MockWithCompositionAwareMixin._kCompositionUpdate,
<Object?, Object?>{ 'data': currComposingSubstr }));
await containExpect;
}
},
// TODO(antholeole): This test fails on Firefox because of how it orders events;
// it's likely that this will be fixed by https://github.com/flutter/flutter/issues/105243.
// Until the refactor gets merged, this test should run on all other browsers to prevent
// regressions in the meantime.
skip: browserEngine == BrowserEngine.firefox);
});
}