Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion guides/using-component-harnesses.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ idea for component harnesses comes from the
[PageObject](https://martinfowler.com/bliki/PageObject.html) pattern commonly used for integration
testing.

Angular Material offers test harnesses for many of its components. The Angular team strongly
Angular Material offers test harnesses for most of its components. The Angular team strongly
encourages developers to use these harnesses for testing to avoid creating brittle tests that rely
on a component's internals.

Expand Down Expand Up @@ -262,3 +262,23 @@ When tests depend on the implementation details, they become a common source of
library changes. Angular CDK's test harnesses makes component library updates easier for both
application authors and the Angular team, as the Angular team only has to update the harness once
for everyone.

## Differences between Testbed and real browser testing (Protractor)

Writing test for both environments is very similar now. But even thought we do our best to get the
behavior as close as possible, it's a low priority task and there are still some slight differences
in the result of sending keys you should be arware of:

* **All keys** will send keydown, keypress and keyup events.
* **Normal characters** will change the value of text inputs at the cursor position or replace
selection.
* **Arrow keys**
* **in text inputs** will emulate cursor movement or, if send with modifier ```{ shift: true }```,
seletion. But in textareas arrow up and down will just use col count and won't hit exactly one
line beacuse font measures and width and line wrapping.
* **in select inputs** might change the selected option.
* **Page-Up/-Down** won't do anything but sending the key events.
* **Backspace/Delete** won't delete anything.
* **Enter** in inputs won't submit forms or insert a new line in multiline inputs. But of course it
may sometimes select an option, if the component listens to the keydown event.
* **Tab** won't change focus to the next or previous input, nor select it's text value.
190 changes: 190 additions & 0 deletions src/cdk/testing/testbed/fake-events/emulate-arrow-in-text-input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {
TextInputElement, getSelectionStart, getValueLength, getSelectionEnd,
} from './text-input-element';

type ArrowKey = 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight';
export function isArrowKey(key: string): key is ArrowKey {
return key.startsWith('Arrow');
}

/**
* will move cursor and/or change selection of text value
*/
export function emulateArrowInTextInput(
modifiers: { shift?: boolean }, key: ArrowKey, element: TextInputElement,
) {
if (modifiers.shift) {
switch (key) {
case 'ArrowUp': return emulateShiftArrowUp(element);
case 'ArrowRight': return emulateShiftArrowRight(element);
case 'ArrowDown': return emulateShiftArrowDown(element);
case 'ArrowLeft': return emulateShiftArrowLeft(element);
}
} else {
switch (key) {
case 'ArrowUp': return emulateArrowUp(element);
case 'ArrowRight': return emulateArrowRight(element);
case 'ArrowDown': return emulateArrowDown(element);
case 'ArrowLeft': return emulateArrowLeft(element);
}
}
}

function emulateArrowUp(target: TextInputElement) {
const lineLength = getLineLength(target);
const selectionStart = getSelectionStart(target);

setCursor(target, Math.max(selectionStart - lineLength, 0));
}

function setCursor(target: TextInputElement, position: number) {
target.setSelectionRange(position, position, 'none');
}

function getLineLength(target: TextInputElement, valueLength = getValueLength(target)) {
return target instanceof HTMLTextAreaElement ? target.cols : valueLength;
}

function emulateArrowRight(target: TextInputElement) {
if (target.selectionStart !== target.selectionEnd) {
setCursor(target, getSelectionEnd(target));
return;
}

const valueLength = getValueLength(target);
const selectionEnd = target.selectionEnd;
if (selectionEnd && selectionEnd < valueLength) {
setCursor(target, selectionEnd + 1);
} else {
setCursor(target, valueLength);
}
}

function emulateArrowDown(target: TextInputElement) {
const valueLength = getValueLength(target);
const lineLength = getLineLength(target, valueLength);
const selectionEnd = getSelectionEnd(target);
setCursor(target, Math.min(selectionEnd + lineLength, valueLength));
}

function emulateArrowLeft(target: TextInputElement) {
if (target.selectionStart !== target.selectionEnd) {
setCursor(target, getSelectionStart(target));
target.selectionEnd = target.selectionStart;
return;
}

const selectionStart = getSelectionStart(target);
if (selectionStart && selectionStart > 0) {
setCursor(target, selectionStart - 1);
return;
}

const valueLength = getValueLength(target);
if (valueLength > 0) {
setCursor(target, valueLength - 1);
return;
}

setCursor(target, 0);
}

function emulateShiftArrowUp(target: TextInputElement) {
const lineLength = getLineLength(target);

if (isSelectionDirectionOf(target, 'forward')) {
reduceSelectionAtSelectionEnd(target, lineLength);
} else {
extendSelectionAtSelectionStart(target, lineLength);
}
}

function isSelectionDirectionOf(target: TextInputElement, direction: 'forward' | 'backward') {
return target.selectionDirection === direction && (target.selectionStart !== target.selectionEnd);
}

function extendSelectionAtSelectionStart(target: TextInputElement, lengthToExtend: number) {
const selectionStart = getSelectionStart(target);
const newSelectionStart = Math.max(selectionStart - lengthToExtend, 0);
target.selectionStart = newSelectionStart;
if (target.selectionDirection !== 'backward') {
target.selectionDirection = 'backward';
}
}

function reduceSelectionAtSelectionEnd(target: TextInputElement, lengthToReduce: number) {
const selectionStart = getSelectionStart(target);
const selectionEnd = getSelectionEnd(target);
const newSelectionEnd = selectionEnd - lengthToReduce;

if (selectionStart < newSelectionEnd) {
target.selectionEnd = newSelectionEnd;
} else {
target.selectionEnd = selectionStart;
target.selectionDirection = 'none';
// there's a different behavior in firefox, which would move selection end:
// target.selectionEnd = selectionStart;
// target.selectionStart = newSelectionEnd;
// target.selectionDirection = 'backward';
}
}

function emulateShiftArrowRight(target: TextInputElement) {
if (isSelectionDirectionOf(target, 'backward')) {
reduceSelectionAtSelectionStart(target, 1);
} else {
extendSelectionAtSelectionEnd(target, 1);
}
}

function extendSelectionAtSelectionEnd(target: TextInputElement, lengthToExtend: number) {
const valueLength = getValueLength(target);
const selectionEnd = getSelectionEnd(target);
target.selectionEnd = Math.min(selectionEnd + lengthToExtend, valueLength);
if (target.selectionDirection !== 'forward') {
target.selectionDirection = 'forward';
}
}

function reduceSelectionAtSelectionStart(target: TextInputElement, lengthToReduce: number) {
const selectionStart = getSelectionStart(target);
const selectionEnd = getSelectionEnd(target);
const newSelectionStart = selectionStart + lengthToReduce;

if (newSelectionStart < selectionEnd) {
target.selectionStart = newSelectionStart;
} else {
target.selectionStart = selectionEnd;
target.selectionDirection = 'none';
// there's a different behavior in firefox, which would move selection end:
// target.selectionStart = selectionEnd;
// target.selectionEnd = newSelectionStart;
// target.selectionDirection = 'forward';
}
}

function emulateShiftArrowDown(target: TextInputElement) {
const lineLength = getLineLength(target);

if (isSelectionDirectionOf(target, 'backward')) {
reduceSelectionAtSelectionStart(target, lineLength);
} else {
extendSelectionAtSelectionEnd(target, lineLength);
}
}

function emulateShiftArrowLeft(target: TextInputElement) {
if (isSelectionDirectionOf(target, 'forward')) {
reduceSelectionAtSelectionEnd(target, 1);
} else {
extendSelectionAtSelectionStart(target, 1);
}
}
47 changes: 47 additions & 0 deletions src/cdk/testing/testbed/fake-events/emulate-char-in-text-input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {dispatchFakeEvent} from './dispatch-events';
import {getSelectionEnd, getSelectionStart, TextInputElement} from './text-input-element';

/**
* emulate writing characters into text input fields.
* @docs-private
*/
export function writeCharacter(element: TextInputElement, key: string) {
if (!hasToRespectSelection(element)) {
element.value += key;
} else {
const value = element.value;
const selectionStart = getSelectionStart(element);
const valueBeforeSelection = value.substr(0, selectionStart);
const selectionEnd = getSelectionEnd(element);
const valueAfterSelection = value.substr(selectionEnd);
element.value = valueBeforeSelection + key + valueAfterSelection;

const cursor = selectionStart + key.length;
element.setSelectionRange(cursor, cursor, 'none');
}
dispatchFakeEvent(element, 'input');
}

const knownInputTypesWithSelectionIssues = ['color'];
function hasToRespectSelection(element: TextInputElement): boolean {
try {
return typeof element.value === 'string' && element.value.length > 0
&& typeof element.selectionStart === 'number' && element.selectionStart < element.value.length
;
} catch (e) {
// Safari will throw 'type error' on input[type=color].selectionStart.get().
if (knownInputTypesWithSelectionIssues.indexOf(element.type) < 0) {
console.warn('could not detect selection of ', element);
}
// In general we can't respect selection if we can't detect selection so this will return false.
return false;
}
}
Loading