Feat/frontend vitest (#6469)

* Added vitest tests.

* Added Settings tests to vitest - not working

* Added attributes and attributemap to vitest.

* Added more tests.

* Also run the vitest tests.

* Also run withoutPlugins

* Fixed pnpm lock
This commit is contained in:
SamTV12345 2024-08-16 22:55:42 +02:00 committed by GitHub
parent babfaab4df
commit c7a2dea4d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1092 additions and 552 deletions

View file

@ -0,0 +1,180 @@
'use strict';
const AttributeMap = require('../../../static/js/AttributeMap.js');
const AttributePool = require('../../../static/js/AttributePool');
const attributes = require('../../../static/js/attributes');
import {expect, describe, it, beforeEach} from 'vitest'
describe('AttributeMap', function () {
const attribs = [
['foo', 'bar'],
['baz', 'bif'],
['emptyValue', ''],
];
let pool: { eachAttrib: (arg0: () => number) => void; putAttrib: (arg0: string[]) => any; getAttrib: (arg0: number) => any; };
const getPoolSize = () => {
let n = 0;
pool.eachAttrib(() => ++n);
return n;
};
beforeEach(async function () {
pool = new AttributePool();
for (let i = 0; i < attribs.length; ++i) expect(pool.putAttrib(attribs[i])).to.equal(i);
});
it('fromString works', async function () {
const got = AttributeMap.fromString('*0*1*2', pool);
for (const [k, v] of attribs) expect(got.get(k)).to.equal(v);
// Maps iterate in insertion order, so [...got] should be in the same order as attribs.
expect(JSON.stringify([...got])).to.equal(JSON.stringify(attribs));
});
describe('set', function () {
it('stores the value', async function () {
const m = new AttributeMap(pool);
expect(m.size).to.equal(0);
m.set('k', 'v');
expect(m.size).to.equal(1);
expect(m.get('k')).to.equal('v');
});
it('reuses attributes in the pool', async function () {
expect(getPoolSize()).to.equal(attribs.length);
const m = new AttributeMap(pool);
const [k0, v0] = attribs[0];
m.set(k0, v0);
expect(getPoolSize()).to.equal(attribs.length);
expect(m.size).to.equal(1);
expect(m.toString()).to.equal('*0');
});
it('inserts new attributes into the pool', async function () {
const m = new AttributeMap(pool);
expect(getPoolSize()).to.equal(attribs.length);
m.set('k', 'v');
expect(getPoolSize()).to.equal(attribs.length + 1);
expect(JSON.stringify(pool.getAttrib(attribs.length))).to.equal(JSON.stringify(['k', 'v']));
});
describe('coerces key and value to string', function () {
const testCases = [
['object (with toString)', {toString: () => 'obj'}, 'obj'],
['undefined', undefined, ''],
['null', null, ''],
['boolean', true, 'true'],
['number', 1, '1'],
];
for (const [desc, input, want] of testCases) {
describe(desc as string, function () {
it('key is coerced to string', async function () {
const m = new AttributeMap(pool);
m.set(input, 'value');
expect(m.get(want)).to.equal('value');
});
it('value is coerced to string', async function () {
const m = new AttributeMap(pool);
m.set('key', input);
expect(m.get('key')).to.equal(want);
});
});
}
});
it('returns the map', async function () {
const m = new AttributeMap(pool);
expect(m.set('k', 'v')).to.equal(m);
});
});
describe('toString', function () {
it('sorts attributes', async function () {
const m = new AttributeMap(pool).update(attribs);
const got = [...attributes.attribsFromString(m.toString(), pool)];
const want = attributes.sort([...attribs]);
// Verify that attribs is not already sorted so that this test doesn't accidentally pass.
expect(JSON.stringify(want)).to.not.equal(JSON.stringify(attribs));
expect(JSON.stringify(got)).to.equal(JSON.stringify(want));
});
it('returns all entries', async function () {
const m = new AttributeMap(pool);
expect(m.toString()).to.equal('');
m.set(...attribs[0]);
expect(m.toString()).to.equal('*0');
m.delete(attribs[0][0]);
expect(m.toString()).to.equal('');
m.set(...attribs[1]);
expect(m.toString()).to.equal('*1');
m.set(attribs[1][0], 'new value');
expect(m.toString()).to.equal(attributes.encodeAttribString([attribs.length]));
m.set(...attribs[2]);
expect(m.toString()).to.equal(attributes.attribsToString(
attributes.sort([attribs[2], [attribs[1][0], 'new value']]), pool));
});
});
for (const funcName of ['update', 'updateFromString']) {
const callUpdateFn = (m: any, ...args: (boolean | (string | null | undefined)[][])[]) => {
if (funcName === 'updateFromString') {
// @ts-ignore
args[0] = attributes.attribsToString(attributes.sort([...args[0]]), pool);
}
return AttributeMap.prototype[funcName].call(m, ...args);
};
describe(funcName, function () {
it('works', async function () {
const m = new AttributeMap(pool);
m.set(attribs[2][0], 'value to be overwritten');
callUpdateFn(m, attribs);
for (const [k, v] of attribs) expect(m.get(k)).to.equal(v);
expect(m.size).to.equal(attribs.length);
const wantStr = attributes.attribsToString(attributes.sort([...attribs]), pool);
expect(m.toString()).to.equal(wantStr);
callUpdateFn(m, []);
expect(m.toString()).to.equal(wantStr);
});
it('inserts new attributes into the pool', async function () {
const m = new AttributeMap(pool);
callUpdateFn(m, [['k', 'v']]);
expect(m.size).to.equal(1);
expect(m.get('k')).to.equal('v');
expect(getPoolSize()).to.equal(attribs.length + 1);
expect(m.toString()).to.equal(attributes.encodeAttribString([attribs.length]));
});
it('returns the map', async function () {
const m = new AttributeMap(pool);
expect(callUpdateFn(m, [])).to.equal(m);
});
describe('emptyValueIsDelete=false inserts empty values', function () {
for (const emptyVal of ['', null, undefined]) {
it(emptyVal == null ? String(emptyVal) : JSON.stringify(emptyVal), async function () {
const m = new AttributeMap(pool);
m.set('k', 'v');
callUpdateFn(m, [['k', emptyVal]]);
expect(m.size).to.equal(1);
expect(m.toString()).to.equal(attributes.attribsToString([['k', '']], pool));
});
}
});
describe('emptyValueIsDelete=true deletes entries', function () {
for (const emptyVal of ['', null, undefined]) {
it(emptyVal == null ? String(emptyVal) : JSON.stringify(emptyVal), async function () {
const m = new AttributeMap(pool);
m.set('k', 'v');
callUpdateFn(m, [['k', emptyVal]], true);
expect(m.size).to.equal(0);
expect(m.toString()).to.equal('');
});
}
});
});
}
});

View file

@ -0,0 +1,347 @@
'use strict';
import {APool} from "../../../node/types/PadType";
const AttributePool = require('../../../static/js/AttributePool');
const attributes = require('../../../static/js/attributes');
import {expect, describe, it, beforeEach} from 'vitest';
describe('attributes', function () {
const attribs = [['foo', 'bar'], ['baz', 'bif']];
let pool: APool;
beforeEach(async function () {
pool = new AttributePool();
for (let i = 0; i < attribs.length; ++i) expect(pool.putAttrib(attribs[i])).to.equal(i);
});
describe('decodeAttribString', function () {
it('is a generator function', async function () {
expect(attributes.decodeAttribString.constructor.name).to.equal('GeneratorFunction');
});
describe('rejects invalid attribute strings', function () {
const testCases = ['x', '*0+1', '*A', '*0$', '*', '0', '*-1'];
for (const tc of testCases) {
it(JSON.stringify(tc), async function () {
expect(() => [...attributes.decodeAttribString(tc)])
.toThrowError(/invalid character/);
});
}
});
describe('accepts valid attribute strings', function () {
const testCases = [
['', []],
['*0', [0]],
['*a', [10]],
['*z', [35]],
['*10', [36]],
[
'*0*1*2*3*4*5*6*7*8*9*a*b*c*d*e*f*g*h*i*j*k*l*m*n*o*p*q*r*s*t*u*v*w*x*y*z*10',
[...Array(37).keys()],
],
];
for (const [input, want] of testCases) {
it(`${JSON.stringify(input)} -> ${JSON.stringify(want)}`, async function () {
const got = [...attributes.decodeAttribString(input)];
expect(JSON.stringify(got)).to.equal(JSON.stringify(want));
});
}
});
});
describe('encodeAttribString', function () {
describe('accepts any kind of iterable', function () {
const testCases = [
['generator', (function* () { yield 0; yield 1; })()],
['list', [0, 1]],
['set', new Set([0, 1])],
];
for (const [desc, input] of testCases) {
it(desc as string, async function () {
expect(attributes.encodeAttribString(input)).to.equal('*0*1');
});
}
});
describe('rejects invalid inputs', function () {
const testCases = [
[null, /.*/], // Different browsers may have different error messages.
[[-1], /is negative/],
[['0'], /not a number/],
[[null], /not a number/],
[[0.5], /not an integer/],
[[{}], /not a number/],
[[true], /not a number/],
];
for (const [input, wantErr] of testCases) {
it(JSON.stringify(input), async function () {
expect(() => attributes.encodeAttribString(input)).toThrowError(wantErr as RegExp);
});
}
});
describe('accepts valid inputs', function () {
const testCases = [
[[], ''],
[[0], '*0'],
[[10], '*a'],
[[35], '*z'],
[[36], '*10'],
[
[...Array(37).keys()],
'*0*1*2*3*4*5*6*7*8*9*a*b*c*d*e*f*g*h*i*j*k*l*m*n*o*p*q*r*s*t*u*v*w*x*y*z*10',
],
];
for (const [input, want] of testCases) {
it(`${JSON.stringify(input)} -> ${JSON.stringify(want)}`, async function () {
expect(attributes.encodeAttribString(input)).to.equal(want);
});
}
});
});
describe('attribsFromNums', function () {
it('is a generator function', async function () {
expect(attributes.attribsFromNums.constructor.name).to.equal("GeneratorFunction");
});
describe('accepts any kind of iterable', function () {
const testCases = [
['generator', (function* () { yield 0; yield 1; })()],
['list', [0, 1]],
['set', new Set([0, 1])],
];
for (const [desc, input] of testCases) {
it(desc as string, async function () {
const gotAttribs = [...attributes.attribsFromNums(input, pool)];
expect(JSON.stringify(gotAttribs)).to.equal(JSON.stringify(attribs));
});
}
});
describe('rejects invalid inputs', function () {
const testCases = [
[null, /.*/], // Different browsers may have different error messages.
[[-1], /is negative/],
[['0'], /not a number/],
[[null], /not a number/],
[[0.5], /not an integer/],
[[{}], /not a number/],
[[true], /not a number/],
[[9999], /does not exist in pool/],
];
for (const [input, wantErr] of testCases) {
it(JSON.stringify(input), async function () {
expect(() => [...attributes.attribsFromNums(input, pool)]).toThrowError(wantErr as RegExp);
});
}
});
describe('accepts valid inputs', function () {
const testCases = [
[[], []],
[[0], [attribs[0]]],
[[1], [attribs[1]]],
[[0, 1], [attribs[0], attribs[1]]],
[[1, 0], [attribs[1], attribs[0]]],
];
for (const [input, want] of testCases) {
it(`${JSON.stringify(input)} -> ${JSON.stringify(want)}`, async function () {
const gotAttribs = [...attributes.attribsFromNums(input, pool)];
expect(JSON.stringify(gotAttribs)).to.equal(JSON.stringify(want));
});
}
});
});
describe('attribsToNums', function () {
it('is a generator function', async function () {
expect(attributes.attribsToNums.constructor.name).to.equal("GeneratorFunction")
});
describe('accepts any kind of iterable', function () {
const testCases = [
['generator', (function* () { yield attribs[0]; yield attribs[1]; })()],
['list', [attribs[0], attribs[1]]],
['set', new Set([attribs[0], attribs[1]])],
];
for (const [desc, input] of testCases) {
it(desc as string, async function () {
const gotNums = [...attributes.attribsToNums(input, pool)];
expect(JSON.stringify(gotNums)).to.equal(JSON.stringify([0, 1]));
});
}
});
describe('rejects invalid inputs', function () {
const testCases = [null, [null]];
for (const input of testCases) {
it(JSON.stringify(input), async function () {
expect(() => [...attributes.attribsToNums(input, pool)]).toThrowError();
});
}
});
describe('reuses existing pool entries', function () {
const testCases = [
[[], []],
[[attribs[0]], [0]],
[[attribs[1]], [1]],
[[attribs[0], attribs[1]], [0, 1]],
[[attribs[1], attribs[0]], [1, 0]],
];
for (const [input, want] of testCases) {
it(`${JSON.stringify(input)} -> ${JSON.stringify(want)}`, async function () {
const got = [...attributes.attribsToNums(input, pool)];
expect(JSON.stringify(got)).to.equal(JSON.stringify(want));
});
}
});
describe('inserts new attributes into the pool', function () {
const testCases = [
[[['k', 'v']], [attribs.length]],
[[attribs[0], ['k', 'v']], [0, attribs.length]],
[[['k', 'v'], attribs[0]], [attribs.length, 0]],
];
for (const [input, want] of testCases) {
it(`${JSON.stringify(input)} -> ${JSON.stringify(want)}`, async function () {
const got = [...attributes.attribsToNums(input, pool)];
expect(JSON.stringify(got)).to.equal(JSON.stringify(want));
expect(JSON.stringify(pool.getAttrib(attribs.length)))
.to.equal(JSON.stringify(['k', 'v']));
});
}
});
describe('coerces key and value to string', function () {
const testCases = [
['object (with toString)', {toString: () => 'obj'}, 'obj'],
['undefined', undefined, ''],
['null', null, ''],
['boolean', true, 'true'],
['number', 1, '1'],
];
for (const [desc, inputVal, wantVal] of testCases) {
describe(desc as string, function () {
for (const [desc, inputAttribs, wantAttribs] of [
['key is coerced to string', [[inputVal, 'value']], [[wantVal, 'value']]],
['value is coerced to string', [['key', inputVal]], [['key', wantVal]]],
]) {
it(desc as string, async function () {
const gotNums = [...attributes.attribsToNums(inputAttribs, pool)];
// Each attrib in inputAttribs is expected to be new to the pool.
const wantNums = [...Array(attribs.length + 1).keys()].slice(attribs.length);
expect(JSON.stringify(gotNums)).to.equal(JSON.stringify(wantNums));
const gotAttribs = gotNums.map((n) => pool.getAttrib(n));
expect(JSON.stringify(gotAttribs)).to.equal(JSON.stringify(wantAttribs));
});
}
});
}
});
});
describe('attribsFromString', function () {
it('is a generator function', async function () {
expect(attributes.attribsFromString.constructor.name).to.equal('GeneratorFunction');
});
describe('rejects invalid attribute strings', function () {
const testCases = [
['x', /invalid character/],
['*0+1', /invalid character/],
['*A', /invalid character/],
['*0$', /invalid character/],
['*', /invalid character/],
['0', /invalid character/],
['*-1', /invalid character/],
['*9999', /does not exist in pool/],
];
for (const [input, wantErr] of testCases) {
it(JSON.stringify(input), async function () {
expect(() => [...attributes.attribsFromString(input, pool)]).toThrowError(wantErr);
});
}
});
describe('accepts valid inputs', function () {
const testCases = [
['', []],
['*0', [attribs[0]]],
['*1', [attribs[1]]],
['*0*1', [attribs[0], attribs[1]]],
['*1*0', [attribs[1], attribs[0]]],
];
for (const [input, want] of testCases) {
it(`${JSON.stringify(input)} -> ${JSON.stringify(want)}`, async function () {
const gotAttribs = [...attributes.attribsFromString(input, pool)];
expect(JSON.stringify(gotAttribs)).to.equal(JSON.stringify(want));
});
}
});
});
describe('attribsToString', function () {
describe('accepts any kind of iterable', function () {
const testCases = [
['generator', (function* () { yield attribs[0]; yield attribs[1]; })()],
['list', [attribs[0], attribs[1]]],
['set', new Set([attribs[0], attribs[1]])],
];
for (const [desc, input] of testCases) {
it(desc as string, async function () {
const got = attributes.attribsToString(input, pool);
expect(got).to.equal('*0*1');
});
}
});
describe('rejects invalid inputs', function () {
const testCases = [null, [null]];
for (const input of testCases) {
it(JSON.stringify(input), async function () {
expect(() => attributes.attribsToString(input, pool)).toThrowError();
});
}
});
describe('reuses existing pool entries', function () {
const testCases = [
[[], ''],
[[attribs[0]], '*0'],
[[attribs[1]], '*1'],
[[attribs[0], attribs[1]], '*0*1'],
[[attribs[1], attribs[0]], '*1*0'],
];
for (const [input, want] of testCases) {
it(`${JSON.stringify(input)} -> ${JSON.stringify(want)}`, async function () {
const got = attributes.attribsToString(input, pool);
expect(got).to.equal(want);
});
}
});
describe('inserts new attributes into the pool', function () {
const testCases = [
[[['k', 'v']], `*${attribs.length}`],
[[attribs[0], ['k', 'v']], `*0*${attribs.length}`],
[[['k', 'v'], attribs[0]], `*${attribs.length}*0`],
];
for (const [input, want] of testCases) {
it(`${JSON.stringify(input)} -> ${JSON.stringify(want)}`, async function () {
const got = attributes.attribsToString(input, pool);
expect(got).to.equal(want);
expect(JSON.stringify(pool.getAttrib(attribs.length)))
.to.equal(JSON.stringify(['k', 'v']));
});
}
});
});
});

View file

@ -0,0 +1,398 @@
'use strict';
/*
* While importexport tests target the `setHTML` API endpoint, which is nearly identical to what
* happens when a user manually imports a document via the UI, the contentcollector tests here don't
* use rehype to process the document. Rehype removes spaces and newĺines were applicable, so the
* expected results here can differ from importexport.js.
*
* If you add tests here, please also add them to importexport.js
*/
import {APool} from "../../../node/types/PadType";
const AttributePool = require('../../../static/js/AttributePool');
const Changeset = require('../../../static/js/Changeset');
const assert = require('assert').strict;
const attributes = require('../../../static/js/attributes');
const contentcollector = require('../../../static/js/contentcollector');
const jsdom = require('jsdom');
import {describe, it, beforeAll, test} from 'vitest';
// All test case `wantAlines` values must only refer to attributes in this list so that the
// attribute numbers do not change due to changes in pool insertion order.
const knownAttribs = [
['insertorder', 'first'],
['italic', 'true'],
['list', 'bullet1'],
['list', 'bullet2'],
['list', 'number1'],
['list', 'number2'],
['lmkr', '1'],
['start', '1'],
['start', '2'],
];
const testCases = [
{
description: 'Simple',
html: '<html><body><p>foo</p></body></html>',
wantAlines: ['+3'],
wantText: ['foo'],
},
{
description: 'Line starts with asterisk',
html: '<html><body><p>*foo</p></body></html>',
wantAlines: ['+4'],
wantText: ['*foo'],
},
{
description: 'Complex nested Li',
html: '<!doctype html><html><body><ol><li>one</li><li><ol><li>1.1</li></ol></li><li>two</li></ol></body></html>',
wantAlines: [
'*0*4*6*7+1+3',
'*0*5*6*8+1+3',
'*0*4*6*8+1+3',
],
wantText: [
'*one', '*1.1', '*two',
],
},
{
description: 'Complex list of different types',
html: '<!doctype html><html><body><ul class="bullet"><li>one</li><li>two</li><li>0</li><li>1</li><li>2<ul class="bullet"><li>3</li><li>4</li></ul></li></ul><ol class="number"><li>item<ol class="number"><li>item1</li><li>item2</li></ol></li></ol></body></html>',
wantAlines: [
'*0*2*6+1+3',
'*0*2*6+1+3',
'*0*2*6+1+1',
'*0*2*6+1+1',
'*0*2*6+1+1',
'*0*3*6+1+1',
'*0*3*6+1+1',
'*0*4*6*7+1+4',
'*0*5*6*8+1+5',
'*0*5*6*8+1+5',
],
wantText: [
'*one',
'*two',
'*0',
'*1',
'*2',
'*3',
'*4',
'*item',
'*item1',
'*item2',
],
},
{
description: 'Tests if uls properly get attributes',
html: '<html><body><ul><li>a</li><li>b</li></ul><div>div</div><p>foo</p></body></html>',
wantAlines: [
'*0*2*6+1+1',
'*0*2*6+1+1',
'+3',
'+3',
],
wantText: ['*a', '*b', 'div', 'foo'],
},
{
description: 'Tests if indented uls properly get attributes',
html: '<html><body><ul><li>a</li><ul><li>b</li></ul><li>a</li></ul><p>foo</p></body></html>',
wantAlines: [
'*0*2*6+1+1',
'*0*3*6+1+1',
'*0*2*6+1+1',
'+3',
],
wantText: ['*a', '*b', '*a', 'foo'],
},
{
description: 'Tests if ols properly get line numbers when in a normal OL',
html: '<html><body><ol><li>a</li><li>b</li><li>c</li></ol><p>test</p></body></html>',
wantAlines: [
'*0*4*6*7+1+1',
'*0*4*6*7+1+1',
'*0*4*6*7+1+1',
'+4',
],
wantText: ['*a', '*b', '*c', 'test'],
noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?',
},
{
description: 'A single completely empty line break within an ol should reset count if OL is closed off..',
html: '<html><body><ol><li>should be 1</li></ol><p>hello</p><ol><li>should be 1</li><li>should be 2</li></ol><p></p></body></html>',
wantAlines: [
'*0*4*6*7+1+b',
'+5',
'*0*4*6*8+1+b',
'*0*4*6*8+1+b',
'',
],
wantText: ['*should be 1', 'hello', '*should be 1', '*should be 2', ''],
noteToSelf: "Shouldn't include attribute marker in the <p> line",
},
{
description: 'A single <p></p> should create a new line',
html: '<html><body><p></p><p></p></body></html>',
wantAlines: ['', ''],
wantText: ['', ''],
noteToSelf: '<p></p>should create a line break but not break numbering',
},
{
description: 'Tests if ols properly get line numbers when in a normal OL #2',
html: '<html><body>a<ol><li>b<ol><li>c</li></ol></ol>notlist<p>foo</p></body></html>',
wantAlines: [
'+1',
'*0*4*6*7+1+1',
'*0*5*6*8+1+1',
'+7',
'+3',
],
wantText: ['a', '*b', '*c', 'notlist', 'foo'],
noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?',
},
{
description: 'First item being an UL then subsequent being OL will fail',
html: '<html><body><ul><li>a<ol><li>b</li><li>c</li></ol></li></ul></body></html>',
wantAlines: ['+1', '*0*1*2*3+1+1', '*0*4*2*5+1+1'],
wantText: ['a', '*b', '*c'],
noteToSelf: 'Ensure empty P does not induce line attribute marker, wont this break the editor?',
disabled: true,
},
{
description: 'A single completely empty line break within an ol should NOT reset count',
html: '<html><body><ol><li>should be 1</li><p></p><li>should be 2</li><li>should be 3</li></ol><p></p></body></html>',
wantAlines: [],
wantText: ['*should be 1', '*should be 2', '*should be 3'],
noteToSelf: "<p></p>should create a line break but not break numbering -- This is what I can't get working!",
disabled: true,
},
{
description: 'Content outside body should be ignored',
html: '<html><head><title>title</title><style></style></head><body>empty<br></body></html>',
wantAlines: ['+5'],
wantText: ['empty'],
},
{
description: 'Multiple spaces should be preserved',
html: '<html><body>Text with more than one space.<br></body></html>',
wantAlines: ['+10'],
wantText: ['Text with more than one space.'],
},
{
description: 'non-breaking and normal space should be preserved',
html: '<html><body>Text&nbsp;with&nbsp; more&nbsp;&nbsp;&nbsp;than &nbsp;one space.<br></body></html>',
wantAlines: ['+10'],
wantText: ['Text with more than one space.'],
},
{
description: 'Multiple nbsp should be preserved',
html: '<html><body>&nbsp;&nbsp;<br></body></html>',
wantAlines: ['+2'],
wantText: [' '],
},
{
description: 'Multiple nbsp between words ',
html: '<html><body>&nbsp;&nbsp;word1&nbsp;&nbsp;word2&nbsp;&nbsp;&nbsp;word3<br></body></html>',
wantAlines: ['+m'],
wantText: [' word1 word2 word3'],
},
{
description: 'A non-breaking space preceded by a normal space',
html: '<html><body> &nbsp;word1 &nbsp;word2 &nbsp;word3<br></body></html>',
wantAlines: ['+l'],
wantText: [' word1 word2 word3'],
},
{
description: 'A non-breaking space followed by a normal space',
html: '<html><body>&nbsp; word1&nbsp; word2&nbsp; word3<br></body></html>',
wantAlines: ['+l'],
wantText: [' word1 word2 word3'],
},
{
description: 'Don\'t collapse spaces that follow a newline',
html: '<!doctype html><html><body>something<br> something<br></body></html>',
wantAlines: ['+9', '+m'],
wantText: ['something', ' something'],
},
{
description: 'Don\'t collapse spaces that follow a empty paragraph',
html: '<!doctype html><html><body>something<p></p> something<br></body></html>',
wantAlines: ['+9', '', '+m'],
wantText: ['something', '', ' something'],
},
{
description: 'Don\'t collapse spaces that preceed/follow a newline',
html: '<html><body>something <br> something<br></body></html>',
wantAlines: ['+l', '+m'],
wantText: ['something ', ' something'],
},
{
description: 'Don\'t collapse spaces that preceed/follow a empty paragraph',
html: '<html><body>something <p></p> something<br></body></html>',
wantAlines: ['+l', '', '+m'],
wantText: ['something ', '', ' something'],
},
{
description: 'Don\'t collapse non-breaking spaces that follow a newline',
html: '<html><body>something<br>&nbsp;&nbsp;&nbsp;something<br></body></html>',
wantAlines: ['+9', '+c'],
wantText: ['something', ' something'],
},
{
description: 'Don\'t collapse non-breaking spaces that follow a paragraph',
html: '<html><body>something<p></p>&nbsp;&nbsp;&nbsp;something<br></body></html>',
wantAlines: ['+9', '', '+c'],
wantText: ['something', '', ' something'],
},
{
description: 'Preserve all spaces when multiple are present',
html: '<html><body>Need <span> more </span> space<i> s </i> !<br></body></html>',
wantAlines: ['+h*1+4+2'],
wantText: ['Need more space s !'],
},
{
description: 'Newlines and multiple spaces across newlines should be preserved',
html: `
<html><body>Need
<span> more </span>
space
<i> s </i>
!<br></body></html>`,
wantAlines: ['+19*1+4+b'],
wantText: ['Need more space s !'],
},
{
description: 'Multiple new lines at the beginning should be preserved',
html: '<html><body><br><br><p></p><p></p>first line<br><br>second line<br></body></html>',
wantAlines: ['', '', '', '', '+a', '', '+b'],
wantText: ['', '', '', '', 'first line', '', 'second line'],
},
{
description: 'A paragraph with multiple lines should not loose spaces when lines are combined',
html: `<html><body><p>
а б в г ґ д е є ж з и і ї й к л м н о
п р с т у ф х ц ч ш щ ю я ь</p>
</body></html>`,
wantAlines: ['+1t'],
wantText: ['а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ю я ь'],
},
{
description: 'lines in preformatted text should be kept intact',
html: `<html><body><p>
а б в г ґ д е є ж з и і ї й к л м н о</p><pre>multiple
lines
in
pre
</pre><p>п р с т у ф х ц ч ш щ ю я
ь</p>
</body></html>`,
wantAlines: ['+11', '+8', '+5', '+2', '+3', '+r'],
wantText: [
'а б в г ґ д е є ж з и і ї й к л м н о',
'multiple',
'lines',
'in',
'pre',
'п р с т у ф х ц ч ш щ ю я ь',
],
},
{
description: 'pre should be on a new line not preceded by a space',
html: `<html><body><p>
1
</p><pre>preline
</pre></body></html>`,
wantAlines: ['+6', '+7'],
wantText: [' 1 ', 'preline'],
},
{
description: 'Preserve spaces on the beginning and end of a element',
html: '<html><body>Need<span> more </span>space<i> s </i>!<br></body></html>',
wantAlines: ['+f*1+3+1'],
wantText: ['Need more space s !'],
},
{
description: 'Preserve spaces outside elements',
html: '<html><body>Need <span>more</span> space <i>s</i> !<br></body></html>',
wantAlines: ['+g*1+1+2'],
wantText: ['Need more space s !'],
},
{
description: 'Preserve spaces at the end of an element',
html: '<html><body>Need <span>more </span>space <i>s </i>!<br></body></html>',
wantAlines: ['+g*1+2+1'],
wantText: ['Need more space s !'],
},
{
description: 'Preserve spaces at the start of an element',
html: '<html><body>Need<span> more</span> space<i> s</i> !<br></body></html>',
wantAlines: ['+f*1+2+2'],
wantText: ['Need more space s !'],
},
];
describe(__filename, function () {
for (const tc of testCases) {
describe(tc.description, function () {
let apool: APool;
let result: {
lines: string[],
lineAttribs: string[],
};
if (tc.disabled) {
test.skip('If disabled we do not run the test');
return;
}
beforeAll(async function () {
const {window: {document}} = new jsdom.JSDOM(tc.html);
apool = new AttributePool();
// To reduce test fragility, the attribute pool is seeded with `knownAttribs`, and all
// attributes in `tc.wantAlines` must be in `knownAttribs`. (This guarantees that attribute
// numbers do not change if the attribute processing code changes.)
for (const attrib of knownAttribs) apool.putAttrib(attrib);
for (const aline of tc.wantAlines) {
for (const op of Changeset.deserializeOps(aline)) {
for (const n of attributes.decodeAttribString(op.attribs)) {
assert(n < knownAttribs.length);
}
}
}
const cc = contentcollector.makeContentCollector(true, null, apool);
cc.collectContent(document.body);
result = cc.finish();
console.log(result);
});
it('text matches', async function () {
assert.deepEqual(result.lines, tc.wantText);
});
it('alines match', async function () {
assert.deepEqual(result.lineAttribs, tc.wantAlines);
});
it('attributes are sorted in canonical order', async function () {
const gotAttribs:string[][][] = [];
const wantAttribs = [];
for (const aline of result.lineAttribs) {
const gotAlineAttribs:string[][] = [];
gotAttribs.push(gotAlineAttribs);
const wantAlineAttribs:string[] = [];
wantAttribs.push(wantAlineAttribs);
for (const op of Changeset.deserializeOps(aline)) {
const gotOpAttribs:string[] = [...attributes.attribsFromString(op.attribs, apool)];
gotAlineAttribs.push(gotOpAttribs);
wantAlineAttribs.push(attributes.sort([...gotOpAttribs]));
}
}
assert.deepEqual(gotAttribs, wantAttribs);
});
});
}
});

View file

@ -0,0 +1,42 @@
import {MapArrayType} from "../../../node/types/MapType";
const {padutils} = require('../../../static/js/pad_utils');
import {describe, it, expect, afterEach, beforeAll} from "vitest";
describe(__filename, function () {
describe('warnDeprecated', function () {
const {warnDeprecated} = padutils;
const backups:MapArrayType<any> = {};
beforeAll(async function () {
backups.logger = warnDeprecated.logger;
});
afterEach(async function () {
warnDeprecated.logger = backups.logger;
delete warnDeprecated._rl; // Reset internal rate limiter state.
});
/*it('includes the stack', async function () {
let got;
warnDeprecated.logger = {warn: (stack: any) => got = stack};
warnDeprecated();
assert(got!.includes(__filename));
});*/
it('rate limited', async function () {
let got = 0;
warnDeprecated.logger = {warn: () => ++got};
warnDeprecated(); // Initialize internal rate limiter state.
const {period} = warnDeprecated._rl;
got = 0;
const testCases = [[0, 1], [0, 1], [period - 1, 1], [period, 2]];
for (const [now, want] of testCases) { // In a loop so that the stack trace is the same.
warnDeprecated._rl.now = () => now;
warnDeprecated();
expect(got).toEqual(want);
}
warnDeprecated(); // Should have a different stack trace.
expect(got).toEqual(testCases[testCases.length - 1][1] + 1);
});
});
});

View file

@ -0,0 +1,98 @@
import {strict as assert} from "assert";
import path from 'path';
import sanitizePathname from '../../../node/utils/sanitizePathname';
import {describe, it, expect} from 'vitest';
describe(__filename, function () {
describe('absolute paths rejected', function () {
const testCases = [
['posix', '/'],
['posix', '/foo'],
['win32', '/'],
['win32', '\\'],
['win32', 'C:/foo'],
['win32', 'C:\\foo'],
['win32', 'c:/foo'],
['win32', 'c:\\foo'],
['win32', '/foo'],
['win32', '\\foo'],
];
for (const [platform, p] of testCases) {
it(`${platform} ${p}`, async function () {
// @ts-ignore
expect(() => sanitizePathname(p, path[platform] as any)).toThrowError(/absolute path/);
});
}
});
describe('directory traversal rejected', function () {
const testCases = [
['posix', '..'],
['posix', '../'],
['posix', '../foo'],
['posix', 'foo/../..'],
['win32', '..'],
['win32', '../'],
['win32', '..\\'],
['win32', '../foo'],
['win32', '..\\foo'],
['win32', 'foo/../..'],
['win32', 'foo\\..\\..'],
];
for (const [platform, p] of testCases) {
it(`${platform} ${p}`, async function () {
// @ts-ignore
assert.throws(() => sanitizePathname(p, path[platform]), {message: /travers/});
});
}
});
describe('accepted paths', function () {
const testCases = [
['posix', '', '.'],
['posix', '.'],
['posix', './'],
['posix', 'foo'],
['posix', 'foo/'],
['posix', 'foo/bar/..', 'foo'],
['posix', 'foo/bar/../', 'foo/'],
['posix', './foo', 'foo'],
['posix', 'foo/bar'],
['posix', 'foo\\bar'],
['posix', '\\foo'],
['posix', '..\\foo'],
['posix', 'foo/../bar', 'bar'],
['posix', 'C:/foo'],
['posix', 'C:\\foo'],
['win32', '', '.'],
['win32', '.'],
['win32', './'],
['win32', '.\\', './'],
['win32', 'foo'],
['win32', 'foo/'],
['win32', 'foo\\', 'foo/'],
['win32', 'foo/bar/..', 'foo'],
['win32', 'foo\\bar\\..', 'foo'],
['win32', 'foo/bar/../', 'foo/'],
['win32', 'foo\\bar\\..\\', 'foo/'],
['win32', './foo', 'foo'],
['win32', '.\\foo', 'foo'],
['win32', 'foo/bar'],
['win32', 'foo\\bar', 'foo/bar'],
['win32', 'foo/../bar', 'bar'],
['win32', 'foo\\..\\bar', 'bar'],
['win32', 'foo/..\\bar', 'bar'],
['win32', 'foo\\../bar', 'bar'],
];
for (const [platform, p, tcWant] of testCases) {
const want = tcWant == null ? p : tcWant;
it(`${platform} ${p || '<empty string>'} -> ${want}`, async function () {
// @ts-ignore
assert.equal(sanitizePathname(p, path[platform]), want);
});
}
});
it('default path API', async function () {
assert.equal(sanitizePathname('foo'), 'foo');
});
});

View file

@ -0,0 +1,55 @@
'use strict';
const SkipList = require('ep_etherpad-lite/static/js/skiplist');
import {expect, describe, it} from 'vitest';
describe('skiplist.js', function () {
it('rejects null keys', async function () {
const skiplist = new SkipList();
for (const key of [undefined, null]) {
expect(() => skiplist.push({key})).toThrowError();
}
});
it('rejects duplicate keys', async function () {
const skiplist = new SkipList();
skiplist.push({key: 'foo'});
expect(() => skiplist.push({key: 'foo'})).toThrowError();
});
it('atOffset() returns last entry that touches offset', async function () {
const skiplist = new SkipList();
const entries: { key: string; width: number; }[] = [];
let nextId = 0;
const makeEntry = (width: number) => {
const entry = {key: `id${nextId++}`, width};
entries.push(entry);
return entry;
};
skiplist.push(makeEntry(5));
expect(skiplist.atOffset(4)).toBe(entries[0]);
expect(skiplist.atOffset(5)).toBe(entries[0]);
expect(() => skiplist.atOffset(6)).toThrowError();
skiplist.push(makeEntry(0));
expect(skiplist.atOffset(4)).toBe(entries[0]);
expect(skiplist.atOffset(5)).toBe(entries[1]);
expect(() => skiplist.atOffset(6)).toThrowError();
skiplist.push(makeEntry(0));
expect(skiplist.atOffset(4)).toBe(entries[0]);
expect(skiplist.atOffset(5)).toBe(entries[2]);
expect(() => skiplist.atOffset(6)).toThrowError();
skiplist.splice(2, 0, [makeEntry(0)]);
expect(skiplist.atOffset(4)).toBe(entries[0]);
expect(skiplist.atOffset(5)).toBe(entries[2]);
expect(() => skiplist.atOffset(6)).toThrowError();
skiplist.push(makeEntry(3));
expect(skiplist.atOffset(4)).toBe(entries[0]);
expect(skiplist.atOffset(5)).toBe(entries[4]);
expect(skiplist.atOffset(6)).toBe(entries[4]);
});
});