Merge pull request #3247 from matrix-org/bwindels/editortests

Unit tests for new editor
This commit is contained in:
Bruno Windels
2019-07-30 14:46:33 +00:00
committed by GitHub
11 changed files with 1018 additions and 44 deletions

205
test/editor/caret-test.js Normal file
View 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);
});
});
});

View 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
View 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
View 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
View 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");
});
});
});

View 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");
});
});