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:
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
63
tests/smoke/type-icon-color-sidebar-label.spec.ts
Normal file
63
tests/smoke/type-icon-color-sidebar-label.spec.ts
Normal 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)')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user