Skip to content

Commit 1cd38cf

Browse files
authored
INT-3297: Refactor tests to use BDD style (#516)
* INT-3297: Refactored helpers to not use any agar chaining * INT-3297: Refactored `Loader.tsx` to not use any agar chaining and simplified context usage * INT-3297: Refactored tests to use newer style BDD testing * INT-3297: Fix linting issues * INT-3297: Added `Loader.withVersion` for using miniature to preload TinyMCE Also removed `deleteTinyMCE` from Loader.tsx and moved it to LoadTinyTest.ts
1 parent 66d5015 commit 1cd38cf

File tree

7 files changed

+304
-325
lines changed

7 files changed

+304
-325
lines changed

.eslintrc.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@
1414
"@tinymce/prefer-fun": "off",
1515
"@typescript-eslint/no-unsafe-argument": "off"
1616
}
17+
},
18+
{
19+
"files": [
20+
"src/test/**/*"
21+
],
22+
"rules": {
23+
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
24+
"no-var": "off" // Without this the `using` keyword causes eslint to throw an error during linting.
25+
}
1726
}
1827
]
1928
}

src/test/ts/alien/Loader.tsx

Lines changed: 63 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1-
import { Chain, NamedChain } from '@ephox/agar';
21
import { Fun, Optional } from '@ephox/katamari';
32
import { SugarElement, SugarNode } from '@ephox/sugar';
43
import * as React from 'react';
54
import * as ReactDOM from 'react-dom';
6-
import { Editor, IAllProps, IProps } from '../../../main/ts/components/Editor';
5+
import { Editor, IAllProps, IProps, Version } from '../../../main/ts/components/Editor';
76
import { Editor as TinyMCEEditor } from 'tinymce';
7+
import { before, context } from '@ephox/bedrock-client';
8+
import { VersionLoader } from '@tinymce/miniature';
9+
10+
// @ts-expect-error Remove when dispose polyfill is not needed
11+
Symbol.dispose ??= Symbol('Symbol.dispose');
12+
// @ts-expect-error Remove when dispose polyfill is not needed
13+
Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose');
814

915
export interface Context {
1016
DOMNode: HTMLElement;
@@ -18,78 +24,78 @@ const getRoot = () => Optional.from(document.getElementById('root')).getOrThunk(
1824
document.body.appendChild(root);
1925
return root;
2026
});
27+
export interface ReactEditorContext extends Context, Disposable {
28+
reRender(props: IAllProps): Promise<void>;
29+
remove(): void;
30+
}
2131

