diff --git a/src/cdk/testing/protractor/protractor-element.ts b/src/cdk/testing/protractor/protractor-element.ts index 9b016f48662d..709c506a3def 100644 --- a/src/cdk/testing/protractor/protractor-element.ts +++ b/src/cdk/testing/protractor/protractor-element.ts @@ -78,7 +78,8 @@ export class ProtractorElement implements TestElement { } async clear(): Promise { - return this.element.clear(); + await this.element.sendKeys(Key.BACK_SPACE); + await this.element.clear(); } async click(...args: [] | ['center'] | [number, number]): Promise { diff --git a/src/cdk/testing/testbed/fake-events/type-in-element.ts b/src/cdk/testing/testbed/fake-events/type-in-element.ts index 416692af5b51..88f5ad469641 100644 --- a/src/cdk/testing/testbed/fake-events/type-in-element.ts +++ b/src/cdk/testing/testbed/fake-events/type-in-element.ts @@ -14,11 +14,31 @@ import {triggerFocus} from './element-focus'; * Checks whether the given Element is a text input element. * @docs-private */ -export function isTextInput(element: Element): element is HTMLInputElement | HTMLTextAreaElement { +function isTextInput(element: Element): element is HTMLInputElement | HTMLTextAreaElement { const nodeName = element.nodeName.toLowerCase(); return nodeName === 'input' || nodeName === 'textarea' ; } +/** + * Checks whether the given Element's content is editable. + * An element is content editable if + * - the element has "contenteditable" attribute set to "true" + * - any of its ancestors has "contenteditable" attribute set to "true" + * - the owner document has designMode attribute set to "on". + * @docs-private + */ +function isContentEditable(element: Element): element is HTMLElement { + return element instanceof HTMLElement && element.isContentEditable; +} + +/** + * Checks whether the given Element changes with input from keyboard. + * @docs-private + */ +function isInputAware(element: Element): element is HTMLElement { + return isTextInput(element) || isContentEditable(element); +} + /** * Focuses an input, sets its value and dispatches * the `input` event, simulating the user typing. @@ -41,6 +61,9 @@ export function typeInElement(element: HTMLElement, modifiers: ModifierKeys, ...keys: (string | {keyCode?: number, key?: string})[]): void; export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any) { + if (!isInputAware(element)) { + throw new Error('Attempting to send keys to an invalid element'); + } const first = modifiersAndKeys[0]; let modifiers: ModifierKeys; let rest: (string | {keyCode?: number, key?: string})[]; @@ -60,8 +83,12 @@ export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any) { for (const key of keys) { dispatchKeyboardEvent(element, 'keydown', key.keyCode, key.key, modifiers); dispatchKeyboardEvent(element, 'keypress', key.keyCode, key.key, modifiers); - if (isTextInput(element) && key.key && key.key.length === 1) { - element.value += key.key; + if (isInputAware(element) && key.key?.length === 1) { + if (isTextInput(element)) { + element.value += key.key; + } else { + element.appendChild(new Text(key.key)); + } dispatchFakeEvent(element, 'input'); } dispatchKeyboardEvent(element, 'keyup', key.keyCode, key.key, modifiers); @@ -69,11 +96,18 @@ export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any) { } /** - * Clears the text in an input or textarea element. + * Clears the content or text of an input aware element. * @docs-private */ -export function clearElement(element: HTMLInputElement | HTMLTextAreaElement) { - triggerFocus(element as HTMLElement); - element.value = ''; +export function clearElement(element: Element) { + if (!isInputAware(element)) { + throw new Error('Attempting to clear an invalid element'); + } + triggerFocus(element); + if (isTextInput(element)) { + element.value = ''; + } else { + element.textContent = ''; + } dispatchFakeEvent(element, 'input'); } diff --git a/src/cdk/testing/testbed/unit-test-element.ts b/src/cdk/testing/testbed/unit-test-element.ts index 15f14e199c57..b1c4129ce148 100644 --- a/src/cdk/testing/testbed/unit-test-element.ts +++ b/src/cdk/testing/testbed/unit-test-element.ts @@ -22,7 +22,6 @@ import { dispatchFakeEvent, dispatchMouseEvent, dispatchPointerEvent, - isTextInput, triggerBlur, triggerFocus, typeInElement, @@ -73,9 +72,6 @@ export class UnitTestElement implements TestElement { } async clear(): Promise { - if (!isTextInput(this.element)) { - throw Error('Attempting to clear an invalid element'); - } clearElement(this.element); await this._stabilize(); } diff --git a/src/cdk/testing/tests/cross-environment.spec.ts b/src/cdk/testing/tests/cross-environment.spec.ts index 5dc68fa915f0..0c14efc923ea 100644 --- a/src/cdk/testing/tests/cross-environment.spec.ts +++ b/src/cdk/testing/tests/cross-environment.spec.ts @@ -15,6 +15,12 @@ import { import {MainComponentHarness} from './harnesses/main-component-harness'; import {SubComponentHarness, SubComponentSpecialHarness} from './harnesses/sub-component-harness'; +/** Environments for the tests. */ +export const enum TestEnvironment { + TEST_BED, + PROTRACTOR, +} + /** * Tests that should behave equal in testbed and protractor environment. * @@ -22,6 +28,7 @@ import {SubComponentHarness, SubComponentSpecialHarness} from './harnesses/sub-c * with TestbedHarnessEnvironment and ProtractorHarnessEnvironment, this set of tests is * executed in unit tests and tests. * + * @param environment in which environment the tests are running * @param getHarnessLoaderFromEnvironment env specific closure to get HarnessLoader * @param getMainComponentHarnessFromEnvironment env specific closure to get MainComponentHarness * @param getActiveElementId env specific closure to get active element @@ -29,11 +36,12 @@ import {SubComponentHarness, SubComponentSpecialHarness} from './harnesses/sub-c * @docs-private */ export function crossEnvironmentSpecs( + environment: TestEnvironment, getHarnessLoaderFromEnvironment: () => HarnessLoader, getMainComponentHarnessFromEnvironment: () => Promise, // Maybe we should introduce HarnessLoader.getActiveElement(): TestElement // then this 3rd parameter could get removed. - getActiveElementId: () => Promise, + getActiveElementId: () => Promise, ) { describe('HarnessLoader', () => { let loader: HarnessLoader; @@ -305,11 +313,14 @@ export function crossEnvironmentSpecs( it('should be able to clear', async () => { const input = await harness.input(); + const inputEvent = await harness.inputEvent(); await input.sendKeys('Yi'); expect(await input.getProperty('value')).toBe('Yi'); + expect(await inputEvent.text()).toBe('Count: 2'); await input.clear(); expect(await input.getProperty('value')).toBe(''); + expect(await inputEvent.text()).toBe('Count: 3'); }); it('should be able to click', async () => { @@ -355,6 +366,68 @@ export function crossEnvironmentSpecs( expect(await getActiveElementId()).toBe(await input.getAttribute('id')); }); + it('should be able to send key to a contenteditable', async () => { + const editable = await harness.editable(); + const editableInputEvent = await harness.editableInputEvent(); + + await editable.sendKeys('Yi'); + + expect(await editable.text()).toBe('Yi'); + expect(await editableInputEvent.text()).toBe('Count: 2'); + + await editable.clear(); + + expect(await editable.text()).toBe(''); + expect(await editableInputEvent.text()).toBe('Count: 3'); + }); + + it('should be able to send key to a child of a contenteditable', async () => { + const editable = await harness.editableP(); + + await editable.sendKeys('Yi'); + + expect(await editable.text()).toBe('Yi'); + }); + + it('should not update not contenteditable div on send key', async () => { + const notEditable = await harness.notEditable(); + const notEditableInputEvent = await harness.notEditableInputEvent(); + + await expectAsyncError(notEditable.sendKeys('Yi')); + + expect(await notEditable.text()).toBe(''); + expect(await notEditableInputEvent.text()).toBe('Count: 0'); + + await expectAsyncError(notEditable.clear()); + }); + + it('should send key based on designMode', async () => { + const notEditable = await harness.notEditable(); + const inheritEditable = await harness.inheritEditable(); + + await expectAsyncError(notEditable.sendKeys('Yi')); + expect(await notEditable.text()).toBe(''); + + await expectAsyncError(inheritEditable.sendKeys('Yi')); + expect(await inheritEditable.text()).toBe(''); + + await (await harness.designModeOnButton()).click(); + + await expectAsyncError(notEditable.sendKeys('Yi')); + expect(await notEditable.text()).toBe(''); + + await inheritEditable.sendKeys('Yi'); + // The following expectation is failing on Protractor environment + // In protractor environment, the text is empty. + if (environment == TestEnvironment.TEST_BED) { + expect(await inheritEditable.text()).toBe('Yi'); + } else { + expect(await inheritEditable.text()).toBe(''); + } + + await (await harness.designModeOffButton()).click(); + }); + it('should be able to retrieve dimensions', async () => { const dimensions = await (await harness.title()).getDimensions(); expect(dimensions).toEqual(jasmine.objectContaining({height: 100, width: 200})); @@ -576,3 +649,9 @@ export async function checkIsHarness( await finalCheck(result as T); } } + +async function expectAsyncError(promise: Promise) { + let error = false; + await promise.catch(() => { error = true; }); + expect(error).toBe(true); +} diff --git a/src/cdk/testing/tests/harnesses/main-component-harness.ts b/src/cdk/testing/tests/harnesses/main-component-harness.ts index 815ea4b8ffbc..0f6e13f3a66f 100644 --- a/src/cdk/testing/tests/harnesses/main-component-harness.ts +++ b/src/cdk/testing/tests/harnesses/main-component-harness.ts @@ -23,6 +23,7 @@ export class MainComponentHarness extends ComponentHarness { readonly counter = this.locatorFor('#counter'); readonly input = this.locatorFor('#input'); readonly value = this.locatorFor('#value'); + readonly inputEvent = this.locatorFor('#input-event-count'); readonly allLabels = this.locatorForAll('label'); readonly allLists = this.locatorForAll(SubComponentHarness); readonly memo = this.locatorFor('textarea'); @@ -95,6 +96,15 @@ export class MainComponentHarness extends ComponentHarness { readonly customEventBasic = this.locatorFor('#custom-event-basic'); readonly customEventObject = this.locatorFor('#custom-event-object'); + readonly editable = this.locatorFor('#editable'); + readonly editableP = this.locatorFor('#editable p'); + readonly editableInputEvent = this.locatorFor('#editable-input-event'); + readonly notEditable = this.locatorFor('#not-editable'); + readonly notEditableInputEvent = this.locatorFor('#not-editable-input-event'); + readonly inheritEditable = this.locatorFor('#inherit-editable'); + readonly designModeOnButton = this.locatorFor('#design-mode-on'); + readonly designModeOffButton = this.locatorFor('#design-mode-off'); + private _testTools = this.locatorFor(SubComponentHarness); async increaseCounter(times: number) { diff --git a/src/cdk/testing/tests/protractor.e2e.spec.ts b/src/cdk/testing/tests/protractor.e2e.spec.ts index 813a2d6b42f5..60e55d682408 100644 --- a/src/cdk/testing/tests/protractor.e2e.spec.ts +++ b/src/cdk/testing/tests/protractor.e2e.spec.ts @@ -1,7 +1,7 @@ import {HarnessLoader} from '@angular/cdk/testing'; import {ProtractorHarnessEnvironment} from '@angular/cdk/testing/protractor'; import {browser, by, element as protractorElement, ElementFinder} from 'protractor'; -import {crossEnvironmentSpecs} from './cross-environment.spec'; +import {crossEnvironmentSpecs, TestEnvironment} from './cross-environment.spec'; import {MainComponentHarness} from './harnesses/main-component-harness'; // Kagekiri is available globally in the browser. We declare it here so we can use it in the @@ -82,6 +82,7 @@ describe('ProtractorHarnessEnvironment', () => { }); describe('environment independent', () => crossEnvironmentSpecs( + TestEnvironment.PROTRACTOR, () => ProtractorHarnessEnvironment.loader(), () => ProtractorHarnessEnvironment.loader().getHarness(MainComponentHarness), async () => (await activeElement()).getAttribute('id'), diff --git a/src/cdk/testing/tests/test-main-component.html b/src/cdk/testing/tests/test-main-component.html index 6ca01563039c..c21ed6afa94e 100644 --- a/src/cdk/testing/tests/test-main-component.html +++ b/src/cdk/testing/tests/test-main-component.html @@ -14,10 +14,29 @@

Main Component

{{asyncCounter}}
- + {{specialKey}}
Input: {{input}}
+
Count: {{inputElementInputEventCount$ | async}}
+
+
+

+

+
+
+ Count: {{editableElementInputEventCount$ | async}} +
+
+
+
+ Count: {{notEditableElementInputEventCount$ | async}} +
+
+
+ + +