The asset panel only listed the categories and types that hold assets. To make the first asset of a kind you had to know its category in the `+` menu. A switch now lists all of them, and a right-click on a heading offers just what sits under it. - General settings has "Show empty asset categories and types", off by default and kept in this browser. Applying it refreshes the panel at once through a new `onGeneralSettings` listener. - `groupAssets(assets, empty)` gives every category and type when asked. The empty ones stand faint with a count of 0, and an empty type has no chevron, since there is nothing to unfold. - `ListItem.faint` draws a row's label, hint and icons at half opacity, like a disabled widget. - A right-click on a category heading offers the types of that category without a submenu, on a type heading only that type. The empty space below the rows still opens the whole `+` menu. `ListView.onListMenu` passes the heading's id for this. - Every menu of new assets says "Create asset" above its entries, so a click on a type reads as making one. `MenuOptions.title` draws that muted line. It is not an entry and matches no search. Tests: - Unit asset-groups and view-tween: 23 green, 2 new. Counter-test with the empty flag ignored: the new groups test fails. - e2e 04 (melektaus-atx): ALL PASSED, 67 checks, 4 new and 1 rewritten. Counter-test with the heading id dropped: the 3 menu checks fail. The title is checked on the `+` button and on a type heading (2 checks extended), the rerun passed with 67 checks. - Gate (melektaus-atx): CHECK EXIT 0, 272 files, 4212 tests. It ran before the menu title. Afterwards only the menu and asset-groups units, typecheck and lint ran, all clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
3.9 KiB
TypeScript
84 lines
3.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type { Asset } from '../../src/core/model/Asset';
|
|
import { ASSET_TYPES, type AssetType } from '../../src/core/model/AssetTypes';
|
|
import { ASSET_CATEGORIES, ASSET_GROUPS, groupAssets } from '../../src/editor/panels/assetGroups';
|
|
|
|
let serial = 0;
|
|
const asset = (type: AssetType, name: string): Asset =>
|
|
({ id: `a${++serial}`, type, name, props: {} }) as unknown as Asset;
|
|
|
|
const groupsOf = (assets: readonly Asset[]) => groupAssets(assets).flatMap((c) => c.groups);
|
|
const shape = (assets: Asset[]) => groupsOf(assets).map((g) => [g.type, g.assets.map((a) => a.name)]);
|
|
|
|
describe('groupAssets (#176)', () => {
|
|
it('puts every asset into the group of its type, and no empty group', () => {
|
|
const assets = [asset('texture', 'Sheet'), asset('sprite', 'Hero'), asset('texture', 'Tiles')];
|
|
expect(shape(assets)).toEqual([
|
|
['sprite', ['Hero']],
|
|
['texture', ['Sheet', 'Tiles']],
|
|
]);
|
|
});
|
|
|
|
it('puts the groups under their category, categories in their fixed order and none empty', () => {
|
|
const assets = [
|
|
asset('song', 'Tune'),
|
|
asset('behavior', 'Mover'),
|
|
asset('scene', 'Level'),
|
|
asset('sprite', 'Hero'),
|
|
];
|
|
expect(groupAssets(assets).map((c) => [c.category, c.groups.map((g) => g.type)])).toEqual([
|
|
['Basics', ['scene']],
|
|
['Visuals', ['sprite']],
|
|
['Logics', ['behavior']],
|
|
['Audio', ['song']],
|
|
]);
|
|
});
|
|
|
|
it('sorts the groups in a category by their heading, not by type or declaration order', () => {
|
|
const types = [...ASSET_TYPES].reverse();
|
|
const categories = groupAssets(types.map((t) => asset(t, 'x')));
|
|
expect(categories.map((c) => c.category)).toEqual([...ASSET_CATEGORIES]);
|
|
for (const { groups } of categories) {
|
|
const headings = groups.map((g) => ASSET_GROUPS[g.type].name);
|
|
expect(headings).toEqual([...headings].sort((a, b) => a.localeCompare(b)));
|
|
}
|
|
expect(categories.flatMap((c) => c.groups)).toHaveLength(ASSET_TYPES.length);
|
|
// By heading: "State machine" sorts under S, the type key `statemachine` would not tell.
|
|
const logic = categories
|
|
.find((c) => c.category === 'Logics')!
|
|
.groups.map((g) => ASSET_GROUPS[g.type].name);
|
|
expect(logic.indexOf('Behavior')).toBeLessThan(logic.indexOf('State machine'));
|
|
expect(logic.indexOf('State machine')).toBeLessThan(logic.indexOf('Struct'));
|
|
});
|
|
|
|
it('sorts the assets in a group by name, ignoring case and reading numbers as numbers', () => {
|
|
const assets = ['Level 10', 'level 2', 'Boss', 'Level 1', 'arena'].map((n) => asset('scene', n));
|
|
expect(shape(assets)).toEqual([['scene', ['arena', 'Boss', 'Level 1', 'level 2', 'Level 10']]]);
|
|
});
|
|
|
|
it('keeps the project order among equal names', () => {
|
|
const first = asset('sprite', 'Coin');
|
|
const second = asset('sprite', 'coin');
|
|
expect(groupsOf([first, second])[0]!.assets).toEqual([first, second]);
|
|
expect(groupsOf([second, first])[0]!.assets).toEqual([second, first]);
|
|
});
|
|
|
|
it('lists every category and type when asked to, the empty ones with no assets (#361)', () => {
|
|
const categories = groupAssets([asset('sprite', 'Hero')], true);
|
|
expect(categories.map((c) => c.category)).toEqual([...ASSET_CATEGORIES]);
|
|
const groups = categories.flatMap((c) => c.groups);
|
|
expect(groups.map((g) => g.type).sort()).toEqual([...ASSET_TYPES].sort());
|
|
expect(groups.filter((g) => g.assets.length > 0).map((g) => g.type)).toEqual(['sprite']);
|
|
for (const { category, groups } of categories)
|
|
for (const { type } of groups) expect(ASSET_GROUPS[type].category).toBe(category);
|
|
expect(groupAssets([], true).flatMap((c) => c.groups)).toHaveLength(ASSET_TYPES.length);
|
|
expect(groupAssets([])).toEqual([]);
|
|
});
|
|
|
|
it('leaves the project list alone', () => {
|
|
const assets = [asset('sprite', 'B'), asset('sprite', 'A')];
|
|
groupAssets(assets);
|
|
expect(assets.map((a) => a.name)).toEqual(['B', 'A']);
|
|
});
|
|
});
|