Skip to content

Commit

Permalink
Merge branch 'feature/INT-3298' into feature/INT-3299
Browse files Browse the repository at this point in the history
  • Loading branch information
danoaky-tiny committed Jun 20, 2024
2 parents 91df0b0 + 926dc87 commit bd092c8
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 44 deletions.
30 changes: 20 additions & 10 deletions tinymce-angular-component/src/test/ts/alien/TestHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@ import { Fun, Global, Arr, Strings } from '@ephox/katamari';
import { Observable, throwError, timeout } from 'rxjs';
import { ScriptLoader } from '../../../main/ts/utils/ScriptLoader';
import { Attribute, Remove, SelectorFilter, SugarElement } from '@ephox/sugar';
import { ComponentFixture } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { EditorComponent } from '../../../main/ts/editor/editor.component';
import { Editor } from 'tinymce';
import { Keyboard, Keys } from '@ephox/agar';

export const apiKey = Fun.constant(
'qagffr3pkuv17a8on1afax661irst1hbr4e6tbv888sz91jc',
);
export const apiKey = Fun.constant('qagffr3pkuv17a8on1afax661irst1hbr4e6tbv888sz91jc');

export const throwTimeout =
(timeoutMs: number, message: string = `Timeout ${timeoutMs}ms`) =>
<T>(source: Observable<T>) =>
source.pipe(
timeout({
first: timeoutMs,
with: () => throwError(() => new Error(message)),
})
);
<T>(source: Observable<T>) =>
source.pipe(
timeout({
first: timeoutMs,
with: () => throwError(() => new Error(message)),
})
);

export const deleteTinymce = () => {
ScriptLoader.reinitialize();
Expand Down Expand Up @@ -48,3 +51,10 @@ export const captureLogs = async (
console[method] = original;
}
};

export const fakeTypeInEditor = (fixture: ComponentFixture<unknown>, str: string) => {
const editor: Editor = fixture.debugElement.query(By.directive(EditorComponent)).componentInstance.editor!;
editor.getBody().innerHTML = '<p>' + str + '</p>';
Keyboard.keystroke(Keys.space(), {}, SugarElement.fromDom(editor.getBody()));
fixture.detectChanges();
};
137 changes: 115 additions & 22 deletions tinymce-angular-component/src/test/ts/browser/FormControlTest.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,132 @@
import '../alien/InitTestEnvironment';

import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { ChangeDetectionStrategy, Component, ViewChild, ElementRef } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { Assertions } from '@ephox/agar';
import { describe, it } from '@ephox/bedrock-client';
import { context, describe, it } from '@ephox/bedrock-client';

import { EditorComponent } from '../../../main/ts/public_api';
import { eachVersionContext, editorHook } from '../alien/TestHooks';
import { eachVersionContext, editorHook, fixtureHook } from '../alien/TestHooks';
import { By } from '@angular/platform-browser';
import { first, firstValueFrom, switchMap } from 'rxjs';
import { Editor } from 'tinymce';
import { fakeTypeInEditor } from '../alien/TestHelpers';

type FormControlProps = Partial<Record<'touched' | 'pristine' | 'dirty' | 'valid', boolean>>;

