Initial commit of 001code-html Scratch frontend project.
Includes scratch-gui, scratch-vm, scratch-blocks, scratch-render, scratch-l10n, and deployment config. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
589
scratch-vm/test/integration/tw_font_manager.js
Normal file
589
scratch-vm/test/integration/tw_font_manager.js
Normal file
@@ -0,0 +1,589 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {test} = require('tap');
|
||||
const _makeTestStorage = require('../fixtures/make-test-storage');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const VirtualMachine = require('../../src/virtual-machine');
|
||||
|
||||
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('isValidFamily', t => {
|
||||
const {fontManager} = new Runtime();
|
||||
t.ok(fontManager.isValidFamily('Roboto'));
|
||||
t.ok(fontManager.isValidFamily('sans-serif'));
|
||||
t.ok(fontManager.isValidFamily('helvetica neue'));
|
||||
t.notOk(fontManager.isValidFamily('Roboto;Bold'));
|
||||
t.notOk(fontManager.isValidFamily('Arial, sans-serif'));
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('getSafeName', t => {
|
||||
const {fontManager} = new Runtime();
|
||||
t.equal(fontManager.getSafeName('Arial'), 'Arial');
|
||||
fontManager.addSystemFont('Arial', 'sans-serif');
|
||||
t.equal(fontManager.getSafeName('Arial'), 'Arial2');
|
||||
t.equal(fontManager.getSafeName('Weird123!@"<>?'), 'Weird123');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('system font', t => {
|
||||
const mockRenderer = {
|
||||
setLayerGroupOrdering: () => {},
|
||||
setCustomFonts: () => {
|
||||
t.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');
|
||||
t.ok(changed, 'addSystemFont() emits change');
|
||||
t.ok(fontManager.hasFont('Noto Sans Mono'), 'updated hasFont()');
|
||||
t.same(fontManager.getFonts(), [
|
||||
{
|
||||
system: true,
|
||||
name: 'Noto Sans Mono',
|
||||
family: '"Noto Sans Mono", monospace',
|
||||
data: null,
|
||||
format: null
|
||||
}
|
||||
]);
|
||||
t.same(fontManager.serializeJSON(), [
|
||||
{
|
||||
system: true,
|
||||
family: 'Noto Sans Mono',
|
||||
fallback: 'monospace'
|
||||
}
|
||||
]);
|
||||
t.same(fontManager.serializeAssets(), []);
|
||||
|
||||
changed = false;
|
||||
fontManager.addSystemFont('Lobster', 'fantasy, sans-serif');
|
||||
t.ok(changed, 'addSystemFont() emits change');
|
||||
t.ok(fontManager.hasFont('Lobster'), 'updated hasFont()');
|
||||
t.same(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
|
||||
}
|
||||
]);
|
||||
t.same(fontManager.serializeJSON(), [
|
||||
{
|
||||
system: true,
|
||||
family: 'Noto Sans Mono',
|
||||
fallback: 'monospace'
|
||||
},
|
||||
{
|
||||
system: true,
|
||||
family: 'Lobster',
|
||||
fallback: 'fantasy, sans-serif'
|
||||
}
|
||||
]);
|
||||
t.same(fontManager.serializeAssets(), []);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('system font validation', t => {
|
||||
const {fontManager} = new Runtime();
|
||||
t.throws(() => {
|
||||
fontManager.addCustomFont(';', 'monospace');
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('clear', t => {
|
||||
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');
|
||||
t.equal(fontManager.getFonts().length, 1);
|
||||
|
||||
let changed = false;
|
||||
fontManager.on('change', () => {
|
||||
changed = true;
|
||||
});
|
||||
fontManager.clear();
|
||||
t.ok(changed, 'clear() emits change');
|
||||
t.equal(fontManager.getFonts().length, 0, 'removed font');
|
||||
t.same(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();
|
||||
t.ok(changed, 'clear() emits change');
|
||||
t.same(setCustomFontsCalls, [{}], 'clear() clears setCustomFonts()');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('custom fonts', t => {
|
||||
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
|
||||
));
|
||||
t.ok(changed, 'addCustomFont() emits change');
|
||||
t.ok(fontManager.hasFont('Arial'), 'updated hasFont()');
|
||||
t.same(fontManager.getFonts(), [
|
||||
{
|
||||
system: false,
|
||||
name: 'Arial',
|
||||
family: '"Arial", sans-serif',
|
||||
data: new Uint8Array([1, 2, 3]),
|
||||
format: 'ttf'
|
||||
}
|
||||
]);
|
||||
t.same(fontManager.serializeJSON(), [
|
||||
{
|
||||
system: false,
|
||||
family: 'Arial',
|
||||
fallback: 'sans-serif',
|
||||
md5ext: '5289df737df57326fcdd22597afb1fac.ttf'
|
||||
}
|
||||
]);
|
||||
t.same(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();
|
||||
t.equal(assets.length, 1);
|
||||
t.same(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);
|
||||
t.ok(changed, 'addCustomFont() emits change');
|
||||
t.ok(fontManager.hasFont('Comic Sans MS'), 'updated hasFont()');
|
||||
t.same(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'
|
||||
}
|
||||
]);
|
||||
t.same(fontManager.serializeJSON(), [
|
||||
{
|
||||
system: false,
|
||||
family: 'Arial',
|
||||
fallback: 'sans-serif',
|
||||
md5ext: '5289df737df57326fcdd22597afb1fac.ttf'
|
||||
},
|
||||
{
|
||||
system: false,
|
||||
family: 'Comic Sans MS',
|
||||
fallback: 'serif',
|
||||
md5ext: 'b4a3ba90641372b4e4eaa841a5a400ec.woff2'
|
||||
}
|
||||
]);
|
||||
t.same(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();
|
||||
t.equal(assets2.length, 2);
|
||||
t.equal(assets2[0], assets[0]);
|
||||
t.equal(assets2[1], asset);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('custom font validation', t => {
|
||||
const rt = new Runtime();
|
||||
rt.attachStorage(makeTestStorage());
|
||||
const {fontManager, storage} = rt;
|
||||
|
||||
t.throws(() => {
|
||||
fontManager.addCustomFont('family;', 'sans-serif', storage.createAsset(
|
||||
storage.DataFormat.Font,
|
||||
storage.DataFormat.TTF,
|
||||
new Uint8Array([1]),
|
||||
null,
|
||||
true
|
||||
));
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('deleteFont', t => {
|
||||
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
|
||||
));
|
||||
|
||||
t.ok(fontManager.hasFont('Liberation Mono'), 'has font initially');
|
||||
t.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);
|
||||
t.ok(changed, 'deleteFont() emits change');
|
||||
t.ok(fontManager.hasFont('Liberation Mono'), 'kept font');
|
||||
t.notOk(fontManager.hasFont('Noto Sans Mono'), 'deleted font');
|
||||
t.same(setCustomFontsCalls, [{}], 'called setCustomFonts() after deleting non-system font');
|
||||
t.same(fontManager.getFonts(), [
|
||||
{
|
||||
system: true,
|
||||
name: 'Liberation Mono',
|
||||
family: '"Liberation Mono", monospace',
|
||||
data: null,
|
||||
format: null
|
||||
}
|
||||
], 'updated getFonts() after deleting');
|
||||
|
||||
changed = false;
|
||||
fontManager.deleteFont(0);
|
||||
t.ok(changed, 'deleteFont() emits change');
|
||||
t.same(setCustomFontsCalls, [{}], 'did not call setCustomFonts() again after deleting system font');
|
||||
t.same(fontManager.getFonts(), [], 'updated getFonts() after deleting');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('fonts are serialized by VM', t => {
|
||||
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;
|
||||
t.same(assets, [fontAsset], 'font is in vm.assets');
|
||||
|
||||
const serializedAssets = vm.serializeAssets();
|
||||
t.same(serializedAssets, [
|
||||
{
|
||||
fileName: '94263e4d553bcec128704e354b659526.ttf',
|
||||
fileContent: new Uint8Array([10, 11, 12])
|
||||
}
|
||||
], 'font is in vm.serializeAssets()');
|
||||
|
||||
const notZippedProject = vm.saveProjectSb3DontZip();
|
||||
t.equal(
|
||||
notZippedProject['94263e4d553bcec128704e354b659526.ttf'],
|
||||
fontAsset.data,
|
||||
'font is in saveProjectSb3DontZip()'
|
||||
);
|
||||
|
||||
const projectJSON = JSON.parse(vm.toJSON());
|
||||
t.same(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()');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('does not serialize fonts if there are none', t => {
|
||||
const vm = new VirtualMachine();
|
||||
const json = JSON.parse(vm.toJSON());
|
||||
t.not('customFonts' in json);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('serialization and deserialization roundtrip - project', t => {
|
||||
const originalVM = new VirtualMachine();
|
||||
originalVM.attachStorage(makeTestStorage());
|
||||
const {storage, fontManager} = originalVM.runtime;
|
||||
|
||||
originalVM.loadProject(fs.readFileSync(emptyProjectFixture)).then(() => {
|
||||
// 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);
|
||||
|
||||
originalVM.saveProjectSb3('arraybuffer').then(projectSb3 => {
|
||||
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;
|
||||
});
|
||||
|
||||
newVM.loadProject(projectSb3).then(() => {
|
||||
t.ok(changed, 'loadProject() emits change');
|
||||
|
||||
t.same(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()');
|
||||
t.same(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();
|
||||
t.equal(assets.length, 1);
|
||||
t.same(assets[0].data, new Uint8Array([20, 21, 22, 23, 24]), 'preserved in serializeAssets()');
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('serialization and deserialization roundtrip - target', t => {
|
||||
const originalVM = new VirtualMachine();
|
||||
originalVM.attachStorage(makeTestStorage());
|
||||
const {fontManager, storage} = originalVM.runtime;
|
||||
|
||||
originalVM.loadProject(fs.readFileSync(emptyProjectFixture)).then(() => {
|
||||
// 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));
|
||||
t.notOk('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));
|
||||
t.same(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');
|
||||
|
||||
originalVM.exportSprite(sprite.id, 'uint8array').then(exportedSprite => {
|
||||
const newVM = new VirtualMachine();
|
||||
newVM.attachStorage(makeTestStorage());
|
||||
const newFontManager = newVM.runtime.fontManager;
|
||||
|
||||
newVM.loadProject(fs.readFileSync(emptyProjectFixture)).then(() => {
|
||||
// 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;
|
||||
});
|
||||
|
||||
newVM.addSprite(exportedSprite).then(() => {
|
||||
t.ok(changed, 'addSprite() emits change');
|
||||
|
||||
t.same(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');
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('deserializes ignores invalid fonts', t => {
|
||||
const {fontManager} = new Runtime();
|
||||
fontManager.deserialize([
|
||||
{
|
||||
system: true,
|
||||
family: ';} body { display: none; }',
|
||||
fallback: 'sans-serif'
|
||||
},
|
||||
{
|
||||
system: true,
|
||||
family: 'Source Code Pro',
|
||||
fallback: 'monospace'
|
||||
}
|
||||
], null, false).then(() => {
|
||||
t.equal(fontManager.getFonts().length, 1);
|
||||
t.equal(fontManager.getFonts()[0].name, 'Source Code Pro');
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user