Skip to content

Commit

Permalink
fix(extension-callout): create valid node with input rule and enter k…
Browse files Browse the repository at this point in the history
…ey (#948)

Fixes the input rule for callouts, hitting enter after :::info (for example) would create an empty callout (with no paragraph), this is invalid according to the schema, so the callout node would not be added.

Also adds a toBeValidNode matcher to jest-prosemirror to assert that a created node is actually valid according to the schema.
  • Loading branch information
whawker committed May 10, 2021
1 parent f9780e6 commit 5c981d9
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 2 deletions.
7 changes: 7 additions & 0 deletions .changeset/cool-toys-allow.md
@@ -0,0 +1,7 @@
---
'jest-prosemirror': minor
'@remirror/extension-callout': patch
---

- Add `toBeValidNode` matcher to assert valid marks and content
- Fix callout extension input rule followed by enter key
49 changes: 49 additions & 0 deletions packages/jest-prosemirror/src/jest-prosemirror-matchers.ts
Expand Up @@ -113,6 +113,31 @@ export const prosemirrorMatchers = {

return { pass, message };
},

toBeValidNode(this: jest.MatcherUtils, actual: TaggedProsemirrorNode) {
let pass = true;
let errorMessage = '';

try {
actual.check();
} catch (error) {
if (error instanceof RangeError) {
pass = false;
errorMessage = error.message;
}
}

const message = pass
? () =>
`${this.utils.matcherHint('.not.toBeValidNode')}\n\n` +
`Expected Prosemirror node not to conform to schema, but it was valid.`
: () =>
`this.utils.matcherHint('.toBeValidNode')}\n\n` +
`Expected Prosemirror node to conform to schema, but an error was thrown.\n` +
`Error: ${this.utils.printReceived(errorMessage)}`;

return { pass, message };
},
};

