import { describe, expect, it } from 'vitest'; import { ASSET_TYPES } from '../../src/core/model/AssetTypes'; import { ASSET_ICON_SVG, GAME_ICON_SVG, LIST_ICON_SVG, PLUGINS_ICON_SVG, } from '../../src/editor/panels/assetIcons'; 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 }[] { 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], ]; // 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('', 0x0000ff)).toBe( '', ); for (const [, svg] of icons) expect(tintSvg(svg!, 0xb066ff)).not.toContain('currentColor'); }); });