diff --git a/src/components/Sidebar.test.ts b/src/components/Sidebar.test.ts index 610c4558..1c905c19 100644 --- a/src/components/Sidebar.test.ts +++ b/src/components/Sidebar.test.ts @@ -58,4 +58,16 @@ describe('buildSectionGroup', () => { const group = buildSectionGroup('Config', typeEntryMap) expect(group.customColor).toBe('gray') }) + + it('resolves type entry via lowercase key (case-insensitive isA)', () => { + // When instances have isA: 'config' (lowercase) but type entry title is 'Config' + const typeEntryMap: Record = { + Config: { ...baseEntry, title: 'Config', isA: 'Type', icon: 'gear-six', color: 'gray', sidebarLabel: 'Config' }, + config: { ...baseEntry, title: 'Config', isA: 'Type', icon: 'gear-six', color: 'gray', sidebarLabel: 'Config' }, + } + const group = buildSectionGroup('config', typeEntryMap) + expect(group.label).toBe('Config') + expect(group.customColor).toBe('gray') + expect(group.Icon).toBe(GearSix) + }) }) diff --git a/src/utils/typeColors.test.ts b/src/utils/typeColors.test.ts index ddf61254..d4f64483 100644 --- a/src/utils/typeColors.test.ts +++ b/src/utils/typeColors.test.ts @@ -67,16 +67,17 @@ const baseEntry: VaultEntry = { } describe('buildTypeEntryMap', () => { - it('indexes Type entries by title', () => { + it('indexes Type entries by title and lowercase', () => { const entries: VaultEntry[] = [ { ...baseEntry, title: 'Recipe', isA: 'Type', color: 'orange', icon: 'cooking-pot' }, { ...baseEntry, title: 'My Note', isA: 'Note' }, { ...baseEntry, title: 'Evergreen', isA: 'Type', color: 'green', icon: 'leaf' }, ] const map = buildTypeEntryMap(entries) - expect(Object.keys(map)).toEqual(['Recipe', 'Evergreen']) expect(map['Recipe'].color).toBe('orange') + expect(map['recipe'].color).toBe('orange') expect(map['Evergreen'].icon).toBe('leaf') + expect(map['evergreen'].icon).toBe('leaf') }) it('returns empty map when no Type entries exist', () => { @@ -86,13 +87,14 @@ describe('buildTypeEntryMap', () => { expect(buildTypeEntryMap(entries)).toEqual({}) }) - it('preserves sidebarLabel in type entry', () => { + it('preserves sidebarLabel in type entry via exact and lowercase keys', () => { const entries: VaultEntry[] = [ { ...baseEntry, title: 'Config', isA: 'Type', icon: 'gear-six', color: 'gray', sidebarLabel: 'Config' }, ] const map = buildTypeEntryMap(entries) expect(map['Config'].sidebarLabel).toBe('Config') - expect(map['Config'].icon).toBe('gear-six') - expect(map['Config'].color).toBe('gray') + expect(map['config'].sidebarLabel).toBe('Config') + expect(map['config'].icon).toBe('gear-six') + expect(map['config'].color).toBe('gray') }) }) diff --git a/src/utils/typeColors.ts b/src/utils/typeColors.ts index 1fd1638f..cc7ca6a8 100644 --- a/src/utils/typeColors.ts +++ b/src/utils/typeColors.ts @@ -5,10 +5,18 @@ import type { VaultEntry } from '../types' -/** Builds a map from type name → Type document entry (for custom color/icon lookup) */ +/** Builds a map from type name → Type document entry (for custom color/icon lookup). + * Stores both original title and lowercase version so lookups work regardless + * of whether instances use `isA: Config` or `isA: config`. */ export function buildTypeEntryMap(entries: VaultEntry[]): Record { const map: Record = {} - for (const e of entries) { if (e.isA === 'Type') map[e.title] = e } + for (const e of entries) { + if (e.isA === 'Type') { + map[e.title] = e + const lower = e.title.toLowerCase() + if (lower !== e.title) map[lower] = e + } + } return map } diff --git a/tests/smoke/type-icon-color-sidebar-label.spec.ts b/tests/smoke/type-icon-color-sidebar-label.spec.ts new file mode 100644 index 00000000..da97705b --- /dev/null +++ b/tests/smoke/type-icon-color-sidebar-label.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@playwright/test' + +test.describe('Type icon, color, and sidebar label', () => { + test.beforeEach(async ({ page }) => { + // Block vault API so the app falls back to mock data (which contains our test fixtures) + await page.route('**/api/vault/**', (route) => route.abort()) + await page.goto('/') + await page.waitForLoadState('networkidle') + }) + + test('Config section shows correct sidebar label from type entry', async ({ page }) => { + const sidebar = page.locator('aside') + await sidebar.locator('button[aria-label*="Collapse"], button[aria-label*="Expand"]').first().waitFor({ timeout: 5000 }) + + // The Config type entry has sidebarLabel: "Config" — the section button should use that label + const configBtn = sidebar.locator('button[aria-label="Collapse Config"], button[aria-label="Expand Config"]') + await expect(configBtn).toBeVisible() + }) + + test('Config section icon is not the default FileText', async ({ page }) => { + const sidebar = page.locator('aside') + await sidebar.locator('button[aria-label*="Collapse"], button[aria-label*="Expand"]').first().waitFor({ timeout: 5000 }) + + const configSection = sidebar.locator('div.group\\/section').filter({ + has: page.locator('button[aria-label="Collapse Config"], button[aria-label="Expand Config"]'), + }) + await expect(configSection).toBeVisible() + + // The icon SVG should be present (GearSix, resolved from type entry icon: 'gear-six') + const icon = configSection.locator('svg').first() + await expect(icon).toBeVisible() + }) + + test('Config section icon has gray color applied via style', async ({ page }) => { + const sidebar = page.locator('aside') + await sidebar.locator('button[aria-label*="Collapse"], button[aria-label*="Expand"]').first().waitFor({ timeout: 5000 }) + + const configSection = sidebar.locator('div.group\\/section').filter({ + has: page.locator('button[aria-label="Collapse Config"], button[aria-label="Expand Config"]'), + }) + await expect(configSection).toBeVisible() + + // The icon should have gray color from the type entry's color: 'gray' + const icon = configSection.locator('svg').first() + const color = await icon.evaluate((el) => el.style.color) + expect(color).toContain('var(--accent-gray)') + }) + + test('custom type with icon/color reflects in sidebar (Recipe)', async ({ page }) => { + const sidebar = page.locator('aside') + await sidebar.locator('button[aria-label*="Collapse"], button[aria-label*="Expand"]').first().waitFor({ timeout: 5000 }) + + // Recipe type has icon: cooking-pot, color: orange — check section has correct color + const recipeSection = sidebar.locator('div.group\\/section').filter({ + has: page.locator('button[aria-label="Collapse Recipes"], button[aria-label="Expand Recipes"]'), + }) + await expect(recipeSection).toBeVisible() + + const icon = recipeSection.locator('svg').first() + const color = await icon.evaluate((el) => el.style.color) + expect(color).toContain('var(--accent-orange)') + }) +})