429 lines
10 KiB
JavaScript
429 lines
10 KiB
JavaScript
const { assert } = require('chai');
|
|
const getLoader = require('../dependencies');
|
|
const components = require('../components.json');
|
|
|
|
|
|
describe('Dependency logic', function () {
|
|
|
|
/** @type {import("../dependencies").Components} */
|
|
const components = {
|
|
languages: {
|
|
'a': {
|
|
alias: 'a2'
|
|
},
|
|
'b': {
|
|
alias: 'b2'
|
|
},
|
|
'c': {
|
|
require: 'a',
|
|
optional: ['b', 'e']
|
|
},
|
|
'd': {
|
|
require: ['c', 'b'],
|
|
alias: 'xyz'
|
|
},
|
|
},
|
|
pluginsOrSomething: {
|
|
'e': {
|
|
modify: 'a'
|
|
},
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns the ids of `getLoader`.
|
|
*
|
|
* @param {string[]} load
|
|
* @param {string[]} [loaded]
|
|
* @returns {string[]}
|
|
*/
|
|
function getIds(load, loaded) {
|
|
return getLoader(components, load, loaded).getIds();
|
|
}
|
|
|
|
describe('Returned ids', function () {
|
|
|
|
it('- should load requirements', function () {
|
|
assert.sameMembers(getIds(['d']), ['a', 'b', 'c', 'd']);
|
|
});
|
|
|
|
it('- should not load already loaded requirements if not necessary', function () {
|
|
assert.sameMembers(getIds(['d'], ['a', 'b']), ['c', 'd']);
|
|
});
|
|
|
|
it('- should load already loaded requirements if requested', function () {
|
|
assert.sameMembers(getIds(['a', 'd'], ['a', 'b']), ['a', 'c', 'd']);
|
|
});
|
|
|
|
it('- should reload modified components', function () {
|
|
assert.sameMembers(getIds(['e'], ['a', 'b', 'c', 'd']), ['a', 'c', 'd', 'e']);
|
|
});
|
|
|
|
it('- should work with empty load', function () {
|
|
assert.sameMembers(getIds([], ['a', 'b', 'c', 'd']), []);
|
|
});
|
|
|
|
it('- should return unknown ids as is', function () {
|
|
assert.sameMembers(getIds(['c', 'foo'], ['bar']), ['foo', 'c', 'a']);
|
|
});
|
|
|
|
it('- should throw on unknown dependencies', function () {
|
|
assert.throws(() => {
|
|
/** @type {import("../dependencies").Components} */
|
|
const circular = {
|
|
languages: {
|
|
a: {
|
|
require: 'c'
|
|
},
|
|
b: 'B'
|
|
}
|
|
};
|
|
getLoader(circular, ['a']).getIds();
|
|
});
|
|
});
|
|
|
|
});
|
|
|
|
describe('Load order', function () {
|
|
|
|
// Note: The order of a and b isn't defined, so don't add any test with both of them being loaded here
|
|
|
|
it('- should load components in the correct order (require)', function () {
|
|
assert.deepStrictEqual(getIds(['c']), ['a', 'c']);
|
|
});
|
|
|
|
it('- should load components in the correct order (modify)', function () {
|
|
assert.deepStrictEqual(getIds(['e', 'a']), ['a', 'e']);
|
|
});
|
|
|
|
it('- should load components in the correct order (optional)', function () {
|
|
assert.deepStrictEqual(getIds(['c', 'b'], ['a']), ['b', 'c']);
|
|
});
|
|
|
|
it('- should load components in the correct order (require + optional)', function () {
|
|
assert.deepStrictEqual(getIds(['d'], ['a']), ['b', 'c', 'd']);
|
|
});
|
|
|
|
it('- should load components in the correct order (require + modify + optional)', function () {
|
|
assert.deepStrictEqual(getIds(['d', 'e'], ['b']), ['a', 'e', 'c', 'd']);
|
|
});
|
|
|
|
});
|
|
|
|
describe('Aliases', function () {
|
|
|
|
it('- should resolve aliases in the list of components to load', function () {
|
|
assert.sameMembers(getIds(['xyz']), ['a', 'b', 'c', 'd']);
|
|
});
|
|
|
|
it('- should resolve aliases in the list of loaded components', function () {
|
|
assert.sameMembers(getIds(['d'], ['a', 'a2', 'b2']), ['c', 'd']);
|
|
});
|
|
|
|
it('- should throw on duplicate aliases', function () {
|
|
assert.throws(() => {
|
|
/** @type {import("../dependencies").Components} */
|
|
const circular = {
|
|
languages: {
|
|
a: {
|
|
alias: 'c'
|
|
},
|
|
b: {
|
|
alias: 'c'
|
|
}
|
|
}
|
|
};
|
|
getLoader(circular, ['a', 'foo' /* force the lazy alias resolver */]).getIds();
|
|
});
|
|
});
|
|
|
|
it('- should throw on aliases which are components', function () {
|
|
assert.throws(() => {
|
|
/** @type {import("../dependencies").Components} */
|
|
const circular = {
|
|
languages: {
|
|
a: {
|
|
alias: 'b'
|
|
},
|
|
b: 'B'
|
|
}
|
|
};
|
|
getLoader(circular, ['a', 'foo' /* force the lazy alias resolver */]).getIds();
|
|
});
|
|
});
|
|
|
|
});
|
|
|
|
describe('Circular dependencies', function () {
|
|
|
|
it('- should throw on circular dependencies', function () {
|
|
assert.throws(() => {
|
|
/** @type {import("../dependencies").Components} */
|
|
const circular = {
|
|
languages: {
|
|
a: {
|
|
require: 'b'
|
|
},
|
|
b: {
|
|
optional: 'a'
|
|
}
|
|
}
|
|
};
|
|
getLoader(circular, ['a']).getIds();
|
|
});
|
|
});
|
|
|
|
});
|
|
|
|
describe('Async loading', function () {
|
|
|
|
it('- should load components in the correct order', async function () {
|
|
|
|
/** @type {import("../dependencies").Components} */
|
|
const localComponents = {
|
|
languages: {
|
|
'a': {},
|
|
'b': {
|
|
require: 'a'
|
|
},
|
|
'c': {
|
|
require: 'b'
|
|
}
|
|
}
|
|
};
|
|
|
|
/** @type {string[]} */
|
|
const actualLoadOrder = [];
|
|
/** @type {string[]} */
|
|
const actualResolveOrder = [];
|
|
|
|
/**
|
|
*
|
|
* @param {string} id
|
|
* @returns {Promise<void>}
|
|
*/
|
|
function loadComp(id) {
|
|
actualLoadOrder.push(id);
|
|
|
|
// the idea is that the components which have to be loaded first, take the longest, so if all were to
|
|
// start getting loaded at the same time, their order would be the reverse of the expected order.
|
|
let delay;
|
|
if (id === 'a') {
|
|
delay = 30;
|
|
} else if (id === 'b') {
|
|
delay = 20;
|
|
} else if (id === 'c') {
|
|
delay = 10;
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
setTimeout(() => {
|
|
actualResolveOrder.push(id);
|
|
resolve();
|
|
}, delay);
|
|
});
|
|
}
|
|
|
|
const loader = getLoader(localComponents, ['c']);
|
|
|
|
await loader.load(id => loadComp(id), {
|
|
series: async (before, after) => {
|
|
await before;
|
|
await after();
|
|
},
|
|
parallel: async (values) => {
|
|
await Promise.all(values);
|
|
}
|
|
});
|
|
|
|
assert.deepStrictEqual(actualLoadOrder, ['a', 'b', 'c'], 'actualLoadOrder:');
|
|
assert.deepStrictEqual(actualResolveOrder, ['a', 'b', 'c'], 'actualResolveOrder:');
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
describe('components.json', function () {
|
|
|
|
/**
|
|
* @param {T | T[] | undefined | null} value
|
|
* @returns {T[]}
|
|
* @template T
|
|
*/
|
|
function toArray(value) {
|
|
if (Array.isArray(value)) {
|
|
return value;
|
|
} else if (value == undefined) {
|
|
return [];
|
|
} else {
|
|
return [value];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {(entry: import("../dependencies").ComponentEntry, id: string, entries: Object<string, import("../dependencies").ComponentEntry>) => void} consumeFn
|
|
*/
|
|
function forEachEntry(consumeFn) {
|
|
/** @type {Object<string, import("../dependencies").ComponentEntry>} */
|
|
const entries = {};
|
|
|
|
for (const category in components) {
|
|
for (const id in components[category]) {
|
|
const entry = components[category][id];
|
|
if (id !== 'meta' && entry && typeof entry === 'object') {
|
|
entries[id] = entry;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const id in entries) {
|
|
consumeFn(entries[id], id, entries);
|
|
}
|
|
}
|
|
|
|
const entryProperties = [
|
|
'title',
|
|
'description',
|
|
'alias',
|
|
'aliasTitles',
|
|
'owner',
|
|
|
|
'require',
|
|
'optional',
|
|
'modify',
|
|
|
|
'noCSS',
|
|
'option'
|
|
];
|
|
|
|
it('- should be valid', function () {
|
|
try {
|
|
const allIds = [];
|
|
for (const category in components) {
|
|
Object.keys(components[category]).forEach(id => allIds.push(id));
|
|
}
|
|
// and an alias, so we force the lazy alias resolver to check all aliases
|
|
allIds.push('js');
|
|
|
|
getLoader(components, allIds).getIds();
|
|
} catch (error) {
|
|
assert.fail(error.toString());
|
|
}
|
|
});
|
|
|
|
it('- should not have redundant optional dependencies', function () {
|
|
forEachEntry((entry, id) => {
|
|
const optional = new Set(toArray(entry.optional));
|
|
|
|
for (const modifyId of toArray(entry.modify)) {
|
|
if (optional.has(modifyId)) {
|
|
assert.fail(`The component "${id}" has declared "${modifyId}" as both optional and modify.`);
|
|
}
|
|
}
|
|
for (const requireId of toArray(entry.require)) {
|
|
if (optional.has(requireId)) {
|
|
assert.fail(`The component "${id}" has declared "${requireId}" as both optional and require.`);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
it('- should have a sorted language list', function () {
|
|
const ignore = new Set(['meta', 'markup', 'css', 'clike', 'javascript']);
|
|
/** @type {{ id: string, title: string }[]} */
|
|
const languages = Object.keys(components.languages).filter(key => !ignore.has(key)).map(key => {
|
|
return {
|
|
id: key,
|
|
title: components.languages[key].title
|
|
};
|
|
});
|
|
|
|
/**
|
|
* Transforms the given title into an intermediate representation to allowed for sensible comparisons
|
|
* between titles.
|
|
*
|
|
* @param {string} title
|
|
*/
|
|
function transformTitle(title) {
|
|
return title.replace(/\W+/g, '').replace(/^\d+/, '').toLowerCase();
|
|
}
|
|
|
|
const sorted = [...languages].sort((a, b) => {
|
|
const comp = transformTitle(a.title).localeCompare(transformTitle(b.title));
|
|
if (comp !== 0) {
|
|
return comp;
|
|
}
|
|
// a and b have the same intermediate form (e.g. "C" => "C", "C++" => "C", "C#" => "C").
|
|
return a.title.toLowerCase().localeCompare(b.title.toLowerCase());
|
|
});
|
|
|
|
assert.sameOrderedMembers(languages, sorted);
|
|
});
|
|
|
|
it('- should not have single-element or empty arrays', function () {
|
|
/** @type {keyof import("../dependencies").ComponentEntry} */
|
|
const properties = ['alias', 'optional', 'require', 'modify'];
|
|
|
|
forEachEntry((entry, id) => {
|
|
for (const prop of properties) {
|
|
const value = entry[prop];
|
|
if (Array.isArray(value)) {
|
|
if (value.length === 0) {
|
|
assert.fail(
|
|
`The component "${id}" defines an empty array for "${prop}".` +
|
|
` Please remove the "${prop}" property.`
|
|
);
|
|
} else if (value.length === 1) {
|
|
assert.fail(
|
|
`The component "${id}" defines a single-empty array for "${prop}".` +
|
|
` Please replace the array with its element.` +
|
|
`\n\t${JSON.stringify(prop)}: ${JSON.stringify(value[0])}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
it('- should only have alias titles for valid aliases', function () {
|
|
forEachEntry((entry, id) => {
|
|
const title = entry.title;
|
|
const alias = toArray(entry.alias);
|
|
const aliasTitles = entry.aliasTitles;
|
|
|
|
for (const key in aliasTitles) {
|
|
if (alias.indexOf(key) === -1) {
|
|
assert.fail(
|
|
`Component "${id}":` +
|
|
` The alias ${JSON.stringify(key)} in "aliasTitles" is not defined in "alias".`
|
|
);
|
|
}
|
|
if (aliasTitles[key] === title) {
|
|
assert.fail(
|
|
`Component "${id}":` +
|
|
` The alias title for ${JSON.stringify(key)} is the same as the normal title.` +
|
|
` Remove the alias title or choose a different alias title.`
|
|
);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
it('- should not have unknown properties', function () {
|
|
const knownProperties = new Set(entryProperties);
|
|
|
|
forEachEntry((entry, id) => {
|
|
for (const prop in entry) {
|
|
if (!knownProperties.has(prop)) {
|
|
assert.fail(
|
|
`Component "${id}":` +
|
|
` The property ${JSON.stringify(prop)} is not supported by Prism.`
|
|
);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
});
|