fix: case-insensitive type entry lookup + Playwright smoke test

buildTypeEntryMap now stores both original title and lowercase key so
isA: 'config' matches type entry titled 'Config'. Adds Playwright smoke
test that blocks the vault API to test against mock data fixtures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Test
2026-03-11 21:06:45 +01:00
parent b3bf2bf76e
commit c499ef30f0
4 changed files with 92 additions and 7 deletions

View File

@@ -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<string, VaultEntry> = {
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)
})
})

View File

@@ -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')
})
})

View File

@@ -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<string, VaultEntry> {
const map: Record<string, VaultEntry> = {}
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
}

View File

@@ -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)')
})
})