Merge pull request #3247 from matrix-org/bwindels/editortests
Unit tests for new editor
This commit is contained in:
commit
e855a056c7
11 changed files with 1018 additions and 44 deletions
|
@ -26,7 +26,7 @@ import {htmlSerializeIfNeeded, textSerialize} from '../../../editor/serialize';
|
||||||
import {findEditableEvent} from '../../../utils/EventUtils';
|
import {findEditableEvent} from '../../../utils/EventUtils';
|
||||||
import {parseEvent} from '../../../editor/deserialize';
|
import {parseEvent} from '../../../editor/deserialize';
|
||||||
import Autocomplete from '../rooms/Autocomplete';
|
import Autocomplete from '../rooms/Autocomplete';
|
||||||
import {PartCreator} from '../../../editor/parts';
|
import {PartCreator, autoCompleteCreator} from '../../../editor/parts';
|
||||||
import {renderModel} from '../../../editor/render';
|
import {renderModel} from '../../../editor/render';
|
||||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||||
import {MatrixClient} from 'matrix-js-sdk';
|
import {MatrixClient} from 'matrix-js-sdk';
|
||||||
|
@ -303,8 +303,7 @@ export default class MessageEditor extends React.Component {
|
||||||
const {editState} = this.props;
|
const {editState} = this.props;
|
||||||
const room = this._getRoom();
|
const room = this._getRoom();
|
||||||
const partCreator = new PartCreator(
|
const partCreator = new PartCreator(
|
||||||
() => this._autocompleteRef,
|
autoCompleteCreator(() => this._autocompleteRef, query => this.setState({query})),
|
||||||
query => this.setState({query}),
|
|
||||||
room,
|
room,
|
||||||
this.context.matrixClient,
|
this.context.matrixClient,
|
||||||
);
|
);
|
||||||
|
|
|
@ -40,7 +40,7 @@ export function setCaretPosition(editor, model, caretPosition) {
|
||||||
sel.addRange(range);
|
sel.addRange(range);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLineAndNodePosition(model, caretPosition) {
|
export function getLineAndNodePosition(model, caretPosition) {
|
||||||
const {parts} = model;
|
const {parts} = model;
|
||||||
const partIndex = caretPosition.index;
|
const partIndex = caretPosition.index;
|
||||||
const lineResult = findNodeInLineForPart(parts, partIndex);
|
const lineResult = findNodeInLineForPart(parts, partIndex);
|
||||||
|
|
|
@ -25,16 +25,6 @@ function firstDiff(a, b) {
|
||||||
return compareLen;
|
return compareLen;
|
||||||
}
|
}
|
||||||
|
|
||||||
function lastDiff(a, b) {
|
|
||||||
const compareLen = Math.min(a.length, b.length);
|
|
||||||
for (let i = 0; i < compareLen; ++i) {
|
|
||||||
if (a[a.length - i] !== b[b.length - i]) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return compareLen;
|
|
||||||
}
|
|
||||||
|
|
||||||
function diffStringsAtEnd(oldStr, newStr) {
|
function diffStringsAtEnd(oldStr, newStr) {
|
||||||
const len = Math.min(oldStr.length, newStr.length);
|
const len = Math.min(oldStr.length, newStr.length);
|
||||||
const startInCommon = oldStr.substr(0, len) === newStr.substr(0, len);
|
const startInCommon = oldStr.substr(0, len) === newStr.substr(0, len);
|
||||||
|
@ -52,24 +42,25 @@ function diffStringsAtEnd(oldStr, newStr) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// assumes only characters have been deleted at one location in the string, and none added
|
||||||
export function diffDeletion(oldStr, newStr) {
|
export function diffDeletion(oldStr, newStr) {
|
||||||
if (oldStr === newStr) {
|
if (oldStr === newStr) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const firstDiffIdx = firstDiff(oldStr, newStr);
|
const firstDiffIdx = firstDiff(oldStr, newStr);
|
||||||
const lastDiffIdx = oldStr.length - lastDiff(oldStr, newStr) + 1;
|
const amount = oldStr.length - newStr.length;
|
||||||
return {at: firstDiffIdx, removed: oldStr.substring(firstDiffIdx, lastDiffIdx)};
|
return {at: firstDiffIdx, removed: oldStr.substr(firstDiffIdx, amount)};
|
||||||
}
|
|
||||||
|
|
||||||
export function diffInsertion(oldStr, newStr) {
|
|
||||||
const diff = diffDeletion(newStr, oldStr);
|
|
||||||
if (diff.removed) {
|
|
||||||
return {at: diff.at, added: diff.removed};
|
|
||||||
} else {
|
|
||||||
return diff;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates which string was added and removed around the caret position
|
||||||
|
* @param {String} oldValue the previous value
|
||||||
|
* @param {String} newValue the new value
|
||||||
|
* @param {Number} caretPosition the position of the caret after `newValue` was applied.
|
||||||
|
* @return {object} an object with `at` as the offset where characters were removed and/or added,
|
||||||
|
* `added` with the added string (if any), and
|
||||||
|
* `removed` with the removed string (if any)
|
||||||
|
*/
|
||||||
export function diffAtCaret(oldValue, newValue, caretPosition) {
|
export function diffAtCaret(oldValue, newValue, caretPosition) {
|
||||||
const diffLen = newValue.length - oldValue.length;
|
const diffLen = newValue.length - oldValue.length;
|
||||||
const caretPositionBeforeInput = caretPosition - diffLen;
|
const caretPositionBeforeInput = caretPosition - diffLen;
|
||||||
|
|
|
@ -117,7 +117,8 @@ class BasePart {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlainPart extends BasePart {
|
// exported for unit tests, should otherwise only be used through PartCreator
|
||||||
|
export class PlainPart extends BasePart {
|
||||||
acceptsInsertion(chr) {
|
acceptsInsertion(chr) {
|
||||||
return chr !== "@" && chr !== "#" && chr !== ":" && chr !== "\n";
|
return chr !== "@" && chr !== "#" && chr !== ":" && chr !== "\n";
|
||||||
}
|
}
|
||||||
|
@ -348,18 +349,24 @@ class PillCandidatePart extends PlainPart {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PartCreator {
|
export function autoCompleteCreator(updateQuery, getAutocompleterComponent) {
|
||||||
constructor(getAutocompleterComponent, updateQuery, room, client) {
|
return (partCreator) => {
|
||||||
this._room = room;
|
return (updateCallback) => {
|
||||||
this._client = client;
|
|
||||||
this._autoCompleteCreator = (updateCallback) => {
|
|
||||||
return new AutocompleteWrapperModel(
|
return new AutocompleteWrapperModel(
|
||||||
updateCallback,
|
updateCallback,
|
||||||
getAutocompleterComponent,
|
getAutocompleterComponent,
|
||||||
updateQuery,
|
updateQuery,
|
||||||
this,
|
partCreator,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PartCreator {
|
||||||
|
constructor(autoCompleteCreator, room, client) {
|
||||||
|
this._room = room;
|
||||||
|
this._client = client;
|
||||||
|
this._autoCompleteCreator = autoCompleteCreator(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
createPartForInput(input) {
|
createPartForInput(input) {
|
||||||
|
|
|
@ -56,15 +56,3 @@ export function textSerialize(model) {
|
||||||
}
|
}
|
||||||
}, "");
|
}, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requiresHtml(model) {
|
|
||||||
return model.parts.some(part => {
|
|
||||||
switch (part.type) {
|
|
||||||
case "room-pill":
|
|
||||||
case "user-pill":
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
205
test/editor/caret-test.js
Normal file
205
test/editor/caret-test.js
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import expect from 'expect';
|
||||||
|
import {getLineAndNodePosition} from "../../src/editor/caret";
|
||||||
|
import EditorModel from "../../src/editor/model";
|
||||||
|
import {createPartCreator} from "./mock";
|
||||||
|
|
||||||
|
describe('editor/caret: DOM position for caret', function() {
|
||||||
|
describe('basic text handling', function() {
|
||||||
|
it('at end of single line', function() {
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.plain("hello"),
|
||||||
|
]);
|
||||||
|
const {offset, lineIndex, nodeIndex} =
|
||||||
|
getLineAndNodePosition(model, {index: 0, offset: 5});
|
||||||
|
expect(lineIndex).toBe(0);
|
||||||
|
expect(nodeIndex).toBe(0);
|
||||||
|
expect(offset).toBe(5);
|
||||||
|
});
|
||||||
|
it('at start of single line', function() {
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.plain("hello"),
|
||||||
|
]);
|
||||||
|
const {offset, lineIndex, nodeIndex} =
|
||||||
|
getLineAndNodePosition(model, {index: 0, offset: 0});
|
||||||
|
expect(lineIndex).toBe(0);
|
||||||
|
expect(nodeIndex).toBe(0);
|
||||||
|
expect(offset).toBe(0);
|
||||||
|
});
|
||||||
|
it('at middle of single line', function() {
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.plain("hello"),
|
||||||
|
]);
|
||||||
|
const {offset, lineIndex, nodeIndex} =
|
||||||
|
getLineAndNodePosition(model, {index: 0, offset: 2});
|
||||||
|
expect(lineIndex).toBe(0);
|
||||||
|
expect(nodeIndex).toBe(0);
|
||||||
|
expect(offset).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('handling line breaks', function() {
|
||||||
|
it('at end of last line', function() {
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.plain("hello"),
|
||||||
|
pc.newline(),
|
||||||
|
pc.plain("world"),
|
||||||
|
]);
|
||||||
|
const {offset, lineIndex, nodeIndex} =
|
||||||
|
getLineAndNodePosition(model, {index: 2, offset: 5});
|
||||||
|
expect(lineIndex).toBe(1);
|
||||||
|
expect(nodeIndex).toBe(0);
|
||||||
|
expect(offset).toBe(5);
|
||||||
|
});
|
||||||
|
it('at start of last line', function() {
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.plain("hello"),
|
||||||
|
pc.newline(),
|
||||||
|
pc.plain("world"),
|
||||||
|
]);
|
||||||
|
const {offset, lineIndex, nodeIndex} =
|
||||||
|
getLineAndNodePosition(model, {index: 2, offset: 0});
|
||||||
|
expect(lineIndex).toBe(1);
|
||||||
|
expect(nodeIndex).toBe(0);
|
||||||
|
expect(offset).toBe(0);
|
||||||
|
});
|
||||||
|
it('in empty line', function() {
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.plain("hello"),
|
||||||
|
pc.newline(),
|
||||||
|
pc.newline(),
|
||||||
|
pc.plain("world"),
|
||||||
|
]);
|
||||||
|
const {offset, lineIndex, nodeIndex} =
|
||||||
|
getLineAndNodePosition(model, {index: 1, offset: 1});
|
||||||
|
expect(lineIndex).toBe(1);
|
||||||
|
expect(nodeIndex).toBe(-1);
|
||||||
|
expect(offset).toBe(0);
|
||||||
|
});
|
||||||
|
it('after empty line', function() {
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.plain("hello"),
|
||||||
|
pc.newline(),
|
||||||
|
pc.newline(),
|
||||||
|
pc.plain("world"),
|
||||||
|
]);
|
||||||
|
const {offset, lineIndex, nodeIndex} =
|
||||||
|
getLineAndNodePosition(model, {index: 3, offset: 0});
|
||||||
|
expect(lineIndex).toBe(2);
|
||||||
|
expect(nodeIndex).toBe(0);
|
||||||
|
expect(offset).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('handling non-editable parts and caret nodes', function() {
|
||||||
|
it('at start of non-editable part (with plain text around)', function() {
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.plain("hello"),
|
||||||
|
pc.userPill("Alice", "@alice:hs.tld"),
|
||||||
|
pc.plain("!"),
|
||||||
|
]);
|
||||||
|
const {offset, lineIndex, nodeIndex} =
|
||||||
|
getLineAndNodePosition(model, {index: 1, offset: 0});
|
||||||
|
expect(lineIndex).toBe(0);
|
||||||
|
expect(nodeIndex).toBe(0);
|
||||||
|
expect(offset).toBe(5);
|
||||||
|
});
|
||||||
|
it('in middle of non-editable part (with plain text around)', function() {
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.plain("hello"),
|
||||||
|
pc.userPill("Alice", "@alice:hs.tld"),
|
||||||
|
pc.plain("!"),
|
||||||
|
]);
|
||||||
|
const {offset, lineIndex, nodeIndex} =
|
||||||
|
getLineAndNodePosition(model, {index: 1, offset: 2});
|
||||||
|
expect(lineIndex).toBe(0);
|
||||||
|
expect(nodeIndex).toBe(2);
|
||||||
|
expect(offset).toBe(0);
|
||||||
|
});
|
||||||
|
it('at start of non-editable part (without plain text around)', function() {
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.userPill("Alice", "@alice:hs.tld"),
|
||||||
|
]);
|
||||||
|
const {offset, lineIndex, nodeIndex} =
|
||||||
|
getLineAndNodePosition(model, {index: 0, offset: 0});
|
||||||
|
expect(lineIndex).toBe(0);
|
||||||
|
//presumed nodes on line are (caret, pill, caret)
|
||||||
|
expect(nodeIndex).toBe(0);
|
||||||
|
expect(offset).toBe(0);
|
||||||
|
});
|
||||||
|
it('in middle of non-editable part (without plain text around)', function() {
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.userPill("Alice", "@alice:hs.tld"),
|
||||||
|
]);
|
||||||
|
const {offset, lineIndex, nodeIndex} =
|
||||||
|
getLineAndNodePosition(model, {index: 0, offset: 1});
|
||||||
|
expect(lineIndex).toBe(0);
|
||||||
|
//presumed nodes on line are (caret, pill, caret)
|
||||||
|
expect(nodeIndex).toBe(2);
|
||||||
|
expect(offset).toBe(0);
|
||||||
|
});
|
||||||
|
it('in middle of a first non-editable part, with another one following', function() {
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.userPill("Alice", "@alice:hs.tld"),
|
||||||
|
pc.userPill("Bob", "@bob:hs.tld"),
|
||||||
|
]);
|
||||||
|
const {offset, lineIndex, nodeIndex} =
|
||||||
|
getLineAndNodePosition(model, {index: 0, offset: 1});
|
||||||
|
expect(lineIndex).toBe(0);
|
||||||
|
//presumed nodes on line are (caret, pill, caret, pill, caret)
|
||||||
|
expect(nodeIndex).toBe(2);
|
||||||
|
expect(offset).toBe(0);
|
||||||
|
});
|
||||||
|
it('in start of a second non-editable part, with another one before it', function() {
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.userPill("Alice", "@alice:hs.tld"),
|
||||||
|
pc.userPill("Bob", "@bob:hs.tld"),
|
||||||
|
]);
|
||||||
|
const {offset, lineIndex, nodeIndex} =
|
||||||
|
getLineAndNodePosition(model, {index: 1, offset: 0});
|
||||||
|
expect(lineIndex).toBe(0);
|
||||||
|
//presumed nodes on line are (caret, pill, caret, pill, caret)
|
||||||
|
expect(nodeIndex).toBe(2);
|
||||||
|
expect(offset).toBe(0);
|
||||||
|
});
|
||||||
|
it('in middle of a second non-editable part, with another one before it', function() {
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.userPill("Alice", "@alice:hs.tld"),
|
||||||
|
pc.userPill("Bob", "@bob:hs.tld"),
|
||||||
|
]);
|
||||||
|
const {offset, lineIndex, nodeIndex} =
|
||||||
|
getLineAndNodePosition(model, {index: 1, offset: 1});
|
||||||
|
expect(lineIndex).toBe(0);
|
||||||
|
//presumed nodes on line are (caret, pill, caret, pill, caret)
|
||||||
|
expect(nodeIndex).toBe(4);
|
||||||
|
expect(offset).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
226
test/editor/deserialize-test.js
Normal file
226
test/editor/deserialize-test.js
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import expect from 'expect';
|
||||||
|
import {parseEvent} from "../../src/editor/deserialize";
|
||||||
|
import {createPartCreator} from "./mock";
|
||||||
|
|
||||||
|
function htmlMessage(formattedBody, msgtype = "m.text") {
|
||||||
|
return {
|
||||||
|
getContent() {
|
||||||
|
return {
|
||||||
|
msgtype,
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
formatted_body: formattedBody,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function textMessage(body, msgtype = "m.text") {
|
||||||
|
return {
|
||||||
|
getContent() {
|
||||||
|
return {
|
||||||
|
msgtype,
|
||||||
|
body,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeAdjacentParts(parts) {
|
||||||
|
let prevPart;
|
||||||
|
for (let i = 0; i < parts.length; ++i) {
|
||||||
|
let part = parts[i];
|
||||||
|
const isEmpty = !part.text.length;
|
||||||
|
const isMerged = !isEmpty && prevPart && prevPart.merge(part);
|
||||||
|
if (isEmpty || isMerged) {
|
||||||
|
// remove empty or merged part
|
||||||
|
part = prevPart;
|
||||||
|
parts.splice(i, 1);
|
||||||
|
//repeat this index, as it's removed now
|
||||||
|
--i;
|
||||||
|
}
|
||||||
|
prevPart = part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalize(parts) {
|
||||||
|
// merge adjacent parts as this will happen
|
||||||
|
// in the model anyway, and whether 1 or multiple
|
||||||
|
// plain parts are returned is an implementation detail
|
||||||
|
mergeAdjacentParts(parts);
|
||||||
|
// convert to data objects for easier asserting
|
||||||
|
return parts.map(p => p.serialize());
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('editor/deserialize', function() {
|
||||||
|
describe('text messages', function() {
|
||||||
|
it('test with newlines', function() {
|
||||||
|
const parts = normalize(parseEvent(textMessage("hello\nworld"), createPartCreator()));
|
||||||
|
expect(parts.length).toBe(3);
|
||||||
|
expect(parts[0]).toStrictEqual({type: "plain", text: "hello"});
|
||||||
|
expect(parts[1]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
|
expect(parts[2]).toStrictEqual({type: "plain", text: "world"});
|
||||||
|
});
|
||||||
|
it('@room pill', function() {
|
||||||
|
const parts = normalize(parseEvent(textMessage("text message for @room"), createPartCreator()));
|
||||||
|
expect(parts.length).toBe(2);
|
||||||
|
expect(parts[0]).toStrictEqual({type: "plain", text: "text message for "});
|
||||||
|
expect(parts[1]).toStrictEqual({type: "at-room-pill", text: "@room"});
|
||||||
|
});
|
||||||
|
it('emote', function() {
|
||||||
|
const text = "says DON'T SHOUT!";
|
||||||
|
const parts = normalize(parseEvent(textMessage(text, "m.emote"), createPartCreator()));
|
||||||
|
expect(parts.length).toBe(1);
|
||||||
|
expect(parts[0]).toStrictEqual({type: "plain", text: "/me says DON'T SHOUT!"});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('html messages', function() {
|
||||||
|
it('inline styling', function() {
|
||||||
|
const html = "<strong>bold</strong> and <em>emphasized</em> text";
|
||||||
|
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
||||||
|
expect(parts.length).toBe(1);
|
||||||
|
expect(parts[0]).toStrictEqual({type: "plain", text: "**bold** and *emphasized* text"});
|
||||||
|
});
|
||||||
|
it('hyperlink', function() {
|
||||||
|
const html = 'click <a href="http://example.com/">this</a>!';
|
||||||
|
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
||||||
|
expect(parts.length).toBe(1);
|
||||||
|
expect(parts[0]).toStrictEqual({type: "plain", text: "click [this](http://example.com/)!"});
|
||||||
|
});
|
||||||
|
it('multiple lines with paragraphs', function() {
|
||||||
|
const html = '<p>hello</p><p>world</p>';
|
||||||
|
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
||||||
|
expect(parts.length).toBe(3);
|
||||||
|
expect(parts[0]).toStrictEqual({type: "plain", text: "hello"});
|
||||||
|
expect(parts[1]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
|
expect(parts[2]).toStrictEqual({type: "plain", text: "world"});
|
||||||
|
});
|
||||||
|
it('multiple lines with line breaks', function() {
|
||||||
|
const html = 'hello<br>world';
|
||||||
|
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
||||||
|
expect(parts.length).toBe(3);
|
||||||
|
expect(parts[0]).toStrictEqual({type: "plain", text: "hello"});
|
||||||
|
expect(parts[1]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
|
expect(parts[2]).toStrictEqual({type: "plain", text: "world"});
|
||||||
|
});
|
||||||
|
it('multiple lines mixing paragraphs and line breaks', function() {
|
||||||
|
const html = '<p>hello<br>warm</p><p>world</p>';
|
||||||
|
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
||||||
|
expect(parts.length).toBe(5);
|
||||||
|
expect(parts[0]).toStrictEqual({type: "plain", text: "hello"});
|
||||||
|
expect(parts[1]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
|
expect(parts[2]).toStrictEqual({type: "plain", text: "warm"});
|
||||||
|
expect(parts[3]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
|
expect(parts[4]).toStrictEqual({type: "plain", text: "world"});
|
||||||
|
});
|
||||||
|
it('quote', function() {
|
||||||
|
const html = '<blockquote><p><em>wise</em><br><strong>words</strong></p></blockquote><p>indeed</p>';
|
||||||
|
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
||||||
|
expect(parts.length).toBe(6);
|
||||||
|
expect(parts[0]).toStrictEqual({type: "plain", text: "> *wise*"});
|
||||||
|
expect(parts[1]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
|
expect(parts[2]).toStrictEqual({type: "plain", text: "> **words**"});
|
||||||
|
expect(parts[3]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
|
expect(parts[4]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
|
expect(parts[5]).toStrictEqual({type: "plain", text: "indeed"});
|
||||||
|
});
|
||||||
|
it('user pill', function() {
|
||||||
|
const html = "Hi <a href=\"https://matrix.to/#/@alice:hs.tld\">Alice</a>!";
|
||||||
|
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
||||||
|
expect(parts.length).toBe(3);
|
||||||
|
expect(parts[0]).toStrictEqual({type: "plain", text: "Hi "});
|
||||||
|
expect(parts[1]).toStrictEqual({type: "user-pill", text: "Alice", userId: "@alice:hs.tld"});
|
||||||
|
expect(parts[2]).toStrictEqual({type: "plain", text: "!"});
|
||||||
|
});
|
||||||
|
it('room pill', function() {
|
||||||
|
const html = "Try <a href=\"https://matrix.to/#/#room:hs.tld\">#room:hs.tld</a>?";
|
||||||
|
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
||||||
|
expect(parts.length).toBe(3);
|
||||||
|
expect(parts[0]).toStrictEqual({type: "plain", text: "Try "});
|
||||||
|
expect(parts[1]).toStrictEqual({type: "room-pill", text: "#room:hs.tld"});
|
||||||
|
expect(parts[2]).toStrictEqual({type: "plain", text: "?"});
|
||||||
|
});
|
||||||
|
it('@room pill', function() {
|
||||||
|
const html = "<em>formatted</em> message for @room";
|
||||||
|
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
||||||
|
expect(parts.length).toBe(2);
|
||||||
|
expect(parts[0]).toStrictEqual({type: "plain", text: "*formatted* message for "});
|
||||||
|
expect(parts[1]).toStrictEqual({type: "at-room-pill", text: "@room"});
|
||||||
|
});
|
||||||
|
it('inline code', function() {
|
||||||
|
const html = "there is no place like <code>127.0.0.1</code>!";
|
||||||
|
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
||||||
|
expect(parts.length).toBe(1);
|
||||||
|
expect(parts[0]).toStrictEqual({type: "plain", text: "there is no place like `127.0.0.1`!"});
|
||||||
|
});
|
||||||
|
it('code block with no trailing text', function() {
|
||||||
|
const html = "<pre><code>0xDEADBEEF\n</code></pre>\n";
|
||||||
|
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
||||||
|
console.log(parts);
|
||||||
|
expect(parts.length).toBe(5);
|
||||||
|
expect(parts[0]).toStrictEqual({type: "plain", text: "```"});
|
||||||
|
expect(parts[1]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
|
expect(parts[2]).toStrictEqual({type: "plain", text: "0xDEADBEEF"});
|
||||||
|
expect(parts[3]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
|
expect(parts[4]).toStrictEqual({type: "plain", text: "```"});
|
||||||
|
});
|
||||||
|
// failing likely because of https://github.com/vector-im/riot-web/issues/10316
|
||||||
|
xit('code block with no trailing text and no newlines', function() {
|
||||||
|
const html = "<pre><code>0xDEADBEEF</code></pre>";
|
||||||
|
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
||||||
|
expect(parts.length).toBe(5);
|
||||||
|
expect(parts[0]).toStrictEqual({type: "plain", text: "```"});
|
||||||
|
expect(parts[1]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
|
expect(parts[2]).toStrictEqual({type: "plain", text: "0xDEADBEEF"});
|
||||||
|
expect(parts[3]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
|
expect(parts[4]).toStrictEqual({type: "plain", text: "```"});
|
||||||
|
});
|
||||||
|
it('unordered lists', function() {
|
||||||
|
const html = "<ul><li>Oak</li><li>Spruce</li><li>Birch</li></ul>";
|
||||||
|
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
||||||
|
expect(parts.length).toBe(5);
|
||||||
|
expect(parts[0]).toStrictEqual({type: "plain", text: "- Oak"});
|
||||||
|
expect(parts[1]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
|
expect(parts[2]).toStrictEqual({type: "plain", text: "- Spruce"});
|
||||||
|
expect(parts[3]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
|
expect(parts[4]).toStrictEqual({type: "plain", text: "- Birch"});
|
||||||
|
});
|
||||||
|
it('ordered lists', function() {
|
||||||
|
const html = "<ol><li>Start</li><li>Continue</li><li>Finish</li></ol>";
|
||||||
|
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
||||||
|
expect(parts.length).toBe(5);
|
||||||
|
expect(parts[0]).toStrictEqual({type: "plain", text: "1. Start"});
|
||||||
|
expect(parts[1]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
|
expect(parts[2]).toStrictEqual({type: "plain", text: "1. Continue"});
|
||||||
|
expect(parts[3]).toStrictEqual({type: "newline", text: "\n"});
|
||||||
|
expect(parts[4]).toStrictEqual({type: "plain", text: "1. Finish"});
|
||||||
|
});
|
||||||
|
it('mx-reply is stripped', function() {
|
||||||
|
const html = "<mx-reply>foo</mx-reply>bar";
|
||||||
|
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
|
||||||
|
expect(parts.length).toBe(1);
|
||||||
|
expect(parts[0]).toStrictEqual({type: "plain", text: "bar"});
|
||||||
|
});
|
||||||
|
it('emote', function() {
|
||||||
|
const html = "says <em>DON'T SHOUT</em>!";
|
||||||
|
const parts = normalize(parseEvent(htmlMessage(html, "m.emote"), createPartCreator()));
|
||||||
|
expect(parts.length).toBe(1);
|
||||||
|
expect(parts[0]).toStrictEqual({type: "plain", text: "/me says *DON'T SHOUT*!"});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
146
test/editor/diff-test.js
Normal file
146
test/editor/diff-test.js
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import expect from 'expect';
|
||||||
|
import {diffDeletion, diffAtCaret} from "../../src/editor/diff";
|
||||||
|
|
||||||
|
describe('editor/diff', function() {
|
||||||
|
describe('diffDeletion', function() {
|
||||||
|
describe('with a single character removed', function() {
|
||||||
|
it('at start of string', function() {
|
||||||
|
const diff = diffDeletion("hello", "ello");
|
||||||
|
expect(diff.at).toBe(0);
|
||||||
|
expect(diff.removed).toBe("h");
|
||||||
|
});
|
||||||
|
it('in middle of string', function() {
|
||||||
|
const diff = diffDeletion("hello", "hllo");
|
||||||
|
expect(diff.at).toBe(1);
|
||||||
|
expect(diff.removed).toBe("e");
|
||||||
|
});
|
||||||
|
it('in middle of string with duplicate character', function() {
|
||||||
|
const diff = diffDeletion("hello", "helo");
|
||||||
|
expect(diff.at).toBe(3);
|
||||||
|
expect(diff.removed).toBe("l");
|
||||||
|
});
|
||||||
|
it('at end of string', function() {
|
||||||
|
const diff = diffDeletion("hello", "hell");
|
||||||
|
expect(diff.at).toBe(4);
|
||||||
|
expect(diff.removed).toBe("o");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('with a multiple removed', function() {
|
||||||
|
it('at start of string', function() {
|
||||||
|
const diff = diffDeletion("hello", "llo");
|
||||||
|
expect(diff.at).toBe(0);
|
||||||
|
expect(diff.removed).toBe("he");
|
||||||
|
});
|
||||||
|
it('removing whole string', function() {
|
||||||
|
const diff = diffDeletion("hello", "");
|
||||||
|
expect(diff.at).toBe(0);
|
||||||
|
expect(diff.removed).toBe("hello");
|
||||||
|
});
|
||||||
|
it('in middle of string', function() {
|
||||||
|
const diff = diffDeletion("hello", "hlo");
|
||||||
|
expect(diff.at).toBe(1);
|
||||||
|
expect(diff.removed).toBe("el");
|
||||||
|
});
|
||||||
|
it('in middle of string with duplicate character', function() {
|
||||||
|
const diff = diffDeletion("hello", "heo");
|
||||||
|
expect(diff.at).toBe(2);
|
||||||
|
expect(diff.removed).toBe("ll");
|
||||||
|
});
|
||||||
|
it('at end of string', function() {
|
||||||
|
const diff = diffDeletion("hello", "hel");
|
||||||
|
expect(diff.at).toBe(3);
|
||||||
|
expect(diff.removed).toBe("lo");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('diffAtCaret', function() {
|
||||||
|
it('insert at start', function() {
|
||||||
|
const diff = diffAtCaret("world", "hello world", 6);
|
||||||
|
expect(diff.at).toBe(0);
|
||||||
|
expect(diff.added).toBe("hello ");
|
||||||
|
expect(diff.removed).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('insert at end', function() {
|
||||||
|
const diff = diffAtCaret("hello", "hello world", 11);
|
||||||
|
expect(diff.at).toBe(5);
|
||||||
|
expect(diff.added).toBe(" world");
|
||||||
|
expect(diff.removed).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('insert in middle', function() {
|
||||||
|
const diff = diffAtCaret("hello world", "hello cruel world", 12);
|
||||||
|
expect(diff.at).toBe(6);
|
||||||
|
expect(diff.added).toBe("cruel ");
|
||||||
|
expect(diff.removed).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('replace at start', function() {
|
||||||
|
const diff = diffAtCaret("morning, world!", "afternoon, world!", 9);
|
||||||
|
expect(diff.at).toBe(0);
|
||||||
|
expect(diff.removed).toBe("morning");
|
||||||
|
expect(diff.added).toBe("afternoon");
|
||||||
|
});
|
||||||
|
it('replace at end', function() {
|
||||||
|
const diff = diffAtCaret("morning, world!", "morning, mars?", 14);
|
||||||
|
expect(diff.at).toBe(9);
|
||||||
|
expect(diff.removed).toBe("world!");
|
||||||
|
expect(diff.added).toBe("mars?");
|
||||||
|
});
|
||||||
|
it('replace in middle', function() {
|
||||||
|
const diff = diffAtCaret("morning, blue planet", "morning, red planet", 12);
|
||||||
|
expect(diff.at).toBe(9);
|
||||||
|
expect(diff.removed).toBe("blue");
|
||||||
|
expect(diff.added).toBe("red");
|
||||||
|
});
|
||||||
|
it('remove at start of string', function() {
|
||||||
|
const diff = diffAtCaret("hello", "ello", 0);
|
||||||
|
expect(diff.at).toBe(0);
|
||||||
|
expect(diff.removed).toBe("h");
|
||||||
|
expect(diff.added).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('removing whole string', function() {
|
||||||
|
const diff = diffAtCaret("hello", "", 0);
|
||||||
|
expect(diff.at).toBe(0);
|
||||||
|
expect(diff.removed).toBe("hello");
|
||||||
|
expect(diff.added).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('remove in middle of string', function() {
|
||||||
|
const diff = diffAtCaret("hello", "hllo", 1);
|
||||||
|
expect(diff.at).toBe(1);
|
||||||
|
expect(diff.removed).toBe("e");
|
||||||
|
expect(diff.added).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('forwards remove in middle of string', function() {
|
||||||
|
const diff = diffAtCaret("hello", "hell", 4);
|
||||||
|
expect(diff.at).toBe(4);
|
||||||
|
expect(diff.removed).toBe("o");
|
||||||
|
expect(diff.added).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('forwards remove in middle of string with duplicate character', function() {
|
||||||
|
const diff = diffAtCaret("hello", "helo", 3);
|
||||||
|
expect(diff.at).toBe(3);
|
||||||
|
expect(diff.removed).toBe("l");
|
||||||
|
expect(diff.added).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('remove at end of string', function() {
|
||||||
|
const diff = diffAtCaret("hello", "hell", 4);
|
||||||
|
expect(diff.at).toBe(4);
|
||||||
|
expect(diff.removed).toBe("o");
|
||||||
|
expect(diff.added).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
69
test/editor/mock.js
Normal file
69
test/editor/mock.js
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {PartCreator} from "../../src/editor/parts";
|
||||||
|
|
||||||
|
class MockAutoComplete {
|
||||||
|
constructor(updateCallback, partCreator, completions) {
|
||||||
|
this._updateCallback = updateCallback;
|
||||||
|
this._partCreator = partCreator;
|
||||||
|
this._completions = completions;
|
||||||
|
this._part = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this._updateCallback({close: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
tryComplete(close = true) {
|
||||||
|
const matches = this._completions.filter(o => {
|
||||||
|
return o.resourceId.startsWith(this._part.text);
|
||||||
|
});
|
||||||
|
if (matches.length === 1 && this._part.text.length > 1) {
|
||||||
|
const match = matches[0];
|
||||||
|
let pill;
|
||||||
|
if (match.resourceId[0] === "@") {
|
||||||
|
pill = this._partCreator.userPill(match.label, match.resourceId);
|
||||||
|
} else {
|
||||||
|
pill = this._partCreator.roomPill(match.resourceId);
|
||||||
|
}
|
||||||
|
this._updateCallback({replacePart: pill, close});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// called by EditorModel when typing into pill-candidate part
|
||||||
|
onPartUpdate(part, offset) {
|
||||||
|
this._part = part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockClient & MockRoom are only used for avatars in room and user pills,
|
||||||
|
// which is not tested
|
||||||
|
class MockClient {
|
||||||
|
getRooms() { return []; }
|
||||||
|
getRoom() { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockRoom {
|
||||||
|
getMember() { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPartCreator(completions = []) {
|
||||||
|
const autoCompleteCreator = (partCreator) => {
|
||||||
|
return (updateCallback) => new MockAutoComplete(updateCallback, partCreator, completions);
|
||||||
|
};
|
||||||
|
return new PartCreator(autoCompleteCreator, new MockRoom(), new MockClient());
|
||||||
|
}
|
296
test/editor/model-test.js
Normal file
296
test/editor/model-test.js
Normal file
|
@ -0,0 +1,296 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import expect from 'expect';
|
||||||
|
import EditorModel from "../../src/editor/model";
|
||||||
|
import {createPartCreator} from "./mock";
|
||||||
|
|
||||||
|
function createRenderer() {
|
||||||
|
const render = (c) => {
|
||||||
|
render.caret = c;
|
||||||
|
render.count += 1;
|
||||||
|
};
|
||||||
|
render.count = 0;
|
||||||
|
render.caret = null;
|
||||||
|
return render;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('editor/model', function() {
|
||||||
|
describe('plain text manipulation', function() {
|
||||||
|
it('insert text into empty document', function() {
|
||||||
|
const renderer = createRenderer();
|
||||||
|
const model = new EditorModel([], createPartCreator(), renderer);
|
||||||
|
model.update("hello", "insertText", {offset: 5, atNodeEnd: true});
|
||||||
|
expect(renderer.count).toBe(1);
|
||||||
|
expect(renderer.caret.index).toBe(0);
|
||||||
|
expect(renderer.caret.offset).toBe(5);
|
||||||
|
expect(model.parts.length).toBe(1);
|
||||||
|
expect(model.parts[0].type).toBe("plain");
|
||||||
|
expect(model.parts[0].text).toBe("hello");
|
||||||
|
});
|
||||||
|
it('append text to existing document', function() {
|
||||||
|
const renderer = createRenderer();
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([pc.plain("hello")], pc, renderer);
|
||||||
|
model.update("hello world", "insertText", {offset: 11, atNodeEnd: true});
|
||||||
|
expect(renderer.count).toBe(1);
|
||||||
|
expect(renderer.caret.index).toBe(0);
|
||||||
|
expect(renderer.caret.offset).toBe(11);
|
||||||
|
expect(model.parts.length).toBe(1);
|
||||||
|
expect(model.parts[0].type).toBe("plain");
|
||||||
|
expect(model.parts[0].text).toBe("hello world");
|
||||||
|
});
|
||||||
|
it('prepend text to existing document', function() {
|
||||||
|
const renderer = createRenderer();
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([pc.plain("world")], pc, renderer);
|
||||||
|
model.update("hello world", "insertText", {offset: 6, atNodeEnd: false});
|
||||||
|
expect(renderer.count).toBe(1);
|
||||||
|
expect(renderer.caret.index).toBe(0);
|
||||||
|
expect(renderer.caret.offset).toBe(6);
|
||||||
|
expect(model.parts.length).toBe(1);
|
||||||
|
expect(model.parts[0].type).toBe("plain");
|
||||||
|
expect(model.parts[0].text).toBe("hello world");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('handling line breaks', function() {
|
||||||
|
it('insert new line into existing document', function() {
|
||||||
|
const renderer = createRenderer();
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([pc.plain("hello")], pc, renderer);
|
||||||
|
model.update("hello\n", "insertText", {offset: 6, atNodeEnd: true});
|
||||||
|
expect(renderer.count).toBe(1);
|
||||||
|
expect(renderer.caret.index).toBe(1);
|
||||||
|
expect(renderer.caret.offset).toBe(1);
|
||||||
|
expect(model.parts.length).toBe(2);
|
||||||
|
expect(model.parts[0].type).toBe("plain");
|
||||||
|
expect(model.parts[0].text).toBe("hello");
|
||||||
|
expect(model.parts[1].type).toBe("newline");
|
||||||
|
expect(model.parts[1].text).toBe("\n");
|
||||||
|
});
|
||||||
|
it('insert multiple new lines into existing document', function() {
|
||||||
|
const renderer = createRenderer();
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([pc.plain("hello")], pc, renderer);
|
||||||
|
model.update("hello\n\n\nworld!", "insertText", {offset: 14, atNodeEnd: true});
|
||||||
|
expect(renderer.count).toBe(1);
|
||||||
|
expect(renderer.caret.index).toBe(4);
|
||||||
|
expect(renderer.caret.offset).toBe(6);
|
||||||
|
expect(model.parts.length).toBe(5);
|
||||||
|
expect(model.parts[0].type).toBe("plain");
|
||||||
|
expect(model.parts[0].text).toBe("hello");
|
||||||
|
expect(model.parts[1].type).toBe("newline");
|
||||||
|
expect(model.parts[1].text).toBe("\n");
|
||||||
|
expect(model.parts[2].type).toBe("newline");
|
||||||
|
expect(model.parts[2].text).toBe("\n");
|
||||||
|
expect(model.parts[3].type).toBe("newline");
|
||||||
|
expect(model.parts[3].text).toBe("\n");
|
||||||
|
expect(model.parts[4].type).toBe("plain");
|
||||||
|
expect(model.parts[4].text).toBe("world!");
|
||||||
|
});
|
||||||
|
it('type in empty line', function() {
|
||||||
|
const renderer = createRenderer();
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.plain("hello"),
|
||||||
|
pc.newline(),
|
||||||
|
pc.newline(),
|
||||||
|
pc.plain("world"),
|
||||||
|
], pc, renderer);
|
||||||
|
model.update("hello\nwarm\nworld", "insertText", {offset: 10, atNodeEnd: true});
|
||||||
|
console.log(model.serializeParts());
|
||||||
|
expect(renderer.count).toBe(1);
|
||||||
|
expect(renderer.caret.index).toBe(2);
|
||||||
|
expect(renderer.caret.offset).toBe(4);
|
||||||
|
expect(model.parts.length).toBe(5);
|
||||||
|
expect(model.parts[0].type).toBe("plain");
|
||||||
|
expect(model.parts[0].text).toBe("hello");
|
||||||
|
expect(model.parts[1].type).toBe("newline");
|
||||||
|
expect(model.parts[1].text).toBe("\n");
|
||||||
|
expect(model.parts[2].type).toBe("plain");
|
||||||
|
expect(model.parts[2].text).toBe("warm");
|
||||||
|
expect(model.parts[3].type).toBe("newline");
|
||||||
|
expect(model.parts[3].text).toBe("\n");
|
||||||
|
expect(model.parts[4].type).toBe("plain");
|
||||||
|
expect(model.parts[4].text).toBe("world");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('non-editable part manipulation', function() {
|
||||||
|
it('typing at start of non-editable part prepends', function() {
|
||||||
|
const renderer = createRenderer();
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.plain("try "),
|
||||||
|
pc.roomPill("#someroom"),
|
||||||
|
], pc, renderer);
|
||||||
|
model.update("try foo#someroom", "insertText", {offset: 7, atNodeEnd: false});
|
||||||
|
expect(renderer.caret.index).toBe(0);
|
||||||
|
expect(renderer.caret.offset).toBe(7);
|
||||||
|
expect(model.parts.length).toBe(2);
|
||||||
|
expect(model.parts[0].type).toBe("plain");
|
||||||
|
expect(model.parts[0].text).toBe("try foo");
|
||||||
|
expect(model.parts[1].type).toBe("room-pill");
|
||||||
|
expect(model.parts[1].text).toBe("#someroom");
|
||||||
|
});
|
||||||
|
it('typing in middle of non-editable part appends', function() {
|
||||||
|
const renderer = createRenderer();
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.plain("try "),
|
||||||
|
pc.roomPill("#someroom"),
|
||||||
|
pc.plain("?"),
|
||||||
|
], pc, renderer);
|
||||||
|
model.update("try #some perhapsroom?", "insertText", {offset: 17, atNodeEnd: false});
|
||||||
|
expect(renderer.caret.index).toBe(2);
|
||||||
|
expect(renderer.caret.offset).toBe(8);
|
||||||
|
expect(model.parts.length).toBe(3);
|
||||||
|
expect(model.parts[0].type).toBe("plain");
|
||||||
|
expect(model.parts[0].text).toBe("try ");
|
||||||
|
expect(model.parts[1].type).toBe("room-pill");
|
||||||
|
expect(model.parts[1].text).toBe("#someroom");
|
||||||
|
expect(model.parts[2].type).toBe("plain");
|
||||||
|
expect(model.parts[2].text).toBe(" perhaps?");
|
||||||
|
});
|
||||||
|
it('remove non-editable part with backspace', function() {
|
||||||
|
const renderer = createRenderer();
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([pc.roomPill("#someroom")], pc, renderer);
|
||||||
|
model.update("#someroo", "deleteContentBackward", {offset: 8, atNodeEnd: true});
|
||||||
|
expect(renderer.count).toBe(1);
|
||||||
|
expect(renderer.caret.index).toBe(-1);
|
||||||
|
expect(renderer.caret.offset).toBe(0);
|
||||||
|
expect(model.parts.length).toBe(0);
|
||||||
|
});
|
||||||
|
it('remove non-editable part with delete', function() {
|
||||||
|
const renderer = createRenderer();
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([pc.roomPill("#someroom")], pc, renderer);
|
||||||
|
model.update("someroom", "deleteContentForward", {offset: 0, atNodeEnd: false});
|
||||||
|
expect(renderer.count).toBe(1);
|
||||||
|
expect(renderer.caret.index).toBe(-1);
|
||||||
|
expect(renderer.caret.offset).toBe(0);
|
||||||
|
expect(model.parts.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('auto-complete', function() {
|
||||||
|
it('insert user pill', function() {
|
||||||
|
const renderer = createRenderer();
|
||||||
|
const pc = createPartCreator([{resourceId: "@alice", label: "Alice"}]);
|
||||||
|
const model = new EditorModel([pc.plain("hello ")], pc, renderer);
|
||||||
|
|
||||||
|
model.update("hello @a", "insertText", {offset: 8, atNodeEnd: true});
|
||||||
|
|
||||||
|
expect(renderer.count).toBe(1);
|
||||||
|
expect(renderer.caret.index).toBe(1);
|
||||||
|
expect(renderer.caret.offset).toBe(2);
|
||||||
|
expect(model.parts.length).toBe(2);
|
||||||
|
expect(model.parts[0].type).toBe("plain");
|
||||||
|
expect(model.parts[0].text).toBe("hello ");
|
||||||
|
expect(model.parts[1].type).toBe("pill-candidate");
|
||||||
|
expect(model.parts[1].text).toBe("@a");
|
||||||
|
|
||||||
|
model.autoComplete.tryComplete(); // see MockAutoComplete
|
||||||
|
|
||||||
|
expect(renderer.count).toBe(2);
|
||||||
|
expect(renderer.caret.index).toBe(1);
|
||||||
|
expect(renderer.caret.offset).toBe(5);
|
||||||
|
expect(model.parts.length).toBe(2);
|
||||||
|
expect(model.parts[0].type).toBe("plain");
|
||||||
|
expect(model.parts[0].text).toBe("hello ");
|
||||||
|
expect(model.parts[1].type).toBe("user-pill");
|
||||||
|
expect(model.parts[1].text).toBe("Alice");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('insert room pill', function() {
|
||||||
|
const renderer = createRenderer();
|
||||||
|
const pc = createPartCreator([{resourceId: "#riot-dev"}]);
|
||||||
|
const model = new EditorModel([pc.plain("hello ")], pc, renderer);
|
||||||
|
|
||||||
|
model.update("hello #r", "insertText", {offset: 8, atNodeEnd: true});
|
||||||
|
|
||||||
|
expect(renderer.count).toBe(1);
|
||||||
|
expect(renderer.caret.index).toBe(1);
|
||||||
|
expect(renderer.caret.offset).toBe(2);
|
||||||
|
expect(model.parts.length).toBe(2);
|
||||||
|
expect(model.parts[0].type).toBe("plain");
|
||||||
|
expect(model.parts[0].text).toBe("hello ");
|
||||||
|
expect(model.parts[1].type).toBe("pill-candidate");
|
||||||
|
expect(model.parts[1].text).toBe("#r");
|
||||||
|
|
||||||
|
model.autoComplete.tryComplete(); // see MockAutoComplete
|
||||||
|
|
||||||
|
expect(renderer.count).toBe(2);
|
||||||
|
expect(renderer.caret.index).toBe(1);
|
||||||
|
expect(renderer.caret.offset).toBe(9);
|
||||||
|
expect(model.parts.length).toBe(2);
|
||||||
|
expect(model.parts[0].type).toBe("plain");
|
||||||
|
expect(model.parts[0].text).toBe("hello ");
|
||||||
|
expect(model.parts[1].type).toBe("room-pill");
|
||||||
|
expect(model.parts[1].text).toBe("#riot-dev");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('type after inserting pill', function() {
|
||||||
|
const renderer = createRenderer();
|
||||||
|
const pc = createPartCreator([{resourceId: "#riot-dev"}]);
|
||||||
|
const model = new EditorModel([pc.plain("hello ")], pc, renderer);
|
||||||
|
|
||||||
|
model.update("hello #r", "insertText", {offset: 8, atNodeEnd: true});
|
||||||
|
model.autoComplete.tryComplete(); // see MockAutoComplete
|
||||||
|
model.update("hello #riot-dev!!", "insertText", {offset: 17, atNodeEnd: true});
|
||||||
|
|
||||||
|
expect(renderer.count).toBe(3);
|
||||||
|
expect(renderer.caret.index).toBe(2);
|
||||||
|
expect(renderer.caret.offset).toBe(2);
|
||||||
|
expect(model.parts.length).toBe(3);
|
||||||
|
expect(model.parts[0].type).toBe("plain");
|
||||||
|
expect(model.parts[0].text).toBe("hello ");
|
||||||
|
expect(model.parts[1].type).toBe("room-pill");
|
||||||
|
expect(model.parts[1].text).toBe("#riot-dev");
|
||||||
|
expect(model.parts[2].type).toBe("plain");
|
||||||
|
expect(model.parts[2].text).toBe("!!");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pasting text does not trigger auto-complete', function() {
|
||||||
|
const renderer = createRenderer();
|
||||||
|
const pc = createPartCreator([{resourceId: "#define-room"}]);
|
||||||
|
const model = new EditorModel([pc.plain("try ")], pc, renderer);
|
||||||
|
|
||||||
|
model.update("try #define", "insertFromPaste", {offset: 11, atNodeEnd: true});
|
||||||
|
|
||||||
|
expect(model.autoComplete).toBeFalsy();
|
||||||
|
expect(renderer.caret.index).toBe(0);
|
||||||
|
expect(renderer.caret.offset).toBe(11);
|
||||||
|
expect(model.parts.length).toBe(1);
|
||||||
|
expect(model.parts[0].type).toBe("plain");
|
||||||
|
expect(model.parts[0].text).toBe("try #define");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dropping text does not trigger auto-complete', function() {
|
||||||
|
const renderer = createRenderer();
|
||||||
|
const pc = createPartCreator([{resourceId: "#define-room"}]);
|
||||||
|
const model = new EditorModel([pc.plain("try ")], pc, renderer);
|
||||||
|
|
||||||
|
model.update("try #define", "insertFromDrop", {offset: 11, atNodeEnd: true});
|
||||||
|
|
||||||
|
expect(model.autoComplete).toBeFalsy();
|
||||||
|
expect(renderer.caret.index).toBe(0);
|
||||||
|
expect(renderer.caret.offset).toBe(11);
|
||||||
|
expect(model.parts.length).toBe(1);
|
||||||
|
expect(model.parts[0].type).toBe("plain");
|
||||||
|
expect(model.parts[0].text).toBe("try #define");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
47
test/editor/serialize-test.js
Normal file
47
test/editor/serialize-test.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import expect from 'expect';
|
||||||
|
import EditorModel from "../../src/editor/model";
|
||||||
|
import {htmlSerializeIfNeeded} from "../../src/editor/serialize";
|
||||||
|
import {createPartCreator} from "./mock";
|
||||||
|
|
||||||
|
describe('editor/serialize', function() {
|
||||||
|
it('user pill turns message into html', function() {
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([pc.userPill("Alice", "@alice:hs.tld")]);
|
||||||
|
const html = htmlSerializeIfNeeded(model, {});
|
||||||
|
expect(html).toBe("<a href=\"https://matrix.to/#/@alice:hs.tld\">Alice</a>");
|
||||||
|
});
|
||||||
|
it('room pill turns message into html', function() {
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([pc.roomPill("#room:hs.tld")]);
|
||||||
|
const html = htmlSerializeIfNeeded(model, {});
|
||||||
|
expect(html).toBe("<a href=\"https://matrix.to/#/#room:hs.tld\">#room:hs.tld</a>");
|
||||||
|
});
|
||||||
|
it('@room pill turns message into html', function() {
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([pc.atRoomPill("@room")]);
|
||||||
|
const html = htmlSerializeIfNeeded(model, {});
|
||||||
|
expect(html).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('any markdown turns message into html', function() {
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([pc.plain("*hello* world")]);
|
||||||
|
const html = htmlSerializeIfNeeded(model, {});
|
||||||
|
expect(html).toBe("<em>hello</em> world");
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue