Files

780 lines
25 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const {test} = require('node:test');
const assert = require('node:assert');
const _makeTestStorage = require('../fixtures/make-test-storage');
const Runtime = require('../../src/engine/runtime');
const VirtualMachine = require('../../src/virtual-machine');
const JSZip = require('@turbowarp/jszip');
const emptyProjectFixture = path.join(__dirname, '..', 'fixtures', 'tw-empty-project.sb3');
const makeTestStorage = () => {
const storage = _makeTestStorage();
storage.DataFormat.TTF = 'ttf';
storage.AssetType.Font = {
contentType: 'font/ttf',
name: 'Font',
runtimeFormat: storage.DataFormat.TTF,
immutable: true
};
return storage;
};
test('isValidSystemFont', () => {
const {fontManager} = new Runtime();
assert.ok(fontManager.isValidSystemFont('Roboto'));
assert.ok(fontManager.isValidSystemFont('sans-serif'));
assert.ok(fontManager.isValidSystemFont('helvetica neue'));
assert.ok(!fontManager.isValidSystemFont('Roboto;Bold'));
assert.ok(!fontManager.isValidSystemFont('Arial, sans-serif'));
fontManager.restrictFont('Roboto');
assert.ok(fontManager.isValidSystemFont('Roboto'));
});
test('isValidCustomFont', () => {
const {fontManager} = new Runtime();
assert.ok(fontManager.isValidCustomFont('Roboto'));
assert.ok(fontManager.isValidCustomFont('sans-serif'));
assert.ok(fontManager.isValidCustomFont('helvetica neue'));
assert.ok(!fontManager.isValidCustomFont('Roboto;Bold'));
assert.ok(!fontManager.isValidCustomFont('Arial, sans-serif'));
fontManager.restrictFont('Roboto');
assert.ok(!fontManager.isValidCustomFont('Roboto'));
assert.ok(!fontManager.isValidCustomFont('roboto'));
assert.ok(!fontManager.isValidCustomFont('ROBOTO'));
assert.ok(fontManager.isValidCustomFont('Roboto '));
assert.ok(fontManager.isValidCustomFont('Roboto2'));
assert.ok(fontManager.isValidCustomFont('sans-serif'));
assert.ok(fontManager.isValidCustomFont('helvetica neue'));
assert.ok(!fontManager.isValidCustomFont('Roboto;Bold'));
assert.ok(!fontManager.isValidCustomFont('Arial, sans-serif'));
});
test('getSafeSystemFont', () => {
const {fontManager} = new Runtime();
assert.strictEqual(fontManager.getUnusedSystemFont('Arial'), 'Arial');
fontManager.addSystemFont('Arial', 'sans-serif');
assert.strictEqual(fontManager.getUnusedSystemFont('Arial'), 'Arial2');
assert.strictEqual(fontManager.getUnusedSystemFont('Weird123!@"<>?'), 'Weird123');
fontManager.restrictFont('Restricted');
assert.strictEqual(fontManager.getUnusedSystemFont('Restricted'), 'Restricted');
});
test('getSafeCustomFont', () => {
const {fontManager} = new Runtime();
assert.strictEqual(fontManager.getUnusedCustomFont('Arial'), 'Arial');
fontManager.addSystemFont('Arial', 'sans-serif');
assert.strictEqual(fontManager.getUnusedCustomFont('Arial'), 'Arial2');
assert.strictEqual(fontManager.getUnusedCustomFont('Weird123!@"<>?'), 'Weird123');
fontManager.restrictFont('Restricted');
assert.strictEqual(fontManager.getUnusedCustomFont('Restricted'), 'Restricted2');
assert.strictEqual(fontManager.getUnusedCustomFont('restricted'), 'restricted2');
assert.strictEqual(fontManager.getUnusedCustomFont(' restricted'), ' restricted');
fontManager.restrictFont('Restricted2');
assert.strictEqual(fontManager.getUnusedCustomFont('Restricted'), 'Restricted3');
assert.strictEqual(fontManager.getUnusedCustomFont('restricted'), 'restricted3');
fontManager.addSystemFont('Restricted3');
assert.strictEqual(fontManager.getUnusedCustomFont('Restricted'), 'Restricted4');
assert.strictEqual(fontManager.getUnusedCustomFont('restricted'), 'restricted4');
});
test('system font', () => {
const mockRenderer = {
setLayerGroupOrdering: () => {},
setCustomFonts: () => {
assert.fail('Should not call renderer.setCustomFonts()');
}
};
const rt = new Runtime();
rt.attachRenderer(mockRenderer);
const {fontManager} = rt;
let changed = false;
fontManager.on('change', () => {
changed = true;
});
fontManager.addSystemFont('Noto Sans Mono', 'monospace');
assert.ok(changed, 'addSystemFont() emits change');
assert.ok(fontManager.hasFont('Noto Sans Mono'), 'updated hasFont()');
assert.ok(fontManager.hasFont('noto sans mono'), 'updated hasFont() case insensitively');
assert.deepEqual(fontManager.getFonts(), [
{
system: true,
name: 'Noto Sans Mono',
family: '"Noto Sans Mono", monospace',
data: null,
format: null
}
]);
assert.deepEqual(fontManager.serializeJSON(), [
{
system: true,
family: 'Noto Sans Mono',
fallback: 'monospace'
}
]);
assert.deepEqual(fontManager.serializeAssets(), []);
changed = false;
fontManager.addSystemFont('Lobster', 'fantasy, sans-serif');
assert.ok(changed, 'addSystemFont() emits change');
assert.ok(fontManager.hasFont('Lobster'), 'updated hasFont()');
assert.deepEqual(fontManager.getFonts(), [
{
system: true,
name: 'Noto Sans Mono',
family: '"Noto Sans Mono", monospace',
data: null,
format: null
},
{
system: true,
name: 'Lobster',
family: '"Lobster", fantasy, sans-serif',
data: null,
format: null
}
]);
assert.deepEqual(fontManager.serializeJSON(), [
{
system: true,
family: 'Noto Sans Mono',
fallback: 'monospace'
},
{
system: true,
family: 'Lobster',
fallback: 'fantasy, sans-serif'
}
]);
assert.deepEqual(fontManager.serializeAssets(), []);
});
test('system font validation', () => {
const {fontManager} = new Runtime();
fontManager.restrictFont('Restricted');
assert.throws(() => {
fontManager.addCustomFont(';', 'monospace');
});
assert.throws(() => {
fontManager.addCustomFont('Restricted', 'monospace');
});
});
test('clear', () => {
const setCustomFontsCalls = [];
const mockRenderer = {
setLayerGroupOrdering: () => {},
setCustomFonts: fonts => {
setCustomFontsCalls.push(fonts);
}
};
const rt = new Runtime();
rt.attachStorage(makeTestStorage());
rt.attachRenderer(mockRenderer);
const {fontManager, storage} = rt;
fontManager.addSystemFont('Arial', 'sans-serif');
assert.strictEqual(fontManager.getFonts().length, 1);
let changed = false;
fontManager.on('change', () => {
changed = true;
});
fontManager.clear();
assert.ok(changed, 'clear() emits change');
assert.strictEqual(fontManager.getFonts().length, 0, 'removed font');
assert.deepEqual(setCustomFontsCalls, [], 'clear() does not call setCustomFonts() if only system fonts');
fontManager.addCustomFont('Wingdings', 'monospace', storage.createAsset(
storage.AssetType.Font,
'ttf',
new Uint8Array([11, 12, 13]),
null,
true
));
changed = false;
setCustomFontsCalls.length = 0;
fontManager.clear();
assert.ok(changed, 'clear() emits change');
assert.deepEqual(setCustomFontsCalls, [{}], 'clear() clears setCustomFonts()');
});
test('custom fonts', () => {
const setCustomFontsCalls = [];
const mockRenderer = {
setLayerGroupOrdering: () => {},
setCustomFonts: customFonts => {
setCustomFontsCalls.push(customFonts);
}
};
const rt = new Runtime();
rt.attachRenderer(mockRenderer);
rt.attachStorage(makeTestStorage());
const {fontManager, storage} = rt;
let changed = false;
fontManager.on('change', () => {
changed = true;
});
fontManager.addCustomFont('Arial', 'sans-serif', storage.createAsset(
storage.AssetType.Font,
storage.DataFormat.TTF,
new Uint8Array([1, 2, 3]),
null,
true
));
assert.ok(changed, 'addCustomFont() emits change');
assert.ok(fontManager.hasFont('Arial'), 'updated hasFont()');
assert.deepEqual(fontManager.getFonts(), [
{
system: false,
name: 'Arial',
family: '"Arial", sans-serif',
data: new Uint8Array([1, 2, 3]),
format: 'ttf'
}
]);
assert.deepEqual(fontManager.serializeJSON(), [
{
system: false,
family: 'Arial',
fallback: 'sans-serif',
md5ext: '5289df737df57326fcdd22597afb1fac.ttf'
}
]);
assert.deepEqual(setCustomFontsCalls, [
{
// eslint-disable-next-line max-len
'"Arial", sans-serif': '@font-face { font-family: "Arial"; src: url("data:font/ttf;base64,AQID"); }'
}
]);
const assets = fontManager.serializeAssets();
assert.strictEqual(assets.length, 1);
assert.deepEqual(assets[0].data, new Uint8Array([1, 2, 3]));
changed = false;
setCustomFontsCalls.length = 0;
const asset = storage.createAsset(
storage.AssetType.Font,
'woff2',
new Uint8Array([4, 5, 6]),
null,
true
);
fontManager.addCustomFont('Comic Sans MS', 'serif', asset);
assert.ok(changed, 'addCustomFont() emits change');
assert.ok(fontManager.hasFont('Comic Sans MS'), 'updated hasFont()');
assert.deepEqual(fontManager.getFonts(), [
{
system: false,
name: 'Arial',
family: '"Arial", sans-serif',
data: new Uint8Array([1, 2, 3]),
format: 'ttf'
},
{
system: false,
name: 'Comic Sans MS',
family: '"Comic Sans MS", serif',
data: new Uint8Array([4, 5, 6]),
format: 'woff2'
}
]);
assert.deepEqual(fontManager.serializeJSON(), [
{
system: false,
family: 'Arial',
fallback: 'sans-serif',
md5ext: '5289df737df57326fcdd22597afb1fac.ttf'
},
{
system: false,
family: 'Comic Sans MS',
fallback: 'serif',
md5ext: 'b4a3ba90641372b4e4eaa841a5a400ec.woff2'
}
]);
assert.deepEqual(setCustomFontsCalls, [
{
// eslint-disable-next-line max-len
'"Arial", sans-serif': '@font-face { font-family: "Arial"; src: url("data:font/ttf;base64,AQID"); }',
// eslint-disable-next-line max-len
'"Comic Sans MS", serif': '@font-face { font-family: "Comic Sans MS"; src: url("data:font/ttf;base64,BAUG"); }'
}
]);
const assets2 = fontManager.serializeAssets();
assert.strictEqual(assets2.length, 2);
assert.strictEqual(assets2[0], assets[0]);
assert.strictEqual(assets2[1], asset);
});
test('custom font validation', () => {
const rt = new Runtime();
rt.attachStorage(makeTestStorage());
const {fontManager, storage} = rt;
assert.throws(() => {
fontManager.addCustomFont('family;', 'sans-serif', storage.createAsset(
storage.DataFormat.Font,
storage.DataFormat.TTF,
new Uint8Array([1]),
null,
true
));
});
});
test('deleteFont', () => {
const rt = new Runtime();
rt.attachStorage(makeTestStorage());
const {fontManager, storage} = rt;
fontManager.addSystemFont('Liberation Mono', 'monospace');
fontManager.addCustomFont('Noto Sans Mono', 'monospace', storage.createAsset(
storage.AssetType.Font,
storage.DataFormat.TTF,
new Uint8Array([17, 18, 19]),
null,
true
));
assert.ok(fontManager.hasFont('Liberation Mono'), 'has font initially');
assert.ok(fontManager.hasFont('Noto Sans Mono'), 'has font initially');
let changed = false;
fontManager.on('change', () => {
changed = true;
});
const setCustomFontsCalls = [];
const mockRenderer = {
setLayerGroupOrdering: () => {},
setCustomFonts: customFonts => {
setCustomFontsCalls.push(customFonts);
}
};
rt.attachRenderer(mockRenderer);
fontManager.deleteFont(1);
assert.ok(changed, 'deleteFont() emits change');
assert.ok(fontManager.hasFont('Liberation Mono'), 'kept font');
assert.ok(!fontManager.hasFont('Noto Sans Mono'), 'deleted font');
assert.deepEqual(setCustomFontsCalls, [{}], 'called setCustomFonts() after deleting non-system font');
assert.deepEqual(fontManager.getFonts(), [
{
system: true,
name: 'Liberation Mono',
family: '"Liberation Mono", monospace',
data: null,
format: null
}
], 'updated getFonts() after deleting');
changed = false;
fontManager.deleteFont(0);
assert.ok(changed, 'deleteFont() emits change');
assert.deepEqual(setCustomFontsCalls, [{}], 'did not call setCustomFonts() again after deleting system font');
assert.deepEqual(fontManager.getFonts(), [], 'updated getFonts() after deleting');
});
test('fonts are serialized by VM', () => {
const vm = new VirtualMachine();
vm.attachStorage(makeTestStorage());
const {storage, fontManager} = vm.runtime;
fontManager.addSystemFont('DejaVu Sans', 'sans-serif');
const fontAsset = storage.createAsset(
storage.AssetType.Font,
storage.DataFormat.TTF,
new Uint8Array([10, 11, 12]),
null,
true
);
fontManager.addCustomFont('Noto Color Emoji', 'emoji', fontAsset);
const assets = vm.assets;
assert.deepEqual(assets, [fontAsset], 'font is in vm.assets');
const serializedAssets = vm.serializeAssets();
assert.deepEqual(serializedAssets, [
{
fileName: '94263e4d553bcec128704e354b659526.ttf',
fileContent: new Uint8Array([10, 11, 12])
}
], 'font is in vm.serializeAssets()');
const notZippedProject = vm.saveProjectSb3DontZip();
assert.strictEqual(
notZippedProject['94263e4d553bcec128704e354b659526.ttf'],
fontAsset.data,
'font is in saveProjectSb3DontZip()'
);
const projectJSON = JSON.parse(vm.toJSON());
assert.deepEqual(projectJSON.customFonts, [
{
system: true,
family: 'DejaVu Sans',
fallback: 'sans-serif'
},
{
system: false,
family: 'Noto Color Emoji',
fallback: 'emoji',
md5ext: '94263e4d553bcec128704e354b659526.ttf'
}
], 'font is in vm.toJSON()');
});
test('does not serialize fonts if there are none', () => {
const vm = new VirtualMachine();
const json = JSON.parse(vm.toJSON());
assert.ok(!('customFonts' in json));
});
test('serialization and deserialization roundtrip - project', async () => {
const originalVM = new VirtualMachine();
originalVM.attachStorage(makeTestStorage());
const {storage, fontManager} = originalVM.runtime;
await originalVM.loadProject(fs.readFileSync(emptyProjectFixture));
// Add our custom fonts here
fontManager.addSystemFont('Ubuntu Mono', 'monospace');
const fontAsset = storage.createAsset(
storage.AssetType.Font,
storage.DataFormat.TTF,
new Uint8Array([20, 21, 22, 23, 24]),
null,
true
);
fontManager.addCustomFont('Inter', 'sans-serif', fontAsset);
const projectSb3 = await originalVM.saveProjectSb3('arraybuffer');
const newVM = new VirtualMachine();
newVM.attachStorage(makeTestStorage());
const newFontManager = newVM.runtime.fontManager;
newFontManager.addSystemFont('ShouldBeRemoved', 'sans-serif');
let changed = false;
newFontManager.on('change', () => {
changed = true;
});
await newVM.loadProject(projectSb3);
assert.ok(changed, 'loadProject() emits change');
assert.deepEqual(newFontManager.getFonts(), [
{
system: true,
name: 'Ubuntu Mono',
family: '"Ubuntu Mono", monospace',
data: null,
format: null
},
{
system: false,
name: 'Inter',
family: '"Inter", sans-serif',
data: new Uint8Array([20, 21, 22, 23, 24]),
format: 'ttf'
}
], 'preserved in getFonts()');
assert.deepEqual(newFontManager.serializeJSON(), [
{
system: true,
family: 'Ubuntu Mono',
fallback: 'monospace'
},
{
system: false,
family: 'Inter',
fallback: 'sans-serif',
md5ext: '316f84429ec778137b2f5c6f893c7e41.ttf'
}
], 'preserved in serializeJSON()');
const assets = newFontManager.serializeAssets();
assert.strictEqual(assets.length, 1);
assert.deepEqual(
assets[0].data,
new Uint8Array([20, 21, 22, 23, 24]),
'preserved in serializeAssets()'
);
});
test('serialization and deserialization roundtrip - target', async () => {
const originalVM = new VirtualMachine();
originalVM.attachStorage(makeTestStorage());
const {fontManager, storage} = originalVM.runtime;
await originalVM.loadProject(fs.readFileSync(emptyProjectFixture));
// The fixture we use only contains a stage. We'll convert it to a sprite so we can
// addSprite() it later.
const sprite = originalVM.runtime.targets[0];
sprite.isStage = false;
const noFontsJSON = JSON.parse(originalVM.toJSON(sprite.id));
assert.ok(!('customFonts' in noFontsJSON), 'does not serialize fonts in target if no fonts');
fontManager.addCustomFont('Noto Sans Traditional Chinese', 'sans-serif', storage.createAsset(
storage.AssetType.Font,
storage.DataFormat.TTF,
new Uint8Array([97, 98, 99]),
null,
true
));
fontManager.addSystemFont('FreeSans', 'sans-serif');
const spriteJSON = JSON.parse(originalVM.toJSON(sprite.id));
assert.deepEqual(spriteJSON.customFonts, [
{
system: false,
family: 'Noto Sans Traditional Chinese',
fallback: 'sans-serif',
md5ext: '900150983cd24fb0d6963f7d28e17f72.ttf'
},
{
system: true,
family: 'FreeSans',
fallback: 'sans-serif'
}
], 'serializes custom fonts to target');
const exportedSprite = await originalVM.exportSprite(sprite.id, 'uint8array');
const newVM = new VirtualMachine();
newVM.attachStorage(makeTestStorage());
const newFontManager = newVM.runtime.fontManager;
await newVM.loadProject(fs.readFileSync(emptyProjectFixture));
// The existing fonts should not be removed or overwritten
newFontManager.addSystemFont('Liberation Sans', 'sans-serif');
newFontManager.addSystemFont('FreeSans', 'monospace');
let changed = false;
newFontManager.on('change', () => {
changed = true;
});
await newVM.addSprite(exportedSprite);
assert.ok(changed, 'addSprite() emits change');
assert.deepEqual(newFontManager.getFonts(), [
// Importing a sprite should not overwrite old fonts.
{
system: true,
name: 'Liberation Sans',
family: '"Liberation Sans", sans-serif',
data: null,
format: null
},
{
system: true,
name: 'FreeSans',
family: '"FreeSans", monospace',
data: null,
format: null
},
{
system: false,
name: 'Noto Sans Traditional Chinese',
family: '"Noto Sans Traditional Chinese", sans-serif',
data: new Uint8Array([97, 98, 99]),
format: 'ttf'
}
], 'imported fonts from sprite');
});
test('deserializes ignores invalid fonts', async () => {
const {fontManager} = new Runtime();
await fontManager.deserialize([
{
system: true,
family: ';} body { display: none; }',
fallback: 'sans-serif'
},
{
system: true,
family: 'Source Code Pro',
fallback: 'monospace'
}
], null, false);
assert.strictEqual(fontManager.getFonts().length, 1);
assert.strictEqual(fontManager.getFonts()[0].name, 'Source Code Pro');
});
test('restrict throws on invalid input', () => {
const {fontManager} = new Runtime();
assert.throws(() => {
fontManager.restrictFont('(#@*$');
}, {
message: 'Invalid font'
});
});
test('restrict removes existing fonts', () => {
let setCustomFontsCalls = 0;
const mockRenderer = {
setLayerGroupOrdering: () => {},
setCustomFonts: () => {
setCustomFontsCalls++;
}
};
const rt = new Runtime();
rt.attachRenderer(mockRenderer);
rt.attachStorage(makeTestStorage());
const {fontManager, storage} = rt;
let changeEvents = 0;
fontManager.on('change', () => {
changeEvents++;
});
fontManager.addSystemFont('System Font', 'sans-serif');
fontManager.addCustomFont('Important Font', 'sans-serif', storage.createAsset(
storage.AssetType.Font,
'ttf',
new Uint8Array([11, 12, 13]),
null,
true
));
fontManager.addCustomFont('Not Important Font', 'sans-serif', storage.createAsset(
storage.AssetType.Font,
'ttf',
new Uint8Array([11, 12, 13]),
null,
true
));
assert.strictEqual(changeEvents, 3, 'sanity check');
assert.strictEqual(setCustomFontsCalls, 2, 'sanity check');
fontManager.restrictFont('Not Used');
assert.strictEqual(changeEvents, 3, 'does not emit change when unused font restricted');
assert.strictEqual(setCustomFontsCalls, 2, 'does not emit change when unused font restricted');
fontManager.restrictFont('System Font');
assert.strictEqual(changeEvents, 3, 'does not emit change when system font restricted');
assert.strictEqual(setCustomFontsCalls, 2, 'does not emit change when system font restricted');
fontManager.restrictFont('important font');
assert.strictEqual(changeEvents, 4, 'emits change when custom font restricted');
assert.strictEqual(setCustomFontsCalls, 3, 'emits change when custom font restricted');
assert.deepEqual(fontManager.getFonts().map(i => i.name), [
'System Font',
'Not Important Font'
]);
fontManager.restrictFont('Important Font');
assert.strictEqual(changeEvents, 4, 'does not emit change when restricted font restricted again');
assert.strictEqual(setCustomFontsCalls, 3, 'does not emit change when restricted font restricted again');
});
test('overriding existing fonts', () => {
let setCustomFontsCalls = 0;
const mockRenderer = {
setLayerGroupOrdering: () => {},
setCustomFonts: () => {
setCustomFontsCalls++;
}
};
const rt = new Runtime();
rt.attachRenderer(mockRenderer);
rt.attachStorage(makeTestStorage());
const {fontManager, storage} = rt;
let changeEvents = 0;
fontManager.on('change', () => {
changeEvents++;
});
const asset = storage.createAsset(
storage.AssetType.Font,
'ttf',
new Uint8Array([11, 12, 13]),
null,
true
);
fontManager.addCustomFont('TestFont', 'sans-serif', asset);
assert.strictEqual(changeEvents, 1);
assert.strictEqual(setCustomFontsCalls, 1);
assert.deepEqual(fontManager.getFonts().map(i => i.name), ['TestFont']);
fontManager.addSystemFont('TestFonT', 'sans-serif');
assert.strictEqual(changeEvents, 2);
assert.strictEqual(setCustomFontsCalls, 2);
assert.deepEqual(fontManager.getFonts().map(i => i.name), ['TestFonT']);
fontManager.addSystemFont('TestFONT', 'sans-serif');
assert.strictEqual(changeEvents, 3);
assert.strictEqual(setCustomFontsCalls, 2);
assert.deepEqual(fontManager.getFonts().map(i => i.name), ['TestFONT']);
fontManager.addCustomFont('TESTFONT', 'sans-serif', asset);
assert.strictEqual(changeEvents, 4);
assert.strictEqual(setCustomFontsCalls, 3);
assert.deepEqual(fontManager.getFonts().map(i => i.name), ['TESTFONT']);
});
test('deserialize waits for renderer to load fonts', async () => {
const zip = new JSZip();
zip.file('8e0b5e4ad4e3b2b9a8b4e9bd0ec4de2c.ttf', new Uint8Array([1, 2, 3]));
let resolveSetCustomFontsCalled;
const setCustomFontsCalled = new Promise(resolve => {
resolveSetCustomFontsCalled = resolve;
});
let resolveFontsLoaded;
const mockRenderer = {
setLayerGroupOrdering: () => {},
setCustomFonts: () => {
resolveSetCustomFontsCalled();
return new Promise(resolve => {
resolveFontsLoaded = resolve;
});
}
};
const rt = new Runtime();
rt.attachRenderer(mockRenderer);
rt.attachStorage(makeTestStorage());
let deserialized = false;
const promise = rt.fontManager.deserialize([
{
system: false,
family: 'TestFont',
fallback: 'sans-serif',
md5ext: '8e0b5e4ad4e3b2b9a8b4e9bd0ec4de2c.ttf'
}
], zip, false).then(() => {
deserialized = true;
});
await setCustomFontsCalled;
await new Promise(resolve => setTimeout(resolve));
assert.strictEqual(deserialized, false, 'still waiting for fonts');
resolveFontsLoaded();
await promise;
assert.strictEqual(deserialized, true);
assert.deepEqual(rt.fontManager.getFonts().map(i => i.name), ['TestFont']);
});