wip; tests passing
This commit is contained in:
@@ -1,491 +0,0 @@
|
||||
import { ribbit, resetDOM } from './setup';
|
||||
|
||||
const lib = ribbit();
|
||||
|
||||
function mockTransport() {
|
||||
const receiveListeners: Array<(update: Uint8Array) => void> = [];
|
||||
const lockListeners: Array<(holder: any) => void> = [];
|
||||
return {
|
||||
connected: false,
|
||||
sent: [] as Uint8Array[],
|
||||
locked: false,
|
||||
connect() {
|
||||
this.connected = true;
|
||||
},
|
||||
disconnect() {
|
||||
this.connected = false;
|
||||
},
|
||||
send(update: Uint8Array) {
|
||||
this.sent.push(update);
|
||||
},
|
||||
onReceive(cb: (update: Uint8Array) => void) {
|
||||
receiveListeners.push(cb);
|
||||
},
|
||||
simulateRemote(content: string) {
|
||||
const encoded = new TextEncoder().encode(content);
|
||||
receiveListeners.forEach(cb => cb(encoded));
|
||||
},
|
||||
lock: async function() {
|
||||
this.locked = true;
|
||||
return true;
|
||||
},
|
||||
unlock() {
|
||||
this.locked = false;
|
||||
},
|
||||
forceLock: async function() {
|
||||
this.locked = true;
|
||||
return true;
|
||||
},
|
||||
onLockChange(cb: (holder: any) => void) {
|
||||
lockListeners.push(cb);
|
||||
},
|
||||
simulateLock(holder: any) {
|
||||
lockListeners.forEach(cb => cb(holder));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function mockPresence() {
|
||||
const listeners: Array<(peers: any[]) => void> = [];
|
||||
return {
|
||||
lastSent: null as any,
|
||||
send(info: any) {
|
||||
this.lastSent = info;
|
||||
},
|
||||
onUpdate(cb: (peers: any[]) => void) {
|
||||
listeners.push(cb);
|
||||
},
|
||||
simulatePeers(peers: any[]) {
|
||||
listeners.forEach(cb => cb(peers));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function mockRevisions() {
|
||||
const store: any[] = [];
|
||||
return {
|
||||
store,
|
||||
list: async () => store,
|
||||
get: async (id: string) => store.find((rev: any) => rev.id === id),
|
||||
create: async (content: string, meta?: any) => {
|
||||
const rev = {
|
||||
id: String(store.length + 1),
|
||||
timestamp: new Date().toISOString(),
|
||||
content,
|
||||
...meta,
|
||||
};
|
||||
store.push(rev);
|
||||
return rev;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('CollaborationManager', () => {
|
||||
beforeEach(() => resetDOM('initial'));
|
||||
|
||||
it('does not create manager without settings', () => {
|
||||
const editor = new lib.Editor({});
|
||||
editor.run();
|
||||
expect(editor.collaboration).toBeUndefined();
|
||||
});
|
||||
|
||||
it('creates manager with settings', () => {
|
||||
const transport = mockTransport();
|
||||
const editor = new lib.Editor({
|
||||
collaboration: {
|
||||
transport,
|
||||
user: {
|
||||
userId: 'test',
|
||||
displayName: 'Test',
|
||||
status: 'active',
|
||||
lastActive: Date.now(),
|
||||
},
|
||||
},
|
||||
});
|
||||
editor.run();
|
||||
expect(editor.collaboration).toBeDefined();
|
||||
});
|
||||
|
||||
describe('connection lifecycle', () => {
|
||||
it('connects on wysiwyg', () => {
|
||||
const transport = mockTransport();
|
||||
const editor = new lib.Editor({
|
||||
collaboration: {
|
||||
transport,
|
||||
user: {
|
||||
userId: 'test',
|
||||
displayName: 'Test',
|
||||
status: 'active',
|
||||
lastActive: Date.now(),
|
||||
},
|
||||
},
|
||||
});
|
||||
editor.run();
|
||||
editor.wysiwyg();
|
||||
expect(transport.connected).toBe(true);
|
||||
});
|
||||
|
||||
it('connects on edit', () => {
|
||||
const transport = mockTransport();
|
||||
const editor = new lib.Editor({
|
||||
collaboration: {
|
||||
transport,
|
||||
user: {
|
||||
userId: 'test',
|
||||
displayName: 'Test',
|
||||
status: 'active',
|
||||
lastActive: Date.now(),
|
||||
},
|
||||
},
|
||||
});
|
||||
editor.run();
|
||||
editor.edit();
|
||||
expect(transport.connected).toBe(true);
|
||||
});
|
||||
|
||||
it('disconnects on view', () => {
|
||||
const transport = mockTransport();
|
||||
const editor = new lib.Editor({
|
||||
collaboration: {
|
||||
transport,
|
||||
user: {
|
||||
userId: 'test',
|
||||
displayName: 'Test',
|
||||
status: 'active',
|
||||
lastActive: Date.now(),
|
||||
},
|
||||
},
|
||||
});
|
||||
editor.run();
|
||||
editor.wysiwyg();
|
||||
editor.view();
|
||||
expect(transport.connected).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('source mode pausing', () => {
|
||||
it('pauses on entering source mode', () => {
|
||||
const transport = mockTransport();
|
||||
const editor = new lib.Editor({
|
||||
collaboration: {
|
||||
transport,
|
||||
user: {
|
||||
userId: 'test',
|
||||
displayName: 'Test',
|
||||
status: 'active',
|
||||
lastActive: Date.now(),
|
||||
},
|
||||
},
|
||||
});
|
||||
editor.run();
|
||||
editor.edit();
|
||||
expect(editor.collaboration!.isPaused()).toBe(true);
|
||||
});
|
||||
|
||||
it('counts remote changes while paused', () => {
|
||||
const transport = mockTransport();
|
||||
const editor = new lib.Editor({
|
||||
collaboration: {
|
||||
transport,
|
||||
user: {
|
||||
userId: 'test',
|
||||
displayName: 'Test',
|
||||
status: 'active',
|
||||
lastActive: Date.now(),
|
||||
},
|
||||
},
|
||||
});
|
||||
editor.run();
|
||||
editor.edit();
|
||||
transport.simulateRemote('change 1');
|
||||
transport.simulateRemote('change 2');
|
||||
expect(editor.collaboration!.getRemoteChangeCount()).toBe(2);
|
||||
});
|
||||
|
||||
it('fires remoteActivity event while paused', (done) => {
|
||||
const transport = mockTransport();
|
||||
const editor = new lib.Editor({
|
||||
collaboration: {
|
||||
transport,
|
||||
user: {
|
||||
userId: 'test',
|
||||
displayName: 'Test',
|
||||
status: 'active',
|
||||
lastActive: Date.now(),
|
||||
},
|
||||
},
|
||||
on: {
|
||||
remoteActivity: ({ count }: any) => {
|
||||
if (count === 1) {
|
||||
done();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
editor.run();
|
||||
editor.edit();
|
||||
transport.simulateRemote('change');
|
||||
});
|
||||
|
||||
it('resumes on switching to wysiwyg', () => {
|
||||
const transport = mockTransport();
|
||||
const editor = new lib.Editor({
|
||||
collaboration: {
|
||||
transport,
|
||||
user: {
|
||||
userId: 'test',
|
||||
displayName: 'Test',
|
||||
status: 'active',
|
||||
lastActive: Date.now(),
|
||||
},
|
||||
},
|
||||
});
|
||||
editor.run();
|
||||
editor.edit();
|
||||
editor.wysiwyg();
|
||||
expect(editor.collaboration!.isPaused()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('locking', () => {
|
||||
it('lock returns true', async () => {
|
||||
const transport = mockTransport();
|
||||
const editor = new lib.Editor({
|
||||
collaboration: {
|
||||
transport,
|
||||
user: {
|
||||
userId: 'test',
|
||||
displayName: 'Test',
|
||||
status: 'active',
|
||||
lastActive: Date.now(),
|
||||
},
|
||||
},
|
||||
});
|
||||
editor.run();
|
||||
expect(await editor.lockForEditing()).toBe(true);
|
||||
});
|
||||
|
||||
it('forceLock returns true', async () => {
|
||||
const transport = mockTransport();
|
||||
const editor = new lib.Editor({
|
||||
collaboration: {
|
||||
transport,
|
||||
user: {
|
||||
userId: 'test',
|
||||
displayName: 'Test',
|
||||
status: 'active',
|
||||
lastActive: Date.now(),
|
||||
},
|
||||
},
|
||||
});
|
||||
editor.run();
|
||||
expect(await editor.forceLockEditing()).toBe(true);
|
||||
});
|
||||
|
||||
it('fires lockChange event', (done) => {
|
||||
const transport = mockTransport();
|
||||
const editor = new lib.Editor({
|
||||
collaboration: {
|
||||
transport,
|
||||
user: {
|
||||
userId: 'test',
|
||||
displayName: 'Test',
|
||||
status: 'active',
|
||||
lastActive: Date.now(),
|
||||
},
|
||||
},
|
||||
on: {
|
||||
lockChange: ({ holder }: any) => {
|
||||
if (holder?.userId === 'alice') {
|
||||
done();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
editor.run();
|
||||
transport.simulateLock({
|
||||
userId: 'alice',
|
||||
displayName: 'Alice',
|
||||
status: 'active',
|
||||
lastActive: Date.now(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('presence', () => {
|
||||
it('sends cursor with status', () => {
|
||||
const transport = mockTransport();
|
||||
const presence = mockPresence();
|
||||
const editor = new lib.Editor({
|
||||
collaboration: {
|
||||
transport,
|
||||
presence,
|
||||
user: {
|
||||
userId: 'test',
|
||||
displayName: 'Test',
|
||||
status: 'active',
|
||||
lastActive: Date.now(),
|
||||
color: '#f00',
|
||||
},
|
||||
},
|
||||
});
|
||||
editor.run();
|
||||
editor.wysiwyg();
|
||||
editor.collaboration!.sendCursor(42);
|
||||
expect(presence.lastSent.status).toBe('active');
|
||||
expect(presence.lastSent.cursor).toBe(42);
|
||||
});
|
||||
|
||||
it('sends editing status when paused', () => {
|
||||
const transport = mockTransport();
|
||||
const presence = mockPresence();
|
||||
const editor = new lib.Editor({
|
||||
collaboration: {
|
||||
transport,
|
||||
presence,
|
||||
user: {
|
||||
userId: 'test',
|
||||
displayName: 'Test',
|
||||
status: 'active',
|
||||
lastActive: Date.now(),
|
||||
},
|
||||
},
|
||||
});
|
||||
editor.run();
|
||||
editor.edit();
|
||||
editor.collaboration!.sendCursor(10);
|
||||
expect(presence.lastSent.status).toBe('editing');
|
||||
});
|
||||
|
||||
it('applies idle status to peers', () => {
|
||||
const transport = mockTransport();
|
||||
const presence = mockPresence();
|
||||
const editor = new lib.Editor({
|
||||
collaboration: {
|
||||
transport,
|
||||
presence,
|
||||
idleTimeout: 100,
|
||||
user: {
|
||||
userId: 'test',
|
||||
displayName: 'Test',
|
||||
status: 'active',
|
||||
lastActive: Date.now(),
|
||||
},
|
||||
},
|
||||
});
|
||||
editor.run();
|
||||
presence.simulatePeers([
|
||||
{
|
||||
userId: 'a',
|
||||
displayName: 'A',
|
||||
status: 'active',
|
||||
lastActive: Date.now() - 200,
|
||||
},
|
||||
{
|
||||
userId: 'b',
|
||||
displayName: 'B',
|
||||
status: 'active',
|
||||
lastActive: Date.now(),
|
||||
},
|
||||
]);
|
||||
const peers = editor.collaboration!.getPeers();
|
||||
expect(peers[0].status).toBe('idle');
|
||||
expect(peers[1].status).toBe('active');
|
||||
});
|
||||
});
|
||||
|
||||
describe('revisions', () => {
|
||||
it('lists revisions', async () => {
|
||||
const transport = mockTransport();
|
||||
const revisions = mockRevisions();
|
||||
await revisions.create('v1', { author: 'test' });
|
||||
const editor = new lib.Editor({
|
||||
collaboration: {
|
||||
transport,
|
||||
revisions,
|
||||
user: {
|
||||
userId: 'test',
|
||||
displayName: 'Test',
|
||||
status: 'active',
|
||||
lastActive: Date.now(),
|
||||
},
|
||||
},
|
||||
});
|
||||
editor.run();
|
||||
const list = await editor.listRevisions();
|
||||
expect(list).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('creates revision', async () => {
|
||||
const transport = mockTransport();
|
||||
const revisions = mockRevisions();
|
||||
const editor = new lib.Editor({
|
||||
collaboration: {
|
||||
transport,
|
||||
revisions,
|
||||
user: {
|
||||
userId: 'test',
|
||||
displayName: 'Test',
|
||||
status: 'active',
|
||||
lastActive: Date.now(),
|
||||
},
|
||||
},
|
||||
});
|
||||
editor.run();
|
||||
const rev = await editor.createRevision({
|
||||
author: 'test',
|
||||
summary: 'test rev',
|
||||
});
|
||||
expect(rev).toBeDefined();
|
||||
expect(revisions.store).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('restores revision', async () => {
|
||||
const transport = mockTransport();
|
||||
const revisions = mockRevisions();
|
||||
await revisions.create('old content', { author: 'test' });
|
||||
const editor = new lib.Editor({
|
||||
collaboration: {
|
||||
transport,
|
||||
revisions,
|
||||
user: {
|
||||
userId: 'test',
|
||||
displayName: 'Test',
|
||||
status: 'active',
|
||||
lastActive: Date.now(),
|
||||
},
|
||||
},
|
||||
});
|
||||
editor.run();
|
||||
editor.wysiwyg();
|
||||
await editor.restoreRevision('1');
|
||||
expect(editor.getMarkdown()).toBe('old content');
|
||||
});
|
||||
|
||||
it('fires revisionCreated event', async () => {
|
||||
const transport = mockTransport();
|
||||
const revisions = mockRevisions();
|
||||
let fired = false;
|
||||
const editor = new lib.Editor({
|
||||
collaboration: {
|
||||
transport,
|
||||
revisions,
|
||||
user: {
|
||||
userId: 'test',
|
||||
displayName: 'Test',
|
||||
status: 'active',
|
||||
lastActive: Date.now(),
|
||||
},
|
||||
},
|
||||
on: {
|
||||
revisionCreated: () => {
|
||||
fired = true;
|
||||
},
|
||||
},
|
||||
});
|
||||
editor.run();
|
||||
await editor.createRevision({ author: 'test' });
|
||||
expect(fired).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ribbit, resetDOM } from './setup';
|
||||
import { HopDown } from '../src';
|
||||
|
||||
const lib = ribbit();
|
||||
|
||||
@@ -25,7 +26,7 @@ describe('Custom block tags', () => {
|
||||
selector: 'DETAILS',
|
||||
toMarkdown: (element: any, convert: any) => '\n\n|||\n' + convert.children(element).trim() + '\n|||\n\n',
|
||||
};
|
||||
const converter = new lib.HopDown({
|
||||
const converter = new HopDown({
|
||||
tags: {
|
||||
'DETAILS': spoiler,
|
||||
...lib.defaultTags,
|
||||
@@ -38,15 +39,15 @@ describe('Custom block tags', () => {
|
||||
|
||||
describe('HopDown({ exclude })', () => {
|
||||
it('excludes table', () => {
|
||||
const converter = new lib.HopDown({ exclude: ['table'] });
|
||||
const converter = new HopDown({ exclude: ['table'] });
|
||||
expect(converter.toHTML('| a |\n|---|\n| 1 |')).not.toContain('<table>');
|
||||
});
|
||||
it('excludes code', () => {
|
||||
const converter = new lib.HopDown({ exclude: ['code'] });
|
||||
const converter = new HopDown({ exclude: ['code'] });
|
||||
expect(converter.toHTML('`code`')).toBe('<p>`code`</p>');
|
||||
});
|
||||
it('other tags still work', () => {
|
||||
const converter = new lib.HopDown({ exclude: ['table'] });
|
||||
const converter = new HopDown({ exclude: ['table'] });
|
||||
expect(converter.toHTML('**bold**')).toContain('<strong>bold</strong>');
|
||||
});
|
||||
});
|
||||
@@ -59,7 +60,7 @@ describe('Collision detection', () => {
|
||||
htmlTag: 'span',
|
||||
precedence: 10,
|
||||
});
|
||||
expect(() => new lib.HopDown({
|
||||
expect(() => new HopDown({
|
||||
tags: {
|
||||
...lib.defaultTags,
|
||||
'SPAN': bad,
|
||||
@@ -75,7 +76,7 @@ describe('Collision detection', () => {
|
||||
selector: 'STRONG',
|
||||
toMarkdown: () => '',
|
||||
};
|
||||
expect(() => new lib.HopDown({
|
||||
expect(() => new HopDown({
|
||||
tags: {
|
||||
...lib.defaultTags,
|
||||
'STRONG': dup,
|
||||
@@ -98,7 +99,7 @@ describe('Collision detection', () => {
|
||||
});
|
||||
// Remove default strikethrough to avoid collision with the custom S/DEL tags
|
||||
const { 'DEL,S,STRIKE': _, ...tagsWithoutStrikethrough } = lib.defaultTags;
|
||||
expect(() => new lib.HopDown({
|
||||
expect(() => new HopDown({
|
||||
tags: {
|
||||
...tagsWithoutStrikethrough,
|
||||
'S': short,
|
||||
|
||||
+1
-3
@@ -128,7 +128,7 @@ describe('RibbitEditor modes', () => {
|
||||
editor.run();
|
||||
editor.wysiwyg();
|
||||
editor.view();
|
||||
expect(modes).toEqual(['view', 'wysiwyg', 'edit', 'view']);
|
||||
expect(modes).toEqual(['view', 'wysiwyg', 'view']);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -231,9 +231,7 @@ describe('Editor htmlToMarkdown', () => {
|
||||
it('returns markdown in view state', () => {
|
||||
resetDOM('**bold**');
|
||||
const editor = new lib.Editor({});
|
||||
console.log(editor.getMarkdown());
|
||||
editor.run();
|
||||
console.log(editor.getMarkdown());
|
||||
expect(editor.getMarkdown()).toBe('**bold**');
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { ribbit } from './setup';
|
||||
|
||||
const lib = ribbit();
|
||||
const hopdown = new lib.HopDown();
|
||||
const editor = new lib.Editor({});
|
||||
const hopdown = editor.converter;
|
||||
|
||||
const H = (md: string) => hopdown.toHTML(md);
|
||||
const M = (html: string) => hopdown.toMarkdown(html);
|
||||
const rt = (md: string) => M(H(md));
|
||||
|
||||
|
||||
describe('Markdown → HTML', () => {
|
||||
describe('inline formatting', () => {
|
||||
it('bold', () => expect(H('**bold**')).toBe('<p><strong>bold</strong></p>'));
|
||||
|
||||
+5
-5
@@ -1,9 +1,7 @@
|
||||
import { ribbit } from './setup';
|
||||
import { ribbit, resetDOM } from './setup';
|
||||
|
||||
const lib = ribbit();
|
||||
|
||||
const spacePattern = / /g;
|
||||
|
||||
const macros = [
|
||||
{
|
||||
name: 'user',
|
||||
@@ -13,7 +11,7 @@ const macros = [
|
||||
name: 'npc',
|
||||
toHTML: ({ keywords }: any) => {
|
||||
const name = keywords.join(' ');
|
||||
return '<a href="/NPC/' + name.replace(spacePattern, '') + '">' + name + '</a>';
|
||||
return '<a href="/NPC/' + name.replace(/ /g, '') + '">' + name + '</a>';
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -26,7 +24,9 @@ const macros = [
|
||||
},
|
||||
];
|
||||
|
||||
const converter = new lib.HopDown({ macros });
|
||||
const editor = new lib.Editor({macros: macros});
|
||||
const converter = editor.converter;
|
||||
|
||||
const H = (md: string) => converter.toHTML(md);
|
||||
const M = (html: string) => converter.toMarkdown(html);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user