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

@ -69,6 +69,9 @@ jobs:
-
name: Run the backend tests
run: pnpm test
- name: Run the new vitest tests
working-directory: src
run: pnpm run test:vitest
withpluginsLinux:
# run on pushes to any branch
@ -142,6 +145,9 @@ jobs:
-
name: Run the backend tests
run: pnpm test
- name: Run the new vitest tests
working-directory: src
run: pnpm run test:vitest
withoutpluginsWindows:
# run on pushes to any branch
@ -193,7 +199,11 @@ jobs:
powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json"
-
name: Run the backend tests
run: cd src && pnpm test
working-directory: src
run: pnpm test
- name: Run the new vitest tests
working-directory: src
run: pnpm run test:vitest
withpluginsWindows:
# run on pushes to any branch
@ -273,4 +283,8 @@ jobs:
powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json"
-
name: Run the backend tests
run: cd src && pnpm test
working-directory: src
run: pnpm test
- name: Run the new vitest tests
working-directory: src
run: pnpm run test:vitest

File diff suppressed because it is too large Load diff

View file

@ -4,11 +4,10 @@ import {MapArrayType} from "../../types/MapType";
import {PartType} from "../../types/PartType";
const fs = require('fs').promises;
const minify = require('../../utils/Minify');
const path = require('path');
import {minify} from '../../utils/Minify';
import path from 'node:path';
const plugins = require('../../../static/js/pluginfw/plugin_defs');
const settings = require('../../utils/Settings');
import CachingMiddleware from '../../utils/caching_middleware';
// Rewrite tar to include modules with no extensions and proper rooted paths.
const getTar = async () => {
@ -32,15 +31,10 @@ const getTar = async () => {
};
exports.expressPreSession = async (hookName:string, {app}:any) => {
// Cache both minified and static.
const assetCache = new CachingMiddleware();
// Cache static assets
app.all(/\/js\/(.*)/, assetCache.handle.bind(assetCache));
app.all(/\/css\/(.*)/, assetCache.handle.bind(assetCache));
// Minify will serve static files compressed (minify enabled). It also has
// file-specific hacks for ace/require-kernel/etc.
app.all('/static/:filename(*)', minify.minify);
app.all('/static/:filename(*)', minify);
// serve plugin definitions
// not very static, but served here so that client can do

View file

@ -35,6 +35,7 @@ export type APool = {
clone: ()=>APool,
check: ()=>Promise<void>,
eachAttrib: (callback: (key: string, value: any)=>void)=>void,
getAttrib: (key: number)=>any,
}

View file

@ -21,20 +21,20 @@
* limitations under the License.
*/
const settings = require('./Settings');
const fs = require('fs').promises;
const path = require('path');
const plugins = require('../../static/js/pluginfw/plugin_defs');
const mime = require('mime-types');
const Threads = require('threads');
const log4js = require('log4js');
const sanitizePathname = require('./sanitizePathname');
import {TransformResult} from "esbuild";
import mime from 'mime-types';
import log4js from 'log4js';
import {compressCSS, compressJS} from './MinifyWorker'
const settings = require('./Settings');
import {promises as fs} from 'fs';
import path from 'node:path';
const plugins = require('../../static/js/pluginfw/plugin_defs');
import sanitizePathname from './sanitizePathname';
const logger = log4js.getLogger('Minify');
const ROOT_DIR = path.join(settings.root, 'src/static/');
const threadsPool = new Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2);
const LIBRARY_WHITELIST = [
'async',
@ -48,10 +48,10 @@ const LIBRARY_WHITELIST = [
// What follows is a terrible hack to avoid loop-back within the server.
// TODO: Serve files from another service, or directly from the file system.
const requestURI = async (url, method, headers) => {
const requestURI = async (url: string | URL, method: any, headers: { [x: string]: any; }) => {
const parsedUrl = new URL(url);
let status = 500;
const content = [];
const content: any[] = [];
const mockRequest = {
url,
method,
@ -61,7 +61,7 @@ const requestURI = async (url, method, headers) => {
let mockResponse;
const p = new Promise((resolve) => {
mockResponse = {
writeHead: (_status, _headers) => {
writeHead: (_status: number, _headers: { [x: string]: any; }) => {
status = _status;
for (const header in _headers) {
if (Object.prototype.hasOwnProperty.call(_headers, header)) {
@ -69,37 +69,63 @@ const requestURI = async (url, method, headers) => {
}
}
},
setHeader: (header, value) => {
setHeader: (header: string, value: { toString: () => any; }) => {
headers[header.toLowerCase()] = value.toString();
},
header: (header, value) => {
header: (header: string, value: { toString: () => any; }) => {
headers[header.toLowerCase()] = value.toString();
},
write: (_content) => {
write: (_content: any) => {
_content && content.push(_content);
},
end: (_content) => {
end: (_content: any) => {
_content && content.push(_content);
resolve([status, headers, content.join('')]);
},
};
});
await minify(mockRequest, mockResponse);
await _minify(mockRequest, mockResponse);
return await p;
};
const requestURIs = (locations, method, headers, callback) => {
const _requestURIs = (locations: any[], method: any, headers: {
[x: string]:
/**
* This Module manages all /minified/* requests. It controls the
* minification && compression of Javascript and CSS.
*/
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* 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.
*/
any;
}, callback: (arg0: any[], arg1: any[], arg2: any[]) => void) => {
Promise.all(locations.map(async (loc) => {
try {
return await requestURI(loc, method, headers);
} catch (err) {
logger.debug(`requestURI(${JSON.stringify(loc)}, ${JSON.stringify(method)}, ` +
`${JSON.stringify(headers)}) failed: ${err.stack || err}`);
// @ts-ignore
`${JSON.stringify(headers)}) failed: ${err.stack || err}`);
return [500, headers, ''];
}
})).then((responses) => {
// @ts-ignore
const statuss = responses.map((x) => x[0]);
// @ts-ignore
const headerss = responses.map((x) => x[1]);
// @ts-ignore
const contentss = responses.map((x) => x[2]);
callback(statuss, headerss, contentss);
});
@ -119,11 +145,12 @@ const compatPaths = {
* @param req the Express request
* @param res the Express response
*/
const minify = async (req, res) => {
const _minify = async (req:any, res:any) => {
let filename = req.params.filename;
try {
filename = sanitizePathname(filename);
} catch (err) {
// @ts-ignore
logger.error(`sanitization of pathname "${filename}" failed: ${err.stack || err}`);
res.writeHead(404, {});
res.end();
@ -131,6 +158,7 @@ const minify = async (req, res) => {
}
// Backward compatibility for plugins that require() files from old paths.
// @ts-ignore
const newLocation = compatPaths[filename.replace(/^plugins\/ep_etherpad-lite\/static\//, '')];
if (newLocation != null) {
logger.warn(`request for deprecated path "${filename}", replacing with "${newLocation}"`);
@ -193,7 +221,7 @@ const minify = async (req, res) => {
res.writeHead(200, {});
res.end();
} else if (req.method === 'GET') {
const content = await getFileCompressed(filename, contentType);
const content = await getFileCompressed(filename, contentType as string);
res.header('Content-Type', contentType);
res.writeHead(200, {});
res.write(content);
@ -205,7 +233,7 @@ const minify = async (req, res) => {
};
// Check for the existance of the file and get the last modification date.
const statFile = async (filename, dirStatLimit) => {
const statFile = async (filename: string, dirStatLimit: number):Promise<(any | boolean)[]> => {
/*
* The only external call to this function provides an explicit value for
* dirStatLimit: this check could be removed.
@ -221,6 +249,7 @@ const statFile = async (filename, dirStatLimit) => {
try {
stats = await fs.stat(path.resolve(ROOT_DIR, filename));
} catch (err) {
// @ts-ignore
if (['ENOENT', 'ENOTDIR'].includes(err.code)) {
// Stat the directory instead.
const [date] = await statFile(path.dirname(filename), dirStatLimit - 1);
@ -234,69 +263,64 @@ const statFile = async (filename, dirStatLimit) => {
let contentCache = new Map();
const getFileCompressed = async (filename, contentType) => {
const getFileCompressed = async (filename: any, contentType: string) => {
if (contentCache.has(filename)) {
return contentCache.get(filename);
}
let content = await getFile(filename);
let content: Buffer|string = await getFile(filename);
if (!content || !settings.minify) {
return content;
} else if (contentType === 'application/javascript') {
return await new Promise((resolve) => {
threadsPool.queue(async ({compressJS}) => {
return await new Promise(async (resolve) => {
try {
logger.info('Compress JS file %s.', filename);
content = content.toString();
try {
logger.info('Compress JS file %s.', filename);
content = content.toString();
const compressResult = await compressJS(content);
if (compressResult.error) {
console.error(`Error compressing JS (${filename}) using terser`, compressResult.error);
} else {
content = compressResult.code.toString(); // Convert content obj code to string
}
let compressResult: TransformResult<{ minify: boolean }>
compressResult = await compressJS(content);
content = compressResult.code.toString(); // Convert content obj code to string
} catch (error) {
console.error('getFile() returned an error in ' +
`getFileCompressed(${filename}, ${contentType}): ${error}`);
console.error(`Error compressing JS (${filename}) using esbuild`, error);
}
contentCache.set(filename, content);
resolve(content);
});
} catch (error) {
console.error('getFile() returned an error in ' +
`getFileCompressed(${filename}, ${contentType}): ${error}`);
}
contentCache.set(filename, content);
resolve(content);
});
} else if (contentType === 'text/css') {
return await new Promise((resolve) => {
threadsPool.queue(async ({compressCSS}) => {
return await new Promise(async (resolve) => {
try {
logger.info('Compress CSS file %s.', filename);
try {
logger.info('Compress CSS file %s.', filename);
const compressResult = await compressCSS(path.resolve(ROOT_DIR,filename));
if (compressResult.error) {
console.error(`Error compressing CSS (${filename}) using terser`, compressResult.error);
} else {
content = compressResult
}
content = await compressCSS(path.resolve(ROOT_DIR, filename));
} catch (error) {
console.error(`CleanCSS.minify() returned an error on ${filename}: ${error}`);
}
contentCache.set(filename, content);
resolve(content);
});
});
} catch (e) {
console.error('getFile() returned an error in ' +
`getFileCompressed(${filename}, ${contentType}): ${e}`);
}
})
} else {
contentCache.set(filename, content);
return content;
}
};
const getFile = async (filename) => {
const getFile = async (filename: any) => {
return await fs.readFile(path.resolve(ROOT_DIR, filename));
};
exports.minify = (req, res, next) => minify(req, res).catch((err) => next(err || new Error(err)));
export const minify = (req:any, res:any, next:Function) => _minify(req, res).catch((err) => next(err || new Error(err)));
exports.requestURIs = requestURIs;
export const requestURIs = _requestURIs;
exports.shutdown = async (hookName, context) => {
await threadsPool.terminate();
export const shutdown = async (hookName: string, context:any) => {
contentCache = new Map();
};

View file

@ -3,14 +3,13 @@
* Worker thread to minify JS & CSS files out of the main NodeJS thread
*/
import {expose} from 'threads'
import {build, transform} from 'esbuild';
/*
* Minify JS content
* @param {string} content - JS content to minify
*/
const compressJS = async (content) => {
export const compressJS = async (content: string) => {
return await transform(content, {minify: true});
}
@ -19,7 +18,7 @@ const compressJS = async (content) => {
* @param {string} filename - name of the file
* @param {string} ROOT_DIR - the root dir of Etherpad
*/
const compressCSS = async (content) => {
export const compressCSS = async (content: string) => {
const transformedCSS = await build(
{
entryPoints: [content],
@ -41,8 +40,3 @@ const compressCSS = async (content) => {
)
return transformedCSS.outputFiles[0].text
};
expose({
compressJS: compressJS,
compressCSS,
});

View file

@ -1,211 +0,0 @@
'use strict';
/*
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* 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 {Buffer} from 'node:buffer'
import fs from 'fs';
const fsp = fs.promises;
import path from 'path';
import zlib from 'zlib';
const settings = require('./Settings');
const existsSync = require('./path_exists');
import util from 'util';
/*
* The crypto module can be absent on reduced node installations.
*
* Here we copy the approach TypeScript guys used for https://github.com/microsoft/TypeScript/issues/19100
* If importing crypto fails at runtime, we replace sha256 with djb2, which is
* weaker, but works for our case.
*
* djb2 was written in 1991 by Daniel J. Bernstein.
*
*/
import _crypto from 'crypto';
let CACHE_DIR: string|undefined = path.join(settings.root, 'var/');
CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined;
type Headers = {
[id: string]: string
}
type ResponseCache = {
[id: string]: {
statusCode: number
headers: Headers
}
}
const responseCache: ResponseCache = {};
const djb2Hash = (data: string) => {
const chars = data.split('').map((str) => str.charCodeAt(0));
return `${chars.reduce((prev, curr) => ((prev << 5) + prev) + curr, 5381)}`;
};
const generateCacheKeyWithSha256 =
(path: string) => _crypto.createHash('sha256').update(path).digest('hex');
const generateCacheKeyWithDjb2 =
(path: string) => Buffer.from(djb2Hash(path)).toString('hex');
let generateCacheKey: (path: string)=>string;
if (_crypto) {
generateCacheKey = generateCacheKeyWithSha256;
} else {
generateCacheKey = generateCacheKeyWithDjb2;
console.warn('No crypto support in this nodejs runtime. Djb2 (weaker) will be used.');
}
// MIMIC https://github.com/microsoft/TypeScript/commit/9677b0641cc5ba7d8b701b4f892ed7e54ceaee9a - END
/*
This caches and compresses 200 and 404 responses to GET and HEAD requests.
TODO: Caching and compressing are solved problems, a middleware configuration
should replace this.
*/
export default class CachingMiddleware {
handle(req: any, res: any, next: any) {
this._handle(req, res, next).catch((err) => next(err || new Error(err)));
}
async _handle(req: any, res: any, next: any) {
if (!(req.method === 'GET' || req.method === 'HEAD') || !CACHE_DIR) {
return next(undefined, req, res);
}
const oldReq:ResponseCache = {};
const oldRes:ResponseCache = {};
const supportsGzip =
(req.get('Accept-Encoding') || '').indexOf('gzip') !== -1;
const url = new URL(req.url, 'http://localhost');
const cacheKey = generateCacheKey(url.pathname + url.search);
const stats = await fsp.stat(`${CACHE_DIR}minified_${cacheKey}`).catch(() => {});
const modifiedSince =
req.headers['if-modified-since'] && new Date(req.headers['if-modified-since']);
if (stats != null && stats.mtime && responseCache[cacheKey]) {
req.headers['if-modified-since'] = stats.mtime.toUTCString();
} else {
delete req.headers['if-modified-since'];
}
// Always issue get to downstream.
oldReq.method = req.method;
req.method = 'GET';
// This handles read/write synchronization as well as its predecessor,
// which is to say, not at all.
// TODO: Implement locking on write or ditch caching of gzip and use
// existing middlewares.
const respond = () => {
req.method = oldReq.method || req.method;
res.write = oldRes.write || res.write;
res.end = oldRes.end || res.end;
const headers: Headers = {};
Object.assign(headers, (responseCache[cacheKey].headers || {}));
const statusCode = responseCache[cacheKey].statusCode;
let pathStr = `${CACHE_DIR}minified_${cacheKey}`;
if (supportsGzip && /application\/javascript/.test(headers['content-type'])) {
pathStr += '.gz';
headers['content-encoding'] = 'gzip';
}
const lastModified = headers['last-modified'] && new Date(headers['last-modified']);
if (statusCode === 200 && lastModified <= modifiedSince) {
res.writeHead(304, headers);
res.end();
} else if (req.method === 'GET') {
const readStream = fs.createReadStream(pathStr);
res.writeHead(statusCode, headers);
readStream.pipe(res);
} else {
res.writeHead(statusCode, headers);
res.end();
}
};
const expirationDate = new Date(((responseCache[cacheKey] || {}).headers || {}).expires);
if (expirationDate > new Date()) {
// Our cached version is still valid.
return respond();
}
const _headers:Headers = {};
oldRes.setHeader = res.setHeader;
res.setHeader = (key: string, value: string) => {
// Don't set cookies, see issue #707
if (key.toLowerCase() === 'set-cookie') return;
_headers[key.toLowerCase()] = value;
// @ts-ignore
oldRes.setHeader.call(res, key, value);
};
oldRes.writeHead = res.writeHead;
res.writeHead = (status: number, headers: Headers) => {
res.writeHead = oldRes.writeHead;
if (status === 200) {
// Update cache
let buffer = '';
Object.keys(headers || {}).forEach((key) => {
res.setHeader(key, headers[key]);
});
headers = _headers;
oldRes.write = res.write;
oldRes.end = res.end;
res.write = (data: number, encoding: number) => {
buffer += data.toString(encoding);
};
res.end = async (data: number, encoding: number) => {
await Promise.all([
fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}`, buffer).catch(() => {}),
util.promisify(zlib.gzip)(buffer)
// @ts-ignore
.then((content: string) => fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}.gz`, content))
.catch(() => {}),
]);
responseCache[cacheKey] = {statusCode: status, headers};
respond();
};
} else if (status === 304) {
// Nothing new changed from the cached version.
oldRes.write = res.write;
oldRes.end = res.end;
res.write = (data: number, encoding: number) => {};
res.end = (data: number, encoding: number) => { respond(); };
} else {
res.writeHead(status, headers);
}
};
next(undefined, req, res);
}
};

View file

@ -1,10 +1,8 @@
'use strict';
const path = require('path');
import path from 'path';
// Normalizes p and ensures that it is a relative path that does not reach outside. See
// https://nvd.nist.gov/vuln/detail/CVE-2015-3297 for additional context.
module.exports = (p: string, pathApi = path) => {
const sanitizeRoot = (p: string, pathApi = path) => {
// The documentation for path.normalize() says that it resolves '..' and '.' segments. The word
// "resolve" implies that it examines the filesystem to resolve symbolic links, so 'a/../b' might
// not be the same thing as 'b'. Most path normalization functions from other libraries (e.g.,
@ -21,3 +19,5 @@ module.exports = (p: string, pathApi = path) => {
if (pathApi.sep === '\\') p = p.replace(/\\/g, '/');
return p;
};
export default sanitizeRoot

View file

@ -68,13 +68,13 @@
"socket.io": "^4.7.5",
"socket.io-client": "^4.7.5",
"superagent": "10.0.2",
"threads": "^1.7.0",
"tinycon": "0.6.8",
"tsx": "4.17.0",
"ueberdb2": "^4.2.93",
"underscore": "1.13.7",
"unorm": "1.6.0",
"wtfnode": "^0.9.3"
"wtfnode": "^0.9.3",
"vitest": "^2.0.5"
},
"bin": {
"etherpad-healthcheck": "../bin/etherpad-healthcheck",
@ -89,6 +89,7 @@
"@types/jquery": "^3.5.30",
"@types/jsdom": "^21.1.7",
"@types/jsonwebtoken": "^9.0.6",
"@types/mime-types": "^2.1.4",
"@types/mocha": "^10.0.7",
"@types/node": "^22.3.0",
"@types/oidc-provider": "^8.5.2",
@ -132,7 +133,8 @@
"test-ui:ui": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/specs --ui",
"test-admin": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/admin-spec --workers 1",
"test-admin:ui": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/admin-spec --ui --workers 1",
"debug:socketio": "cross-env DEBUG=socket.io* node --require tsx/cjs node/server.ts"
"debug:socketio": "cross-env DEBUG=socket.io* node --require tsx/cjs node/server.ts",
"test:vitest": "vitest"
},
"version": "2.2.2",
"license": "Apache-2.0"

View file

@ -1,8 +1,9 @@
'use strict';
const AttributeMap = require('../../../static/js/AttributeMap');
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 = [
@ -10,7 +11,7 @@ describe('AttributeMap', function () {
['baz', 'bif'],
['emptyValue', ''],
];
let pool;
let pool: { eachAttrib: (arg0: () => number) => void; putAttrib: (arg0: string[]) => any; getAttrib: (arg0: number) => any; };
const getPoolSize = () => {
let n = 0;
@ -66,7 +67,7 @@ describe('AttributeMap', function () {
['number', 1, '1'],
];
for (const [desc, input, want] of testCases) {
describe(desc, function () {
describe(desc as string, function () {
it('key is coerced to string', async function () {
const m = new AttributeMap(pool);
m.set(input, 'value');
@ -116,8 +117,9 @@ describe('AttributeMap', function () {
});
for (const funcName of ['update', 'updateFromString']) {
const callUpdateFn = (m, ...args) => {
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);

View file

@ -1,11 +1,15 @@
'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;
let pool: APool;
beforeEach(async function () {
pool = new AttributePool();
@ -14,7 +18,7 @@ describe('attributes', function () {
describe('decodeAttribString', function () {
it('is a generator function', async function () {
expect(attributes.decodeAttribString).to.be.a((function* () {}).constructor);
expect(attributes.decodeAttribString.constructor.name).to.equal('GeneratorFunction');
});
describe('rejects invalid attribute strings', function () {
@ -22,7 +26,7 @@ describe('attributes', function () {
for (const tc of testCases) {
it(JSON.stringify(tc), async function () {
expect(() => [...attributes.decodeAttribString(tc)])
.to.throwException(/invalid character/);
.toThrowError(/invalid character/);
});
}
});
@ -56,7 +60,7 @@ describe('attributes', function () {
['set', new Set([0, 1])],
];
for (const [desc, input] of testCases) {
it(desc, async function () {
it(desc as string, async function () {
expect(attributes.encodeAttribString(input)).to.equal('*0*1');
});
}
@ -74,7 +78,7 @@ describe('attributes', function () {
];
for (const [input, wantErr] of testCases) {
it(JSON.stringify(input), async function () {
expect(() => attributes.encodeAttribString(input)).to.throwException(wantErr);
expect(() => attributes.encodeAttribString(input)).toThrowError(wantErr as RegExp);
});
}
});
@ -101,7 +105,7 @@ describe('attributes', function () {
describe('attribsFromNums', function () {
it('is a generator function', async function () {
expect(attributes.attribsFromNums).to.be.a((function* () {}).constructor);
expect(attributes.attribsFromNums.constructor.name).to.equal("GeneratorFunction");
});
describe('accepts any kind of iterable', function () {
@ -112,7 +116,7 @@ describe('attributes', function () {
];
for (const [desc, input] of testCases) {
it(desc, async function () {
it(desc as string, async function () {
const gotAttribs = [...attributes.attribsFromNums(input, pool)];
expect(JSON.stringify(gotAttribs)).to.equal(JSON.stringify(attribs));
});
@ -132,7 +136,7 @@ describe('attributes', function () {
];
for (const [input, wantErr] of testCases) {
it(JSON.stringify(input), async function () {
expect(() => [...attributes.attribsFromNums(input, pool)]).to.throwException(wantErr);
expect(() => [...attributes.attribsFromNums(input, pool)]).toThrowError(wantErr as RegExp);
});
}
});
@ -156,7 +160,7 @@ describe('attributes', function () {
describe('attribsToNums', function () {
it('is a generator function', async function () {
expect(attributes.attribsToNums).to.be.a((function* () {}).constructor);
expect(attributes.attribsToNums.constructor.name).to.equal("GeneratorFunction")
});
describe('accepts any kind of iterable', function () {
@ -167,7 +171,7 @@ describe('attributes', function () {
];
for (const [desc, input] of testCases) {
it(desc, async function () {
it(desc as string, async function () {
const gotNums = [...attributes.attribsToNums(input, pool)];
expect(JSON.stringify(gotNums)).to.equal(JSON.stringify([0, 1]));
});
@ -178,7 +182,7 @@ describe('attributes', function () {
const testCases = [null, [null]];
for (const input of testCases) {
it(JSON.stringify(input), async function () {
expect(() => [...attributes.attribsToNums(input, pool)]).to.throwException();
expect(() => [...attributes.attribsToNums(input, pool)]).toThrowError();
});
}
});
@ -224,12 +228,12 @@ describe('attributes', function () {
['number', 1, '1'],
];
for (const [desc, inputVal, wantVal] of testCases) {
describe(desc, function () {
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, async function () {
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);
@ -245,7 +249,7 @@ describe('attributes', function () {
describe('attribsFromString', function () {
it('is a generator function', async function () {
expect(attributes.attribsFromString).to.be.a((function* () {}).constructor);
expect(attributes.attribsFromString.constructor.name).to.equal('GeneratorFunction');
});
describe('rejects invalid attribute strings', function () {
@ -261,7 +265,7 @@ describe('attributes', function () {
];
for (const [input, wantErr] of testCases) {
it(JSON.stringify(input), async function () {
expect(() => [...attributes.attribsFromString(input, pool)]).to.throwException(wantErr);
expect(() => [...attributes.attribsFromString(input, pool)]).toThrowError(wantErr);
});
}
});
@ -292,7 +296,7 @@ describe('attributes', function () {
];
for (const [desc, input] of testCases) {
it(desc, async function () {
it(desc as string, async function () {
const got = attributes.attribsToString(input, pool);
expect(got).to.equal('*0*1');
});
@ -303,7 +307,7 @@ describe('attributes', function () {
const testCases = [null, [null]];
for (const input of testCases) {
it(JSON.stringify(input), async function () {
expect(() => attributes.attribsToString(input, pool)).to.throwException();
expect(() => attributes.attribsToString(input, pool)).toThrowError();
});
}
});

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

@ -1,16 +1,13 @@
'use strict';
import {MapArrayType} from "../../../node/types/MapType";
import {strict as assert} from "assert";
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> = {};
before(async function () {
beforeAll(async function () {
backups.logger = warnDeprecated.logger;
});
@ -36,10 +33,10 @@ describe(__filename, function () {
for (const [now, want] of testCases) { // In a loop so that the stack trace is the same.
warnDeprecated._rl.now = () => now;
warnDeprecated();
assert.equal(got, want);
expect(got).toEqual(want);
}
warnDeprecated(); // Should have a different stack trace.
assert.equal(got, testCases[testCases.length - 1][1] + 1);
expect(got).toEqual(testCases[testCases.length - 1][1] + 1);
});
});
});

View file

@ -1,8 +1,7 @@
'use strict';
import {strict as assert} from "assert";
import path from 'path';
const sanitizePathname = require('../../../node/utils/sanitizePathname');
import sanitizePathname from '../../../node/utils/sanitizePathname';
import {describe, it, expect} from 'vitest';
describe(__filename, function () {
describe('absolute paths rejected', function () {
@ -21,7 +20,7 @@ describe(__filename, function () {
for (const [platform, p] of testCases) {
it(`${platform} ${p}`, async function () {
// @ts-ignore
assert.throws(() => sanitizePathname(p, path[platform]), {message: /absolute path/});
expect(() => sanitizePathname(p, path[platform] as any)).toThrowError(/absolute path/);
});
}
});

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]);
});
});

View file

@ -40,7 +40,7 @@ describe(__filename, function () {
it('do nothing', async function () {
await agent.get('/p/UPPERCASEpad')
.expect(200);
.expect(200);
});
});
@ -50,8 +50,8 @@ describe(__filename, function () {
});
it('lowercase pad ids', async function () {
await agent.get('/p/UPPERCASEpad')
.expect(302)
.expect('location', 'uppercasepad');
.expect(302)
.expect('location', 'uppercasepad');
});
it('keeps old pads accessible', async function () {

View file

@ -6,87 +6,87 @@ import path from 'path';
import process from 'process';
describe(__filename, function () {
describe('parseSettings', function () {
let settings: any;
const envVarSubstTestCases = [
{name: 'true', val: 'true', var: 'SET_VAR_TRUE', want: true},
{name: 'false', val: 'false', var: 'SET_VAR_FALSE', want: false},
{name: 'null', val: 'null', var: 'SET_VAR_NULL', want: null},
{name: 'undefined', val: 'undefined', var: 'SET_VAR_UNDEFINED', want: undefined},
{name: 'number', val: '123', var: 'SET_VAR_NUMBER', want: 123},
{name: 'string', val: 'foo', var: 'SET_VAR_STRING', want: 'foo'},
{name: 'empty string', val: '', var: 'SET_VAR_EMPTY_STRING', want: ''},
];
describe('parseSettings', function () {
let settings: any;
const envVarSubstTestCases = [
{name: 'true', val: 'true', var: 'SET_VAR_TRUE', want: true},
{name: 'false', val: 'false', var: 'SET_VAR_FALSE', want: false},
{name: 'null', val: 'null', var: 'SET_VAR_NULL', want: null},
{name: 'undefined', val: 'undefined', var: 'SET_VAR_UNDEFINED', want: undefined},
{name: 'number', val: '123', var: 'SET_VAR_NUMBER', want: 123},
{name: 'string', val: 'foo', var: 'SET_VAR_STRING', want: 'foo'},
{name: 'empty string', val: '', var: 'SET_VAR_EMPTY_STRING', want: ''},
];
before(async function () {
for (const tc of envVarSubstTestCases) process.env[tc.var] = tc.val;
delete process.env.UNSET_VAR;
settings = parseSettings(path.join(__dirname, 'settings.json'), true);
assert(settings != null);
});
describe('environment variable substitution', function () {
describe('set', function () {
for (const tc of envVarSubstTestCases) {
it(tc.name, async function () {
const obj = settings['environment variable substitution'].set;
if (tc.name === 'undefined') {
assert(!(tc.name in obj));
} else {
assert.equal(obj[tc.name], tc.want);
}
});
}
});
describe('unset', function () {
it('no default', async function () {
const obj = settings['environment variable substitution'].unset;
assert.equal(obj['no default'], null);
});
for (const tc of envVarSubstTestCases) {
it(tc.name, async function () {
const obj = settings['environment variable substitution'].unset;
if (tc.name === 'undefined') {
assert(!(tc.name in obj));
} else {
assert.equal(obj[tc.name], tc.want);
}
});
}
});
});
before(async function () {
for (const tc of envVarSubstTestCases) process.env[tc.var] = tc.val;
delete process.env.UNSET_VAR;
settings = parseSettings(path.join(__dirname, 'settings.json'), true);
assert(settings != null);
});
describe('environment variable substitution', function () {
describe('set', function () {
for (const tc of envVarSubstTestCases) {
it(tc.name, async function () {
const obj = settings['environment variable substitution'].set;
if (tc.name === 'undefined') {
assert(!(tc.name in obj));
} else {
assert.equal(obj[tc.name], tc.want);
}
});
}
});
describe("Parse plugin settings", function () {
describe('unset', function () {
it('no default', async function () {
const obj = settings['environment variable substitution'].unset;
assert.equal(obj['no default'], null);
});
before(async function () {
process.env["EP__ADMIN__PASSWORD"] = "test"
})
for (const tc of envVarSubstTestCases) {
it(tc.name, async function () {
const obj = settings['environment variable substitution'].unset;
if (tc.name === 'undefined') {
assert(!(tc.name in obj));
} else {
assert.equal(obj[tc.name], tc.want);
}
});
}
});
});
});
it('should parse plugin settings', async function () {
let settings = parseSettings(path.join(__dirname, 'settings.json'), true);
assert.equal(settings.ADMIN.PASSWORD, "test");
})
it('should bundle settings with same path', async function () {
process.env["EP__ADMIN__USERNAME"] = "test"
let settings = parseSettings(path.join(__dirname, 'settings.json'), true);
assert.deepEqual(settings.ADMIN, {PASSWORD: "test", USERNAME: "test"});
})
describe("Parse plugin settings", function () {
it("Can set the ep themes", async function () {
process.env["EP__ep_themes__default_theme"] = "hacker"
let settings = parseSettings(path.join(__dirname, 'settings.json'), true);
assert.deepEqual(settings.ep_themes, {"default_theme": "hacker"});
})
it("can set the ep_webrtc settings", async function () {
process.env["EP__ep_webrtc__enabled"] = "true"
let settings = parseSettings(path.join(__dirname, 'settings.json'), true);
assert.deepEqual(settings.ep_webrtc, {"enabled": true});
})
before(async function () {
process.env["EP__ADMIN__PASSWORD"] = "test"
})
it('should parse plugin settings', async function () {
let settings = parseSettings(path.join(__dirname, 'settings.json'), true);
assert.equal(settings.ADMIN.PASSWORD, "test");
})
it('should bundle settings with same path', async function () {
process.env["EP__ADMIN__USERNAME"] = "test"
let settings = parseSettings(path.join(__dirname, 'settings.json'), true);
assert.deepEqual(settings.ADMIN, {PASSWORD: "test", USERNAME: "test"});
})
it("Can set the ep themes", async function () {
process.env["EP__ep_themes__default_theme"] = "hacker"
let settings = parseSettings(path.join(__dirname, 'settings.json'), true);
assert.deepEqual(settings.ep_themes, {"default_theme": "hacker"});
})
it("can set the ep_webrtc settings", async function () {
process.env["EP__ep_webrtc__enabled"] = "true"
let settings = parseSettings(path.join(__dirname, 'settings.json'), true);
assert.deepEqual(settings.ep_webrtc, {"enabled": true});
})
})
});

View file

@ -4,11 +4,19 @@ const Changeset = require('../../../static/js/Changeset');
const {padutils} = require('../../../static/js/pad_utils');
const {poolOrArray} = require('../easysync-helper.js');
import {describe, it, expect} from 'vitest'
describe('easysync-assembler', function () {
it('opAssembler', async function () {
const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1';
const assem = Changeset.opAssembler();
for (const op of Changeset.deserializeOps(x)) assem.append(op);
var opLength = 0
for (const op of Changeset.deserializeOps(x)){
console.log(op)
assem.append(op);
opLength++
}
expect(assem.toString()).to.equal(x);
});
@ -145,14 +153,23 @@ describe('easysync-assembler', function () {
next() { const v = this._n.value; this._n = ops.next(); return v; },
};
const assem = Changeset.smartOpAssembler();
assem.append(iter.next());
assem.append(iter.next());
assem.append(iter.next());
var iter1 = iter.next()
assem.append(iter1);
var iter2 = iter.next()
assem.append(iter2);
var iter3 = iter.next()
assem.append(iter3);
console.log(assem.toString());
assem.clear();
assem.append(iter.next());
assem.append(iter.next());
console.log(assem.toString());
assem.clear();
while (iter.hasNext()) assem.append(iter.next());
let counter = 0;
while (iter.hasNext()) {
console.log(counter++)
assem.append(iter.next());
}
assem.endDocument();
expect(assem.toString()).to.equal('-1+1*0+1=1-1+1|c=c-1');
});

View file

@ -4,6 +4,8 @@ const Changeset = require('../../../static/js/Changeset');
const AttributePool = require('../../../static/js/AttributePool');
const {randomMultiline, poolOrArray} = require('../easysync-helper.js');
const {padutils} = require('../../../static/js/pad_utils');
import {describe, it, expect} from 'vitest'
describe('easysync-other', function () {
describe('filter attribute numbers', function () {
@ -66,7 +68,8 @@ describe('easysync-other', function () {
it('testMakeSplice', async function () {
const t = 'a\nb\nc\n';
const t2 = Changeset.applyToText(Changeset.makeSplice(t, 5, 0, 'def'), t);
let splice = Changeset.makeSplice(t, 5, 0, 'def')
const t2 = Changeset.applyToText(splice, t);
expect(t2).to.equal('a\nb\ncdef\n');
});

View file

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

7
src/vitest.config.ts Normal file
View file

@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ["tests/backend-new/**/*.ts"],
},
})