22-
const cRender = (props: Partial<IAllProps>) => Chain.async<unknown, Context>((_, next, die) => {
32+
export const render = async (props: Partial<IAllProps> = {}, container: HTMLElement = getRoot()): Promise<ReactEditorContext> => {
2333
const originalInit = props.init || {};
2434
const originalSetup = originalInit.setup || Fun.noop;
2535
const ref = React.createRef<Editor>();
2636

27-
const init: IProps['init'] = {
28-
...originalInit,
29-
setup: (editor) => {
30-
originalSetup(editor);
37+
const ctx = await new Promise<Context>((resolve, reject) => {
38+
const init: IProps['init'] = {
39+
...originalInit,
40+
setup: (editor) => {
41+
originalSetup(editor);
3142

32-
editor.on('SkinLoaded', () => {
33-
setTimeout(() => {
34-
Optional.from(ref.current)
35-
.map(ReactDOM.findDOMNode)
36-
.bind(Optional.from)
37-
.map(SugarElement.fromDom)
38-
.filter(SugarNode.isHTMLElement)
39-
.map((val) => val.dom)
40-
.fold(() => die('Could not find DOMNode'), (DOMNode) => {
41-
next({
42-
ref,
43-
editor,
44-
DOMNode
43+
editor.on('SkinLoaded', () => {
44+
setTimeout(() => {
45+
Optional.from(ref.current)
46+
.map(ReactDOM.findDOMNode)
47+
.bind(Optional.from)
48+
.map(SugarElement.fromDom)
49+
.filter(SugarNode.isHTMLElement)
50+
.map((val) => val.dom)
51+
.fold(() => reject('Could not find DOMNode'), (DOMNode) => {
52+
resolve({
53+
ref,
54+
editor,
55+
DOMNode,
56+
});
4557
});
46-
});
47-
}, 0);
48-
});
49-
}
50-
};
58+
}, 0);
59+
});
60+
}
61+
};
5162

52-
/**
63+
/**
5364
* NOTE: TinyMCE will manipulate the DOM directly and this may cause issues with React's virtual DOM getting
5465
* out of sync. The official fix for this is wrap everything (textarea + editor) in an element. As far as React
5566
* is concerned, the wrapper always only has a single child, thus ensuring that React doesn’t have a reason to
5667
* touch the nodes created by TinyMCE. Since this only seems to be an issue when rendering TinyMCE 4 directly
5768
* into a root and a fix would be a breaking change, let's just wrap the editor in a <div> here for now.
5869
*/
59-
ReactDOM.render(<div><Editor ref={ref} apiKey='no-api-key' {...props} init={init} /></div>, getRoot());
60-
});
61-
62-
// By rendering the Editor into the same root, React will perform a diff and update.
63-
const cReRender = (props: Partial<IAllProps>) => Chain.op<Context>((context) => {
64-
ReactDOM.render(<div><Editor apiKey='no-api-key' ref={context.ref} {...props} /></div>, getRoot());
65-
});
70+
ReactDOM.render(<div><Editor ref={ref} apiKey='no-api-key' {...props} init={init} /></div>, container);
71+
});
6672

67-
const cRemove = Chain.op((_) => {
68-
ReactDOM.unmountComponentAtNode(getRoot());
69-
});
73+
const remove = () => {
74+
ReactDOM.unmountComponentAtNode(container);
75+
};
7076

71-
const cNamedChainDirect = (name: keyof Context) => NamedChain.direct(
72-
NamedChain.inputName(),
73-
Chain.mapper((res: Context) => res[name]),
74-
name
75-
);
77+
return {
78+
...ctx,
79+
/** By rendering the Editor into the same root, React will perform a diff and update. */
80+
reRender: (newProps: IAllProps) => new Promise<void>((resolve) =>
81+
ReactDOM.render(<div><Editor apiKey='no-api-key' ref={ctx.ref} {...newProps} /></div>, container, resolve)
82+
),
83+
remove,
84+
[Symbol.dispose]: remove
85+
};
86+
};
7687

77-
const cDOMNode = (chain: Chain<Context['DOMNode'], unknown>): Chain<Context, Context> => NamedChain.asChain<Context>([
78-
cNamedChainDirect('DOMNode'),
79-
NamedChain.read('DOMNode', chain),
80-
NamedChain.outputInput
81-
]);
88+
type RenderWithVersion = (
89+
props: Omit<IAllProps, 'cloudChannel' | 'tinymceScriptSrc'>,
90+
container?: HTMLElement | HTMLDivElement
91+
) => Promise<ReactEditorContext>;
8292

83-
const cEditor = (chain: Chain<Context['editor'], unknown>): Chain<Context, Context> => NamedChain.asChain<Context>([
84-
cNamedChainDirect('editor'),
85-
NamedChain.read('editor', chain),
86-
NamedChain.outputInput
87-
]);
93+
export const withVersion = (version: Version, fn: (render: RenderWithVersion) => void): void => {
94+
context(`TinyMCE (${version})`, () => {
95+
before(async () => {
96+
await VersionLoader.pLoadVersion(version);
97+
});
8898

89-
export {
90-
cRender,
91-
cReRender,
92-
cRemove,
93-
cDOMNode,
94-
cEditor
99+
fn(render as RenderWithVersion);
100+
});
95101
};

src/test/ts/alien/TestHelpers.ts

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { Chain, Assertions } from '@ephox/agar';
1+
import { Assertions } from '@ephox/agar';
22
import { Cell, Obj } from '@ephox/katamari';
3-
import { ApiChains } from '@ephox/mcagar';
43
import { Version } from 'src/main/ts/components/Editor';
54
import { Editor as TinyMCEEditor } from 'tinymce';
65

@@ -14,6 +13,8 @@ type HandlerType<A> = (a: A, editor: TinyMCEEditor) => unknown;
1413
const VERSIONS: Version[] = [ '4', '5', '6', '7' ];
1514
const CLOUD_VERSIONS: Version[] = [ '5', '6', '7' ];
1615

16+
const VALID_API_KEY = 'qagffr3pkuv17a8on1afax661irst1hbr4e6tbv888sz91jc';
17+
1718
const EventStore = () => {
1819
const state: Cell<Record<string, EventHandlerArgs<unknown>[]>> = Cell({});
1920

@@ -30,32 +31,25 @@ const EventStore = () => {
3031
});
3132
};
3233

33-
const cEach = <T>(name: string, assertState: (state: EventHandlerArgs<T>[]) => void) => Chain.fromChains([
34-
Chain.op(() => {
35-
Assertions.assertEq('State from "' + name + '" handler should exist', true, name in state.get());
36-
assertState(state.get()[name] as unknown as EventHandlerArgs<T>[]);
37-
})
38-
]);
34+
const each = <T>(name: string, assertState: (state: EventHandlerArgs<T>[]) => void) => {
35+
Assertions.assertEq('State from "' + name + '" handler should exist', true, name in state.get());
36+
assertState(state.get()[name] as unknown as EventHandlerArgs<T>[]);
37+
};
3938

40-
const cClearState = Chain.op(() => {
39+
const clearState = () => {
4140
state.set({});
42-
});
41+
};
4342

4443
return {
45-
cEach,
44+
each,
4645
createHandler,
47-
cClearState
46+
clearState
4847
};
4948
};
5049

51-
// casting needed due to fake types used in mcagar
52-
const cSetContent = (content: string) => ApiChains.cSetContent(content) as unknown as Chain<TinyMCEEditor, TinyMCEEditor>;
53-
const cAssertContent = (content: string) => ApiChains.cAssertContent(content) as unknown as Chain<TinyMCEEditor, TinyMCEEditor>;
54-
5550
export {
51+
VALID_API_KEY,
5652
EventStore,
57-
cSetContent,
58-
cAssertContent,
5953
VERSIONS,
6054
CLOUD_VERSIONS,
6155
Version

0 commit comments

Comments
 (0)