declare global {
Expand Down Expand Up @@ -189,6 +214,30 @@ declare global {
* ```
*/
toEqualProsemirrorNode: (params: _ProsemirrorNode) => R;

/**
* Tests that a given node conforms to the schema - the node (and it's
* descendants) have valid content and marks.
*
* ```ts
* import { createEditor, doc, p } from 'jest-prosemirror';
* import { removeNodeAtPosition } from '@remirror/core-utils';
*
* test('inputRules', () => {
* const {
* add,
* nodes: { p, doc, blockquote },
* } = create();
*
* add(doc(p('<cursor>')))
* .insertText('> I am a blockquote')
* .callback((content) => {
* expect(content.state.doc).toBeValidNode();
* });
* });
* ```
*/
toBeValidNode: () => R;
}
}
}
@@ -0,0 +1,28 @@
import 'remirror/styles/all.css';

import React from 'react';
import { CalloutExtension } from 'remirror/extensions';
import { htmlToProsemirrorNode } from '@remirror/core';
import { Remirror, ThemeProvider, useRemirror } from '@remirror/react';

export default { title: 'Callouts' };

const basicExtensions = () => [new CalloutExtension()];

export const Basic: React.FC = () => {
const { manager, state, onChange } = useRemirror({
extensions: basicExtensions,
content:
'<div data-callout-type="info"><p>Info callout</p></div><p />' +
'<div data-callout-type="warning"><p>Warning callout</p></div><p />' +
'<div data-callout-type="error"><p>Error callout</p></div><p />' +
'<div data-callout-type="success"><p>Success callout</p></div>',
stringHandler: htmlToProsemirrorNode,
});

return (
<ThemeProvider>
<Remirror manager={manager} autoFocus onChange={onChange} state={state} autoRender='end' />
</ThemeProvider>
);
};
53 changes: 53 additions & 0 deletions packages/remirror__extension-callout/__stories__/tsconfig.json
@@ -0,0 +1,53 @@
{
"__AUTO_GENERATED__": [
"To update the configuration edit the following field.",
"`package.json > @remirror > tsconfigs > '__stories__'`",
"",
"Then run: `pnpm -w generate:ts`"
],
"extends": "../../../support/tsconfig.base.json",
"compilerOptions": {
"types": [],
"declaration": false,
"noEmit": true,
"skipLibCheck": true,
"importsNotUsedAsValues": "remove",
"paths": {
"react": [
"../../../node_modules/.pnpm/@types+react@17.0.3/node_modules/@types/react/index.d.ts"
],
"react/jsx-dev-runtime": [
"../../../node_modules/.pnpm/@types+react@17.0.3/node_modules/@types/react/jsx-dev-runtime.d.ts"
],
"react/jsx-runtime": [
"../../../node_modules/.pnpm/@types+react@17.0.3/node_modules/@types/react/jsx-runtime.d.ts"
],
"react-dom": [
"../../../node_modules/.pnpm/@types+react-dom@17.0.3/node_modules/@types/react-dom/index.d.ts"
],
"reakit": [
"../../../node_modules/.pnpm/reakit@1.3.7_react-dom@17.0.2+react@17.0.2/node_modules/reakit/ts/index.d.ts"
],
"@remirror/react": ["../../remirror__react/src/index.ts"],
"@storybook/react": [
"../../../node_modules/.pnpm/@storybook+react@6.2.8_fab8b5747c4374c1ef861a7534eabef4/node_modules/@storybook/react/types-6-0.d.ts"
],
"@remirror/dev": ["../../remirror__dev/src/index.ts"]
}
},
"include": ["./"],
"references": [
{
"path": "../../testing/src"
},
{
"path": "../../remirror/src"
},
{
"path": "../../remirror__core/src"
},
{
"path": "../../remirror__messages/src"
}
]
}
Expand Up @@ -253,3 +253,107 @@ describe('plugin', () => {
});
});
});

describe('inputRules', () => {
const {
add,
nodes: { p, doc },
attributeNodes: { callout },
} = create();

describe('valid content', () => {
it('followed by a space', () => {
const { state } = add(doc(p('<cursor>'))).insertText(':::info ');

expect(state.doc).toBeValidNode();
});

it('followed by enter', () => {
const { state } = add(doc(p('<cursor>')))
.insertText(':::info')
.press('Enter');

expect(state.doc).toBeValidNode();
});
});

describe(':::info', () => {
it('followed by space creates an info callout', () => {
const { state } = add(doc(p('<cursor>'))).insertText(':::info ');

expect(state.doc).toEqualRemirrorDocument(doc(callout({ type: 'info' })(p(''))));
});

it('followed by enter creates an info callout', () => {
const { state } = add(doc(p('<cursor>')))
.insertText(':::info')
.press('Enter');

expect(state.doc).toEqualRemirrorDocument(doc(callout({ type: 'info' })(p(''))));
});
});

describe(':::warning', () => {
it('followed by space creates a warning callout', () => {
const { state } = add(doc(p('<cursor>'))).insertText(':::warning ');

expect(state.doc).toEqualRemirrorDocument(doc(callout({ type: 'warning' })(p(''))));
});

it('followed by enter creates a warning callout', () => {
const { state } = add(doc(p('<cursor>')))
.insertText(':::warning')
.press('Enter');

expect(state.doc).toEqualRemirrorDocument(doc(callout({ type: 'warning' })(p(''))));
});
});

describe(':::error', () => {
it('followed by space creates an error callout', () => {
const { state } = add(doc(p('<cursor>'))).insertText(':::error ');

expect(state.doc).toEqualRemirrorDocument(doc(callout({ type: 'error' })(p(''))));
});

it('followed by enter creates an error callout', () => {
const { state } = add(doc(p('<cursor>')))
.insertText(':::error')
.press('Enter');

expect(state.doc).toEqualRemirrorDocument(doc(callout({ type: 'error' })(p(''))));
});
});

describe(':::success', () => {
it('followed by space creates a success callout', () => {
const { state } = add(doc(p('<cursor>'))).insertText(':::success ');

expect(state.doc).toEqualRemirrorDocument(doc(callout({ type: 'success' })(p(''))));
});

it('followed by enter creates a success callout', () => {
const { state } = add(doc(p('<cursor>')))
.insertText(':::success')
.press('Enter');

expect(state.doc).toEqualRemirrorDocument(doc(callout({ type: 'success' })(p(''))));
});
});

describe('unknown type', () => {
it('followed by space creates the default type callout', () => {
const { state } = add(doc(p('<cursor>'))).insertText(':::unknown ');

expect(state.doc).toEqualRemirrorDocument(doc(callout({ type: 'info' })(p(''))));
});

it('followed by enter creates the default type callout', () => {
const { state } = add(doc(p('<cursor>')))
.insertText(':::invalid')
.press('Enter');

expect(state.doc).toEqualRemirrorDocument(doc(callout({ type: 'info' })(p(''))));
});
});
});
6 changes: 4 additions & 2 deletions packages/remirror__extension-callout/src/callout-extension.ts
Expand Up @@ -18,6 +18,7 @@ import {
omitExtraAttributes,
toggleWrap,
} from '@remirror/core';
import { Fragment, Slice } from '@remirror/pm/model';
import { TextSelection } from '@remirror/pm/state';

import type { CalloutAttributes, CalloutOptions } from './callout-types';
Expand Down Expand Up @@ -171,9 +172,10 @@ export class CalloutExtension extends NodeExtension<CalloutOptions> {
// +1 to account for the extra pos a node takes up

if (dispatch) {
tr.replaceWith(pos, end, this.type.create({ type }));
const slice = new Slice(Fragment.from(this.type.create({ type })), 0, 1);
tr.replace(pos, end, slice);

// Set the selection to within the codeBlock
// Set the selection to within the callout
tr.setSelection(TextSelection.create(tr.doc, pos + 1));
dispatch(tr);
}
Expand Down
3 changes: 3 additions & 0 deletions support/root/tsconfig.json
Expand Up @@ -140,6 +140,9 @@
{
"path": "packages/remirror__extension-bold/src"
},
{
"path": "packages/remirror__extension-callout/__stories__"
},
{
"path": "packages/remirror__extension-callout/__tests__"
},
Expand Down

0 comments on commit 5c981d9

Please sign in to comment.