Skip to content

Commit

Permalink
INT-3297: Refactor tests to use BDD style (#516)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
danoaky-tiny authored Apr 24, 2024
1 parent 66d5015 commit 1cd38cf
Show file tree
Hide file tree
Showing 7 changed files with 304 additions and 325 deletions.
9 changes: 9 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
"@tinymce/prefer-fun": "off",
"@typescript-eslint/no-unsafe-argument": "off"
}
},
{
"files": [
"src/test/**/*"
],
"rules": {
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"no-var": "off" // Without this the `using` keyword causes eslint to throw an error during linting.
}
}
]
}
120 changes: 63 additions & 57 deletions src/test/ts/alien/Loader.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { Chain, NamedChain } from '@ephox/agar';
import { Fun, Optional } from '@ephox/katamari';
import { SugarElement, SugarNode } from '@ephox/sugar';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Editor, IAllProps, IProps } from '../../../main/ts/components/Editor';
import { Editor, IAllProps, IProps, Version } from '../../../main/ts/components/Editor';
import { Editor as TinyMCEEditor } from 'tinymce';
import { before, context } from '@ephox/bedrock-client';
import { VersionLoader } from '@tinymce/miniature';

// @ts-expect-error Remove when dispose polyfill is not needed
Symbol.dispose ??= Symbol('Symbol.dispose');
// @ts-expect-error Remove when dispose polyfill is not needed
Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose');

export interface Context {
DOMNode: HTMLElement;
Expand All @@ -18,78 +24,78 @@ const getRoot = () => Optional.from(document.getElementById('root')).getOrThunk(
document.body.appendChild(root);
return root;
});
export interface ReactEditorContext extends Context, Disposable {
reRender(props: IAllProps): Promise<void>;
remove(): void;
}

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

const init: IProps['init'] = {
...originalInit,
setup: (editor) => {
originalSetup(editor);
const ctx = await new Promise<Context>((resolve, reject) => {
const init: IProps['init'] = {
...originalInit,
setup: (editor) => {
originalSetup(editor);

editor.on('SkinLoaded', () => {
setTimeout(() => {
Optional.from(ref.current)
.map(ReactDOM.findDOMNode)
.bind(Optional.from)
.map(SugarElement.fromDom)
.filter(SugarNode.isHTMLElement)
.map((val) => val.dom)
.fold(() => die('Could not find DOMNode'), (DOMNode) => {
next({
ref,
editor,
DOMNode
editor.on('SkinLoaded', () => {
setTimeout(() => {
Optional.from(ref.current)
.map(ReactDOM.findDOMNode)
.bind(Optional.from)
.map(SugarElement.fromDom)
.filter(SugarNode.isHTMLElement)
.map((val) => val.dom)
.fold(() => reject('Could not find DOMNode'), (DOMNode) => {
resolve({
ref,
editor,
DOMNode,
});
});
});
}, 0);
});
}
};
}, 0);
});
}
};

/**
/**
* NOTE: TinyMCE will manipulate the DOM directly and this may cause issues with React's virtual DOM getting
* out of sync. The official fix for this is wrap everything (textarea + editor) in an element. As far as React
* is concerned, the wrapper always only has a single child, thus ensuring that React doesn’t have a reason to
* touch the nodes created by TinyMCE. Since this only seems to be an issue when rendering TinyMCE 4 directly
* into a root and a fix would be a breaking change, let's just wrap the editor in a <div> here for now.
*/
ReactDOM.render(<div><Editor ref={ref} apiKey='no-api-key' {...props} init={init} /></div>, getRoot());
});

// By rendering the Editor into the same root, React will perform a diff and update.
const cReRender = (props: Partial<IAllProps>) => Chain.op<Context>((context) => {
ReactDOM.render(<div><Editor apiKey='no-api-key' ref={context.ref} {...props} /></div>, getRoot());
});
ReactDOM.render(<div><Editor ref={ref} apiKey='no-api-key' {...props} init={init} /></div>, container);
});

const cRemove = Chain.op((_) => {
ReactDOM.unmountComponentAtNode(getRoot());
});
const remove = () => {
ReactDOM.unmountComponentAtNode(container);
};

const cNamedChainDirect = (name: keyof Context) => NamedChain.direct(
NamedChain.inputName(),
Chain.mapper((res: Context) => res[name]),
name
);
return {
...ctx,
/** By rendering the Editor into the same root, React will perform a diff and update. */
reRender: (newProps: IAllProps) => new Promise<void>((resolve) =>
ReactDOM.render(<div><Editor apiKey='no-api-key' ref={ctx.ref} {...newProps} /></div>, container, resolve)
),
remove,
[Symbol.dispose]: remove
};
};

const cDOMNode = (chain: Chain<Context['DOMNode'], unknown>): Chain<Context, Context> => NamedChain.asChain<Context>([
cNamedChainDirect('DOMNode'),
NamedChain.read('DOMNode', chain),
NamedChain.outputInput
]);
type RenderWithVersion = (
props: Omit<IAllProps, 'cloudChannel' | 'tinymceScriptSrc'>,
container?: HTMLElement | HTMLDivElement
) => Promise<ReactEditorContext>;

const cEditor = (chain: Chain<Context['editor'], unknown>): Chain<Context, Context> => NamedChain.asChain<Context>([
cNamedChainDirect('editor'),
NamedChain.read('editor', chain),
NamedChain.outputInput
]);
export const withVersion = (version: Version, fn: (render: RenderWithVersion) => void): void => {
context(`TinyMCE (${version})`, () => {
before(async () => {
await VersionLoader.pLoadVersion(version);
});

export {
cRender,
cReRender,
cRemove,
cDOMNode,
cEditor
fn(render as RenderWithVersion);
});
};
30 changes: 12 additions & 18 deletions src/test/ts/alien/TestHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Chain, Assertions } from '@ephox/agar';
import { Assertions } from '@ephox/agar';
import { Cell, Obj } from '@ephox/katamari';
import { ApiChains } from '@ephox/mcagar';
import { Version } from 'src/main/ts/components/Editor';
import { Editor as TinyMCEEditor } from 'tinymce';

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

const VALID_API_KEY = 'qagffr3pkuv17a8on1afax661irst1hbr4e6tbv888sz91jc';

const EventStore = () => {
const state: Cell<Record<string, EventHandlerArgs<unknown>[]>> = Cell({});

Expand All @@ -30,32 +31,25 @@ const EventStore = () => {
});
};

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

const cClearState = Chain.op(() => {
const clearState = () => {
state.set({});
});
};

return {
cEach,
each,
createHandler,
cClearState
clearState
};
};

// casting needed due to fake types used in mcagar
const cSetContent = (content: string) => ApiChains.cSetContent(content) as unknown as Chain<TinyMCEEditor, TinyMCEEditor>;
const cAssertContent = (content: string) => ApiChains.cAssertContent(content) as unknown as Chain<TinyMCEEditor, TinyMCEEditor>;

export {
VALID_API_KEY,
EventStore,
cSetContent,
cAssertContent,
VERSIONS,
CLOUD_VERSIONS,
Version
Expand Down
Loading

0 comments on commit 1cd38cf

Please sign in to comment.