Skip to content

Commit

Permalink
test: improve tests for prosemirror-suggest
Browse files Browse the repository at this point in the history
  • Loading branch information
ifiokjr committed Sep 19, 2019
1 parent 9c1637e commit 517d29c
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 16 deletions.
54 changes: 52 additions & 2 deletions packages/jest-prosemirror/src/jest-prosemirror-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import {
CommandFunction,
EditorSchema,
EditorState,
EditorStateParams,
findElementAtPosition,
InputRule,
isElementDOMNode,
isTextDOMNode,
pick,
PlainObject,
Plugin,
PosParams,
SelectionParams,
TextParams,
} from '@remirror/core';
import { EventType, fireEvent } from '@testing-library/dom';
Expand Down Expand Up @@ -114,17 +117,43 @@ export const createEditor = <GSchema extends EditorSchema = any>(

const createReturnValue = () => {
const { selection, doc } = view.state;
return {
const returnValue = {
start: selection.from,
selection,
end: selection.to,
state: view.state,
view,
schema: view.state.schema,
insertText(text: string) {
/**
* Overwrite all the current content within the editor.
*/
overwrite: (newDoc: TaggedProsemirrorNode<GSchema>) => {
const tr = view.state.tr.replaceWith(0, view.state.doc.nodeSize - 2, newDoc);
tr.setMeta('addToHistory', false);
view.dispatch(tr);
return createReturnValue();
},

/**
* Run the command within the editor context.
*/
command: (command: CommandFunction) => {
command(view.state, view.dispatch, view);
return createReturnValue();
},

/**
* Insert text into the editor at the current position.
*/
insertText: (text: string) => {
const { from } = view.state.selection;
insertText({ start: from, text, view });
return createReturnValue();
},

/**
* Jump to the position in the editor.
*/
jumpTo: (pos: 'start' | 'end' | number, endPos?: number) => {
if (pos === 'start') {
dispatchTextSelection({ view, start: 1 });
Expand All @@ -135,10 +164,16 @@ export const createEditor = <GSchema extends EditorSchema = any>(
}
return createReturnValue();
},

shortcut: (text: string) => {
shortcut({ shortcut: text, view });
return createReturnValue();
},
/**
* Simulate a keypress which is run through the editor's key handlers.
* **NOTE** This only simulates. For example an `Enter` would run all enter key
* handlers but not actually create a new line.
*/
press: (char: string) => {
press({ char, view });
return createReturnValue();
Expand All @@ -147,12 +182,27 @@ export const createEditor = <GSchema extends EditorSchema = any>(
fireEventAtPosition({ view, ...params });
return createReturnValue();
},
callback: (fn: (content: ReturnValueCallbackParams<GSchema>) => void) => {
fn(pick(returnValue, ['start', 'end', 'state', 'view', 'schema', 'selection']));
return createReturnValue();
},
};

return returnValue;
};

return createReturnValue();
};

interface ReturnValueCallbackParams<GSchema extends EditorSchema = any>
extends TestEditorViewParams<GSchema>,
EditorStateParams<GSchema>,
SelectionParams<GSchema> {
start: number;
end: number;
schema: GSchema;
}

export interface InsertTextParams<GSchema extends EditorSchema = any>
extends TestEditorViewParams<GSchema>,
TextParams {
Expand Down
61 changes: 58 additions & 3 deletions packages/prosemirror-suggest/src/__tests__/suggest-plugin.spec.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,49 @@
import { createEditor, doc, p } from 'jest-prosemirror';
import { ExitReason } from '../suggest-constants';
import { suggest } from '../suggest-plugin';
import { SuggestExitHandlerParams } from '../suggest-types';
import { SuggestExitHandlerParams, SuggestKeyBindingParams } from '../suggest-types';

test('`onChange` and `onExit` handlers are called', () => {
expect.assertions(3);
test('`onChange`, `onExit` and `createCommand` handlers are called', () => {
const command = jest.fn();
const expected = 'suggest';
const handlers = {
onExit: jest.fn((params: SuggestExitHandlerParams) => {
params.command('command');
expect(params.query.full).toBe(expected);
expect(params.reason).toBe(ExitReason.MoveEnd);
}),
onChange: jest.fn(),
createCommand: () => command,
};
const plugin = suggest({ char: '@', name: 'at', ...handlers, matchOffset: 0 });
const editor = createEditor(doc(p('<cursor>')), { plugins: [plugin] }).insertText('@');
expect(handlers.onChange).toHaveBeenCalledTimes(1);

editor.insertText(`${expected} `);
expect(handlers.onChange).toHaveBeenCalledTimes(8);
expect(command).toHaveBeenCalledWith('command');
});

test('`keyBindings`', () => {
const keyBindings = {
Enter: jest.fn((params: SuggestKeyBindingParams) => {
params.command();
}),
};
const plugin = suggest({
char: '@',
name: 'at',
keyBindings,
matchOffset: 0,
createCommand: ({ view }) => () => view.dispatch(view.state.tr.insertText('awesome')),
});

createEditor(doc(p('<cursor>')), { plugins: [plugin] })
.insertText('@')
.press('Enter')
.callback(content => {
expect(content.state.doc).toEqualPMNode(doc(p('@awesome')));
});
});

test('`onChange` not called for the char when matchOffset is greater than 0', () => {
Expand All @@ -27,3 +54,31 @@ test('`onChange` not called for the char when matchOffset is greater than 0', ()
createEditor(doc(p('<cursor>')), { plugins: [plugin] }).insertText('@');
expect(handlers.onChange).not.toHaveBeenCalled();
});

test('handles jumping with two suggesters', () => {
const handlers1 = {
onChange: jest.fn(),
onExit: jest.fn(),
};
const handlers2 = {
onChange: jest.fn(),
onExit: jest.fn(),
};
const plugin = suggest({ char: '@', name: 'at', ...handlers1 }, { char: '#', name: 'hash', ...handlers2 });

createEditor(doc(p('<cursor>')), { plugins: [plugin] })
.insertText('@abc #xyz')
.callback(() => {
expect(handlers1.onChange).toHaveBeenCalledTimes(4);
expect(handlers1.onExit).toHaveBeenCalledTimes(1);
expect(handlers2.onChange).toHaveBeenCalledTimes(4);
jest.clearAllMocks();
})
.jumpTo(5)
.callback(() => {
expect(handlers1.onChange).toHaveBeenCalledTimes(1);
expect(handlers1.onExit).not.toHaveBeenCalled();
expect(handlers2.onExit).toHaveBeenCalledTimes(1);
expect(handlers2.onChange).not.toHaveBeenCalled();
});
});
9 changes: 5 additions & 4 deletions packages/prosemirror-suggest/src/suggest-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const DEFAULT_SUGGESTER = {
getStage: () => 'new' as const,
ignoreDecorations: false,
validPrefixCharacters: /^[\s\0]?$/,
invalidPrefixCharacters: undefined,
invalidPrefixCharacters: undefined as any,
};

/**
Expand Down Expand Up @@ -52,7 +52,7 @@ export enum ActionTaken {
*/
export enum ExitReason {
/**
* The user has pasted some text with multiple characters or run a command that adds multiple character.
* The user has pasted some text with multiple characters or run a command that adds multiple characters.
*
* `onExit` should be called but the previous match should be retested as it's possible that it's been extended.
*/
Expand Down Expand Up @@ -82,8 +82,9 @@ export enum ExitReason {
InvalidSplit = 'invalid-exit-split',

/**
* User has moved out of the suggestion at the end. This will typically be using arrow keys, but can also be
* via a mouse click or custom command. All that has changed is the cursor position.
* User has moved out of the suggestion at the end. This can happen via using arrow keys, but can also be
* via the suggestion no longer matching as the user types, a mouse click or custom command.
* All that has changed is the cursor position.
*/
MoveEnd = 'move-end',

Expand Down
9 changes: 4 additions & 5 deletions packages/prosemirror-suggest/src/suggest-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,7 @@ export class SuggestState<GSchema extends EditorSchema = any> {
* Provides the current stage of the mention.
*/
get stage(): SuggestStage {
return this.match && this.match.suggester.getStage({ match: this.match, state: this.view.state })
? 'edit'
: 'new';
return this.match ? this.match.suggester.getStage({ match: this.match, state: this.view.state }) : 'new';
}

// TODO Check for duplicate names and characters and log warnings when these
Expand Down Expand Up @@ -158,8 +156,9 @@ export class SuggestState<GSchema extends EditorSchema = any> {
const changeParams = this.createReasonParams(change);
const movedForwards = exit.range.from < change.range.from;

movedForwards ? onChange(changeParams) : onExit(exitParams);
movedForwards ? onExit(exitParams) : onChange(changeParams);
movedForwards ? change.suggester.onChange(changeParams) : exit.suggester.onExit(exitParams);
movedForwards ? exit.suggester.onExit(exitParams) : change.suggester.onChange(changeParams);
return;
}

if (change) {
Expand Down
4 changes: 2 additions & 2 deletions packages/prosemirror-suggest/src/suggest-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,9 @@ export interface SuggestMatcher {
* This has preference over the `validPrefixCharacters` option and when it is
* defined only it will be looked at in determining whether a prefix is valid.
*
* @defaultValue `undefined`
* @defaultValue ''
*/
invalidPrefixCharacters: RegExp | string | undefined;
invalidPrefixCharacters: RegExp | string;

/**
* Name of matching character - This will be appended to the classnames
Expand Down

0 comments on commit 517d29c

Please sign in to comment.