describe('FormControlTest', () => {
eachVersionContext([ '4', '5', '6', '7' ], () => {
@Component({
standalone: true,
imports: [ EditorComponent, ReactiveFormsModule ],
template: `<editor [formControl]="control" />`,
})
class EditorWithFormControl {
public control = new FormControl();
const assertFormControl = (label: string, control: FormControlProps, expected: FormControlProps) => {
for (const [ key, value ] of Object.entries(expected)) {
Assertions.assertEq(`${label} - ${key}`, value, control[key as keyof FormControlProps]);
}
const createFixture = editorHook(EditorWithFormControl);
};

eachVersionContext([ '4', '5', '6', '7' ], () => {
[ ChangeDetectionStrategy.Default, ChangeDetectionStrategy.OnPush ].forEach((changeDetection) => {
context(`[formControl] with change detection: ${changeDetection}`, () => {
@Component({
standalone: true,
imports: [ EditorComponent, ReactiveFormsModule ],
changeDetection,
template: `<editor [formControl]="control" />`,
})
class EditorWithFormControl {
public control = new FormControl();
}
const createFixture = editorHook(EditorWithFormControl);

it('FormControl interaction', async () => {
const fixture = await createFixture();

Assertions.assertEq('Expect editor to have no initial value', '', fixture.editor.getContent());

fixture.componentInstance.control.setValue('<p>Some Value</p>');
fixture.detectChanges();

Assertions.assertEq('Expect editor to have a value', '<p>Some Value</p>', fixture.editor.getContent());

it('FormControl interaction', async () => {
const fixture = await createFixture();
fixture.componentInstance.control.reset();
fixture.detectChanges();

Assertions.assertEq('Expect editor to have no initial value', '', fixture.editor.getContent());
Assertions.assertEq('Expect editor to be empty after reset', '', fixture.editor.getContent());
});
});

fixture.componentInstance.control.setValue('<p>Some Value</p>');
fixture.detectChanges();
context(`[formGroup] with change detection: ${changeDetection}`, () => {
@Component({
standalone: true,
changeDetection,
imports: [ EditorComponent, ReactiveFormsModule ],
template: `
<form [formGroup]="form">
<editor formControlName="editor" />
<button #resetBtn type="reset">Reset form</button>
<button #submitBtn [disabled]="form.invalid" type="submit">Submit form</button>
</form>
`,
})
class FormWithEditor {
@ViewChild('resetBtn') public resetBtn!: ElementRef<HTMLButtonElement>;
@ViewChild('submitBtn') public submitBtn!: ElementRef<HTMLButtonElement>;
public readonly form = new FormGroup({
editor: new FormControl('', {
validators: Validators.compose([
// eslint-disable-next-line @typescript-eslint/unbound-method
Validators.required,
Validators.minLength(10),
]),
}),
});
}
const createFixture = fixtureHook(FormWithEditor, { imports: [ FormWithEditor ] });

Assertions.assertEq('Expect editor to have a value', '<p>Some Value</p>', fixture.editor.getContent());
it('interaction', async () => {
const fixture = createFixture();
fixture.detectChanges();
const editorComponent: EditorComponent = fixture.debugElement.query(
By.directive(EditorComponent)
).componentInstance;
const editor = await firstValueFrom(
editorComponent.onInit.pipe(
first(),
switchMap((ev) => new Promise<Editor>((resolve) => ev.editor.on('SkinLoaded', () => resolve(ev.editor))))
)
);
const form = fixture.componentInstance.form;
const initialProps: FormControlProps = { valid: false, dirty: false, pristine: true, touched: false };
// const editorCtrl = form.get('editor')!;

fixture.componentInstance.control.reset();
fixture.detectChanges();
assertFormControl('Initial form', form, initialProps);
editor.fire('blur');
assertFormControl('Form after editor blur', form, { ...initialProps, touched: true });
fixture.componentInstance.resetBtn.nativeElement.click();
fixture.detectChanges();
assertFormControl('Form after reset', form, initialProps);

Assertions.assertEq('Expect editor to be empty after reset', '', fixture.editor.getContent());
fakeTypeInEditor(fixture, 'x');
assertFormControl('Form after typing one character', form, {
valid: false,
dirty: true,
pristine: false,
touched: false,
});
editor.fire('blur');
assertFormControl('Form after editor blur', form, {
valid: false,
dirty: true,
pristine: false,
touched: true,
});
fakeTypeInEditor(fixture, 'x'.repeat(20));
assertFormControl('Form after typing 10 characters', form, {
valid: true,
dirty: true,
pristine: false,
touched: true,
});
Assertions.assertEq('Editor value has expected value', `<p>${'x'.repeat(20)}</p>`, form.value.editor);
});
});
});
});
});
18 changes: 6 additions & 12 deletions tinymce-angular-component/src/test/ts/browser/NgModelTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,18 @@ import '../alien/InitTestEnvironment';

import { Component } from '@angular/core';
import { FormsModule, NgModel } from '@angular/forms';
import { Assertions, Waiter, Keyboard, Keys } from '@ephox/agar';
import { Assertions, Waiter } from '@ephox/agar';
import { describe, it } from '@ephox/bedrock-client';
import { SugarElement } from '@ephox/sugar';

import { EditorComponent } from '../../../main/ts/editor/editor.component';
import { EditorFixture, eachVersionContext, editorHook } from '../alien/TestHooks';
import { eachVersionContext, editorHook } from '../alien/TestHooks';
import { fakeTypeInEditor } from '../alien/TestHelpers';

describe('NgModelTest', () => {
const assertNgModelState = (prop: 'valid' | 'pristine' | 'touched', expected: boolean, ngModel: NgModel) => {
Assertions.assertEq('assert ngModel ' + prop + ' state', expected, ngModel[prop]);
};

const fakeType = (fixture: EditorFixture<unknown>, str: string) => {
fixture.editor.getBody().innerHTML = '<p>' + str + '</p>';
Keyboard.keystroke(Keys.space(), {}, SugarElement.fromDom(fixture.editor.getBody()));
fixture.detectChanges();
};

eachVersionContext([ '4', '5', '6', '7' ], () => {
@Component({
standalone: true,
Expand Down Expand Up @@ -58,7 +52,7 @@ describe('NgModelTest', () => {
it('should have correct control flags after interaction', async () => {
const fixture = await createFixture();
const ngModel = fixture.ngModel.getOrDie('NgModel not found');
fakeType(fixture, 'X');
fakeTypeInEditor(fixture, 'X');
// Should be dirty after user input but remain untouched
assertNgModelState('pristine', false, ngModel);
assertNgModelState('touched', false, ngModel);
Expand All @@ -71,7 +65,7 @@ describe('NgModelTest', () => {

it('Test outputFormat="text"', async () => {
const fixture = await createFixture({ outputFormat: 'text' });
fakeType(fixture, 'X');
fakeTypeInEditor(fixture, 'X');
Assertions.assertEq(
'Value bound to content via ngModel should be plain text',
'X',
Expand All @@ -81,7 +75,7 @@ describe('NgModelTest', () => {

it('Test outputFormat="html"', async () => {
const fixture = await createFixture({ outputFormat: 'html' });
fakeType(fixture, 'X');
fakeTypeInEditor(fixture, 'X');
Assertions.assertEq(
'Value bound to content via ngModel should be html',
'<p>X</p>',
Expand Down

0 comments on commit bd092c8

Please sign in to comment.