The theme asks for Source Sans 3 at weight 500, but fonts.ts only
loaded 400, 600 and 700. The browser synthesised the 500 text. The
issue comment also asked for the symbols the font lacks to become SVG
icons. A scan of src/ against the font's cmap found 20 such characters
in 30 places.
- fonts.ts loads source-sans-3-latin-500-normal.woff2 as well.
- Menus draw their ticks and the "current" dot as icons. The dot is a
new MenuItem.current field and no longer part of the label text.
- Inspector action buttons (reset, apply, fit width/height) are
icons. The menu arrow behind them is the drawn Dropdown chevron.
- Kind marks come from a table in markIcons.ts. Picture kinds use an
icon, # and Aa stay text because the font has them.
- Scene marks in the asset panel, sound play/stop and the transition
arrows are icons too.
- Apply is an icon although the font has the arrow. It sits next to
the reset icon and would look different as a glyph.
- A new Icon widget draws on redraw like Button, so Node tests need no
DOMParser.
- Tests find icons through the SVG id. The new hook
__gm.locateIcons(id) and the harness helpers findIcon/clickIcon
replace locate() on the old characters.
Findings:
- Weight 700 is loaded but the theme never uses it. Left as is.
- The old scene-mark check in module 35 accepted any play glyph on
screen. It now checks the mark on the start scene's row.
- Module 44 needs the textures from module 37 and fails without it.
Tests:
- ./check.sh: 145 files, 2187 tests, exit 0 (before 144 / 2143).
- New tests/ui/fonts.test.ts: every theme weight is loaded, and every
character in src/ exists in every shipped face. The cmap reader
matches fontTools (231 characters).
- Icon tests cover the 19 new files. Each kind mark is either an
icon or text, never both.
- e2e 1-16,22,30,34,35,43,44: 586 green, 0 FAIL, then 44 stopped
for lack of module 37. e2e 1-16,37,44,47,48: 618 green.
e2e 1-14 on the final build: 454 green.
- Two new e2e checks: loaded weights in 14, a reset icon pixel in 04.
- Counter-probes all fail as expected: 500 removed, a play glyph put
back, the struct icon removed (one unit test each). In the browser,
500 removed and reset icon hidden fail exactly the two new checks.
- Editor-suite check sum 1312 (1310 measured by CI at 056a622, +2).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
157 lines
7.5 KiB
TypeScript
157 lines
7.5 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import { ASSET_TYPES } from '../../src/core/model/AssetTypes';
|
||
import { DECLARED_PORT_KINDS, FIELD_KINDS, kindMark, VARIABLE_KINDS } from '../../src/core/properties/kinds';
|
||
import {
|
||
ASSET_ICON_SVG,
|
||
GAME_ICON_SVG,
|
||
LIST_ICON_SVG,
|
||
PLUGINS_ICON_SVG,
|
||
} from '../../src/editor/panels/assetIcons';
|
||
import { HEADER_ICON_SVG } from '../../src/editor/panels/headerIcons';
|
||
import { ARROW_ICON_SVG, KIND_ICON_SVG, ROW_ACTION_ICON_SVG } from '../../src/editor/panels/markIcons';
|
||
import { TOOLBAR_ICON_SVG } from '../../src/editor/panels/toolbarIcons';
|
||
import { UI_ICON_SVG } from '../../src/ui/icons';
|
||
import { tintSvg } from '../../src/ui/svg';
|
||
import { SYMBOL_SVG } from '../../src/viewport/symbolIcons';
|
||
|
||
/** Opening tags as name + attributes; the files are flat, so no real XML parser is needed. */
|
||
function tags(svg: string): { name: string; attrs: Record<string, string> }[] {
|
||
return [...svg.matchAll(/<([a-z]+)([^>]*?)\/?>/g)].map(([, name, rest]) => ({
|
||
name: name!,
|
||
attrs: Object.fromEntries([...rest!.matchAll(/([a-zA-Z-]+)="([^"]*)"/g)].map(([, k, v]) => [k!, v!])),
|
||
}));
|
||
}
|
||
|
||
describe('icon files', () => {
|
||
it('has a scene view symbol for every symbol (#175)', () => {
|
||
expect(Object.keys(SYMBOL_SVG).sort()).toEqual([
|
||
'behavior',
|
||
'camera',
|
||
'container',
|
||
'particles',
|
||
'rigidbody',
|
||
]);
|
||
});
|
||
|
||
it('has an asset panel icon for every asset type, each its own picture (#176)', () => {
|
||
expect(Object.keys(ASSET_ICON_SVG).sort()).toEqual([...ASSET_TYPES].sort());
|
||
expect(new Set(Object.values(ASSET_ICON_SVG)).size).toBe(ASSET_TYPES.length);
|
||
});
|
||
|
||
const icons = [
|
||
...Object.entries(SYMBOL_SVG).map(([name, svg]) => [`symbol ${name}`, svg]),
|
||
...Object.entries(ASSET_ICON_SVG).map(([name, svg]) => [`asset ${name}`, svg]),
|
||
['list tab', LIST_ICON_SVG],
|
||
['game tab', GAME_ICON_SVG],
|
||
['plugins tab', PLUGINS_ICON_SVG],
|
||
...Object.entries(TOOLBAR_ICON_SVG).map(([name, svg]) => [`toolbar ${name}`, svg]),
|
||
...Object.entries(HEADER_ICON_SVG).map(([name, svg]) => [`header ${name}`, svg]),
|
||
...Object.entries(UI_ICON_SVG).map(([name, svg]) => [`ui ${name}`, svg]),
|
||
...Object.entries(KIND_ICON_SVG).map(([name, svg]) => [`kind ${name}`, svg]),
|
||
...Object.entries(ROW_ACTION_ICON_SVG).map(([name, svg]) => [`row action ${name}`, svg]),
|
||
...Object.entries(ARROW_ICON_SVG).map(([name, svg]) => [`arrow ${name}`, svg]),
|
||
];
|
||
|
||
// The symbols that were characters until #214: the font the editor ships has no such glyphs.
|
||
it('marks every kind a user can pick once – by a picture, or by letters where it has none (#214)', () => {
|
||
const picked = [...new Set([...VARIABLE_KINDS, ...FIELD_KINDS, ...DECLARED_PORT_KINDS])];
|
||
const marks = picked.map((kind) => {
|
||
const icon = KIND_ICON_SVG[kind];
|
||
const letters = kindMark(kind);
|
||
expect(Boolean(icon) !== Boolean(letters), `${kind}: a picture or letters, not both`).toBe(true);
|
||
return icon ?? letters;
|
||
});
|
||
expect(new Set(marks).size).toBe(picked.length);
|
||
});
|
||
|
||
// A test finds a picture by the id of its file (`locateIcon`), so every id names one file.
|
||
it('names each picture a test looks for after its own file (#214)', () => {
|
||
const named = icons.map(([, svg]) => tags(svg!)[0]!.attrs.id).filter((id) => id !== undefined);
|
||
expect(new Set(named).size).toBe(named.length);
|
||
for (const svg of [
|
||
...Object.values(UI_ICON_SVG),
|
||
...Object.values(KIND_ICON_SVG),
|
||
...Object.values(ROW_ACTION_ICON_SVG),
|
||
...Object.values(ARROW_ICON_SVG),
|
||
])
|
||
expect(tags(svg)[0]!.attrs.id).toBeDefined();
|
||
});
|
||
|
||
// The magnet stands alone in the header, without the "Snap" it replaced (#194): a picture another
|
||
// button already wears would say that button's thing.
|
||
it('draws the snap switch as a magnet of its own, bent without arcs (#194)', () => {
|
||
const others = [
|
||
...Object.values(ASSET_ICON_SVG),
|
||
...Object.values(TOOLBAR_ICON_SVG),
|
||
LIST_ICON_SVG,
|
||
GAME_ICON_SVG,
|
||
PLUGINS_ICON_SVG,
|
||
];
|
||
expect(others).not.toContain(HEADER_ICON_SVG.snap);
|
||
expect(HEADER_ICON_SVG.snap).toContain('C');
|
||
for (const { attrs } of tags(HEADER_ICON_SVG.snap).slice(1)) expect(attrs.d ?? '').not.toMatch(/[Aa]/);
|
||
});
|
||
|
||
it('has one icon per button of the top bar, each its own picture (#183)', () => {
|
||
expect(Object.keys(TOOLBAR_ICON_SVG).sort()).toEqual(['menu', 'pause', 'play', 'redo', 'stop', 'undo']);
|
||
expect(new Set(Object.values(TOOLBAR_ICON_SVG)).size).toBe(6);
|
||
});
|
||
|
||
// The bar carries no captions any more, so a picture shared with a tab or an asset type would be
|
||
// the only thing left telling the two apart – and it would not.
|
||
it('draws the top bar in pictures of its own, none of them an asset icon (#183)', () => {
|
||
const others = [...Object.values(ASSET_ICON_SVG), LIST_ICON_SVG, GAME_ICON_SVG, PLUGINS_ICON_SVG];
|
||
for (const svg of Object.values(TOOLBAR_ICON_SVG)) expect(others).not.toContain(svg);
|
||
});
|
||
|
||
// Pixi's parser draws an A in the middle of a path as a spike (the puzzle piece of #119); undo
|
||
// and redo are the arcs of this set, so they are the ones that have to keep clear of it.
|
||
it('bends the top bar arrows with cubic curves rather than arcs (#183)', () => {
|
||
for (const svg of [TOOLBAR_ICON_SVG.undo, TOOLBAR_ICON_SVG.redo]) {
|
||
expect(svg).toContain('C');
|
||
for (const { attrs } of tags(svg).slice(1)) expect(attrs.d ?? '').not.toMatch(/[Aa]/);
|
||
}
|
||
});
|
||
|
||
// Pixi draws the file's coordinates as they are and ignores the viewBox, so the viewBox only
|
||
// shows the file right elsewhere – centred on the origin, it matches what the editor draws.
|
||
it.each(icons)('%s is centred on its origin', (_, svg) => {
|
||
const root = tags(svg)[0]!;
|
||
expect(root.name).toBe('svg');
|
||
const [x, y, w, h] = root.attrs.viewBox!.split(' ').map(Number);
|
||
expect([x, y]).toEqual([-w! / 2, -h! / 2]);
|
||
expect([Number(root.attrs.width), Number(root.attrs.height)]).toEqual([w, h]);
|
||
});
|
||
|
||
it('draws the asset icons in the 16 units a list row and a button reserve', () => {
|
||
for (const svg of [...Object.values(ASSET_ICON_SVG), LIST_ICON_SVG, GAME_ICON_SVG, PLUGINS_ICON_SVG])
|
||
expect(tags(svg)[0]!.attrs.viewBox).toBe('-8 -8 16 16');
|
||
});
|
||
|
||
it('gives the List, Game and Plugins tabs a picture no asset type has (#176, #119)', () => {
|
||
const tabs = [LIST_ICON_SVG, GAME_ICON_SVG, PLUGINS_ICON_SVG];
|
||
for (const svg of tabs) expect(Object.values(ASSET_ICON_SVG)).not.toContain(svg);
|
||
expect(new Set(tabs).size).toBe(tabs.length);
|
||
});
|
||
|
||
it.each(icons)('%s uses only what the Pixi parser draws like a browser', (_, svg) => {
|
||
for (const { name, attrs } of tags(svg).slice(1)) {
|
||
// polygon and polyline read their points as whole numbers; groups would carry transforms,
|
||
// which the parser ignores.
|
||
expect(['path', 'circle', 'rect', 'ellipse', 'line']).toContain(name);
|
||
expect(attrs.transform).toBeUndefined();
|
||
// Without a fill a browser fills black, Pixi does not fill at all once there is a stroke.
|
||
expect(attrs.fill, `${name} states its fill`).toBeDefined();
|
||
for (const paint of [attrs.fill, attrs.stroke].filter((p) => p !== undefined))
|
||
expect(paint).toMatch(/^(none|currentColor|#[0-9a-f]{6})$/);
|
||
}
|
||
});
|
||
|
||
it('puts the theme colour where the file says currentColor', () => {
|
||
expect(tintSvg('<path fill="currentColor" stroke="currentColor"/>', 0x0000ff)).toBe(
|
||
'<path fill="#0000ff" stroke="#0000ff"/>',
|
||
);
|
||
for (const [, svg] of icons) expect(tintSvg(svg!, 0xb066ff)).not.toContain('currentColor');
|
||
});
|
||
});
|