Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Merge pull request #3247 from matrix-org/bwindels/editortests
Browse files Browse the repository at this point in the history
Unit tests for new editor
  • Loading branch information
bwindels authored Jul 30, 2019
2 parents 106af39 + 44e4661 commit e855a05
Show file tree
Hide file tree
Showing 11 changed files with 1,018 additions and 44 deletions.
5 changes: 2 additions & 3 deletions src/components/views/elements/MessageEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {htmlSerializeIfNeeded, textSerialize} from '../../../editor/serialize';
import {findEditableEvent} from '../../../utils/EventUtils';
import {parseEvent} from '../../../editor/deserialize';
import Autocomplete from '../rooms/Autocomplete';
import {PartCreator} from '../../../editor/parts';
import {PartCreator, autoCompleteCreator} from '../../../editor/parts';
import {renderModel} from '../../../editor/render';
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
import {MatrixClient} from 'matrix-js-sdk';
Expand Down Expand Up @@ -303,8 +303,7 @@ export default class MessageEditor extends React.Component {
const {editState} = this.props;
const room = this._getRoom();
const partCreator = new PartCreator(
() => this._autocompleteRef,
query => this.setState({query}),
autoCompleteCreator(() => this._autocompleteRef, query => this.setState({query})),
room,
this.context.matrixClient,
);
Expand Down
2 changes: 1 addition & 1 deletion src/editor/caret.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function setCaretPosition(editor, model, caretPosition) {
sel.addRange(range);
}

function getLineAndNodePosition(model, caretPosition) {
export function getLineAndNodePosition(model, caretPosition) {
const {parts} = model;
const partIndex = caretPosition.index;
const lineResult = findNodeInLineForPart(parts, partIndex);
Expand Down
33 changes: 12 additions & 21 deletions src/editor/diff.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,6 @@ function firstDiff(a, b) {
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) {
const len = Math.min(oldStr.length, newStr.length);
const startInCommon = oldStr.substr(0, len) === newStr.substr(0, len);
Expand All @@ -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) {
if (oldStr === newStr) {
return {};
}
const firstDiffIdx = firstDiff(oldStr, newStr);
const lastDiffIdx = oldStr.length - lastDiff(oldStr, newStr) + 1;
return {at: firstDiffIdx, removed: oldStr.substring(firstDiffIdx, lastDiffIdx)};
}

export function diffInsertion(oldStr, newStr) {
const diff = diffDeletion(newStr, oldStr);
if (diff.removed) {
return {at: diff.at, added: diff.removed};
} else {
return diff;
}
const amount = oldStr.length - newStr.length;
return {at: firstDiffIdx, removed: oldStr.substr(firstDiffIdx, amount)};
}

/**
* 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) {
const diffLen = newValue.length - oldValue.length;
const caretPositionBeforeInput = caretPosition - diffLen;
Expand Down
21 changes: 14 additions & 7 deletions src/editor/parts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
return chr !== "@" && chr !== "#" && chr !== ":" && chr !== "\n";
}
Expand Down Expand Up @@ -348,18 +349,24 @@ class PillCandidatePart extends PlainPart {
}
}

export class PartCreator {
constructor(getAutocompleterComponent, updateQuery, room, client) {
this._room = room;
this._client = client;
this._autoCompleteCreator = (updateCallback) => {
export function autoCompleteCreator(updateQuery, getAutocompleterComponent) {
return (partCreator) => {
return (updateCallback) => {
return new AutocompleteWrapperModel(
updateCallback,
getAutocompleterComponent,
updateQuery,
this,
partCreator,
);
};
};
}

export class PartCreator {
constructor(autoCompleteCreator, room, client) {
this._room = room;
this._client = client;
this._autoCompleteCreator = autoCompleteCreator(this);
}

createPartForInput(input) {
Expand Down
12 changes: 0 additions & 12 deletions src/editor/serialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 205 additions & 0 deletions test/editor/caret-test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Loading

0 comments on commit e855a05

Please sign in to comment.