Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

INT-3297: Refactor tests to use BDD style #516

Merged
merged 11 commits into from
Apr 24, 2024
Merged
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.
}
}
]
}
129 changes: 68 additions & 61 deletions src/test/ts/alien/Loader.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { Chain, NamedChain } from '@ephox/agar';
import { Fun, Optional } from '@ephox/katamari';
import { SugarElement, SugarNode } from '@ephox/sugar';
import { Arr, Fun, Global, Optional, Strings } from '@ephox/katamari';
import { Attribute, Remove, SelectorFilter, 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 as TinyMCEEditor } from 'tinymce';
import { ScriptLoader } from '../../../main/ts/ScriptLoader2';

// @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 +23,80 @@ 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>;
danoaky-tiny marked this conversation as resolved.
Show resolved Hide resolved
remove(): void;
}

const deleteTinymce = () => {
ScriptLoader.reinitialize();

delete Global.tinymce;
delete Global.tinyMCE;
danoaky-tiny marked this conversation as resolved.
Show resolved Hide resolved
const hasTinymceUri = (attrName: string) => (elm: SugarElement<Element>) =>
Attribute.getOpt(elm, attrName).exists((src) => Strings.contains(src, 'tinymce'));

const elements = Arr.flatten([
Arr.filter(SelectorFilter.all('script'), hasTinymceUri('src')),
Arr.filter(SelectorFilter.all('link'), hasTinymceUri('href')),
]);

const cRender = (props: Partial<IAllProps>) => Chain.async<unknown, Context>((_, next, die) => {
Arr.each(elements, Remove.remove);
};

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 context = 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());
});
ReactDOM.render(<div><Editor ref={ref} apiKey='no-api-key' {...props} init={init} /></div>, container);
});

// 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());
});

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

const cNamedChainDirect = (name: keyof Context) => NamedChain.direct(
NamedChain.inputName(),
Chain.mapper((res: Context) => res[name]),
name
);

const cDOMNode = (chain: Chain<Context['DOMNode'], unknown>): Chain<Context, Context> => NamedChain.asChain<Context>([
cNamedChainDirect('DOMNode'),
NamedChain.read('DOMNode', chain),
NamedChain.outputInput
]);

const cEditor = (chain: Chain<Context['editor'], unknown>): Chain<Context, Context> => NamedChain.asChain<Context>([
cNamedChainDirect('editor'),
NamedChain.read('editor', chain),
NamedChain.outputInput
]);
const remove = () => {
ReactDOM.unmountComponentAtNode(container);
deleteTinymce();
};
danoaky-tiny marked this conversation as resolved.
Show resolved Hide resolved

export {
cRender,
cReRender,
cRemove,
cDOMNode,
cEditor
return {
...context,
/** 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={context.ref} {...newProps} /></div>, container, resolve)
),
remove,
[Symbol.dispose]: remove
};
};
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
Loading