mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-01-19 06:03:34 +01:00
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:
parent
babfaab4df
commit
c7a2dea4d1
21 changed files with 1092 additions and 552 deletions
18
.github/workflows/backend-tests.yml
vendored
18
.github/workflows/backend-tests.yml
vendored
|
@ -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
|
||||
|
|
468
pnpm-lock.yaml
468
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
|
|
|
@ -35,6 +35,7 @@ export type APool = {
|
|||
clone: ()=>APool,
|
||||
check: ()=>Promise<void>,
|
||||
eachAttrib: (callback: (key: string, value: any)=>void)=>void,
|
||||
getAttrib: (key: number)=>any,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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();
|
||||
};
|
|
@ -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,
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
});
|
398
src/tests/backend-new/specs/contentcollector.ts
Normal file
398
src/tests/backend-new/specs/contentcollector.ts
Normal 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 with more than one space.<br></body></html>',
|
||||
wantAlines: ['+10'],
|
||||
wantText: ['Text with more than one space.'],
|
||||
},
|
||||
{
|
||||
description: 'Multiple nbsp should be preserved',
|
||||
html: '<html><body> <br></body></html>',
|
||||
wantAlines: ['+2'],
|
||||
wantText: [' '],
|
||||
},
|
||||
{
|
||||
description: 'Multiple nbsp between words ',
|
||||
html: '<html><body> word1 word2 word3<br></body></html>',
|
||||
wantAlines: ['+m'],
|
||||
wantText: [' word1 word2 word3'],
|
||||
},
|
||||
{
|
||||
description: 'A non-breaking space preceded by a normal space',
|
||||
html: '<html><body> word1 word2 word3<br></body></html>',
|
||||
wantAlines: ['+l'],
|
||||
wantText: [' word1 word2 word3'],
|
||||
},
|
||||
{
|
||||
description: 'A non-breaking space followed by a normal space',
|
||||
html: '<html><body> word1 word2 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> 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> 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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/);
|
||||
});
|
||||
}
|
||||
});
|
55
src/tests/backend-new/specs/skiplist.ts
Normal file
55
src/tests/backend-new/specs/skiplist.ts
Normal 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]);
|
||||
});
|
||||
});
|
|
@ -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 () {
|
||||
|
|
|
@ -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});
|
||||
})
|
||||
})
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
|
|
@ -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
7
src/vitest.config.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ["tests/backend-new/**/*.ts"],
|
||||
},
|
||||
})
|
Loading…
Reference in a new issue