// The Store's icon-pack and built-in kinds as X configures them: the source folders (store/x/icons/), // the area plugins as `builtin` catalog items with no file to download, the icon pack the app can // import (the same parser Appearance uses), and what the Store view says of each. import assert from 'node:assert/strict'; import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import test from 'node:test'; // @ts-expect-error: the framework CLI's plain ES module import { buildStore, catalogProblems } from '../../../waybetter/packages/waybetter/cli/store.mjs'; import { parseIconPackJson } from '../../../waybetter/packages/runtime/src/themeIcons.ts'; // @ts-expect-error: plain ES modules shared with the Store scripts import config from '../../../scripts/store.config.mjs'; // @ts-expect-error: plain ES module shared with scripts/check-store.mjs import { readStoreSource } from '../../../scripts/store/source.mjs'; import { X_TABLE } from '../src/areas/table.ts'; import { filterItems, itemState, type StoreItem } from '../src/shell/installModel.ts'; const PACK = { id: 'line-2', name: 'Line 2', author: 'me', license: 'CC0-1.0', description: 'Thin lines.', glyphs: { home: { d: 'M3 3h18v18H3z' }, like: { d: 'M4 4h16v16H4z' } } }; function folder(files: Record) { const root = mkdtempSync(join(tmpdir(), 'wb-x-store-')); const dir = join(root, 'store/x/icons/line-2'); mkdirSync(dir, { recursive: true }); for (const [name, value] of Object.entries(files)) writeFileSync(join(dir, name), typeof value === 'string' ? value : JSON.stringify(value)); return root; } const read = (root: string) => readStoreSource({ root, app: 'x', bundledDir: root, parseIconPack: parseIconPackJson }) as { entries: Array>; problems: string[] }; test('an icons folder becomes a catalog entry for the pack file, with the pack\'s own words', () => { const { entries, problems } = read(folder({ 'line-2.icons.json': PACK, 'item.json': { category: 'icons', tags: ['line'], credits: 'CRT' } })); assert.deepEqual(problems, []); assert.equal(entries.length, 1); const entry = entries[0]!; assert.deepEqual([entry.kind, entry.id, entry.name, entry.author, entry.license, entry.credits, entry.path], ['icons', 'line-2', 'Line 2', 'me', 'CC0-1.0', 'CRT', 'icons/line-2.icons.json']); assert.equal(entry.preview.path, 'previews/icons-line-2.svg'); assert.ok(entry.pack.glyphs.home, 'the parsed pack is kept for the checks'); }); test('an icons folder is refused when the pack is not one, is named differently, or has the wrong files', () => { const problemOf = (files: Record) => read(folder(files)).problems.join('\n'); assert.match(problemOf({ 'line-2.icons.json': { ...PACK, id: 'other' }, 'item.json': { category: 'icons' } }), /folder name/); assert.match(problemOf({ 'line-2.icons.json': { id: 'line-2', name: 'No glyphs' }, 'item.json': { category: 'icons' } }), /glyph/); assert.match(problemOf({ 'line-2.icons.json': '{nope', 'item.json': {} }), /not valid JSON/); assert.match(problemOf({ 'item.json': {} }), /exactly one \*\.icons\.json/); assert.match(problemOf({ 'line-2.icons.json': PACK, 'item.json': { code: 'x' } }), /unknown field "code"/); }); test('the area plugins are built-in catalog items: no file, nothing to download, one per area, with their words', async () => { const items = (await config.items()) as Array>; const built = items.filter(item => item.kind === 'builtin'); assert.deepEqual(built.map(item => item.id), X_TABLE.areas.map(area => area.id), 'one item per area, in the table\'s order'); for (const item of built) { assert.ok(item.name && item.description && item.category && item.version, item.id); assert.ok(!('path' in item) && !('bytes' in item) && !('file' in item), `${item.id} has no file`); } const out = mkdtempSync(join(tmpdir(), 'wb-x-built-')); const { catalog } = await buildStore({ ...config, items: async () => built }, { out }); assert.deepEqual(catalogProblems(catalog, { app: 'x' }), []); for (const item of catalog.items) for (const field of ['url', 'sha256', 'size', 'file']) assert.ok(!(field in item), `${item.id}: no ${field}`); assert.equal(JSON.parse(readFileSync(join(out, 'catalog.v3.json'), 'utf8')).items.length, X_TABLE.areas.length, 'catalog.v3.json lists them'); assert.equal(JSON.parse(readFileSync(join(out, 'catalog.json'), 'utf8')).items.length, 0, 'catalog.json (older apps) does not'); }); test('the Store view: a built-in item is an area (its switch), an icon pack follows its record and the packs the owner has', () => { const area = { apps: ['x'], kind: 'builtin', id: 'compose', name: 'Compose+', author: '', version: '1.0.0', description: 'Writing tools.', category: 'writing', tags: [] } as StoreItem; const base = { plugins: {}, records: {}, builtinThemes: [], userThemes: [] as string[] }; assert.equal(itemState(area, base).state, 'area'); assert.deepEqual(filterItems([area], 'compose', 'builtin', 'writing').map(item => item.id), ['compose']); const pack = { apps: ['x'], kind: 'icons', id: 'line-2', name: 'Line 2', author: 'me', version: '1.0.0', description: 'Thin lines.', category: 'icons', tags: [], url: 'https://waybetter.fyi/store/icons/line-2.icons.json', sha256: 'a'.repeat(64), size: 99 } as StoreItem; assert.equal(itemState(pack, base).state, 'install'); const records = { 'icons:line-2': { kind: 'icons' as const, source: 'store' as const, storeId: 'line-2', version: '1.0.0', sha256: 'a'.repeat(64), installedAt: '' } }; assert.equal(itemState(pack, { ...base, records, userIcons: ['line-2'] }).state, 'installed'); assert.equal(itemState(pack, { ...base, records, userIcons: [] }).state, 'install', 'removed in Appearance: installable again'); });