← back to ClawCoder

src/tests/lib/profiles.test.ts

141 lines

import { describe, it, expect } from 'vitest'
import { PROFILES, getProfile, getProfilesByAge, getProfilesBySkill } from '@/lib/profiles/data'
import type { AgeGroup, SkillLevel } from '@/lib/profiles/types'

describe('Profile Data', () => {
  it('has exactly 15 profiles', () => {
    expect(PROFILES).toHaveLength(15)
  })

  it('covers all 5x3 matrix cells (5 age groups x 3 skill levels)', () => {
    const ageGroups: AgeGroup[] = ['teen', 'young_adult', 'professional', 'pre_senior', 'senior']
    const skillLevels: SkillLevel[] = ['beginner', 'intermediate', 'expert']

    for (const age of ageGroups) {
      for (const skill of skillLevels) {
        const match = PROFILES.find(p => p.ageGroup === age && p.skillLevel === skill)
        expect(match, `Missing profile for ${age}-${skill}`).toBeDefined()
      }
    }
  })

  it('has unique slugs for every profile', () => {
    const slugs = PROFILES.map(p => p.slug)
    const uniqueSlugs = new Set(slugs)
    expect(uniqueSlugs.size).toBe(15)
  })

  it('has required fields on every profile', () => {
    for (const profile of PROFILES) {
      expect(profile.slug, `${profile.slug}: slug`).toBeTruthy()
      expect(profile.ageGroup, `${profile.slug}: ageGroup`).toBeTruthy()
      expect(profile.skillLevel, `${profile.slug}: skillLevel`).toBeTruthy()
      expect(profile.displayName, `${profile.slug}: displayName`).toBeTruthy()
      expect(profile.persona, `${profile.slug}: persona`).toBeTruthy()
      expect(profile.ui, `${profile.slug}: ui`).toBeDefined()
      expect(profile.template, `${profile.slug}: template`).toBeDefined()
      expect(profile.template.content, `${profile.slug}: template.content`).toBeTruthy()
      expect(Array.isArray(profile.skillBundle), `${profile.slug}: skillBundle`).toBe(true)
      expect(Array.isArray(profile.mcpServers), `${profile.slug}: mcpServers`).toBe(true)
      expect(profile.hooks, `${profile.slug}: hooks`).toBeDefined()
      expect(Array.isArray(profile.subagents), `${profile.slug}: subagents`).toBe(true)
      expect(Array.isArray(profile.learningPath), `${profile.slug}: learningPath`).toBe(true)
      expect(Array.isArray(profile.rulesFiles), `${profile.slug}: rulesFiles`).toBe(true)
    }
  })

  describe('Font size progression', () => {
    it('seniors have larger fonts than teens at the same skill level', () => {
      const skillLevels: SkillLevel[] = ['beginner', 'intermediate', 'expert']

      for (const skill of skillLevels) {
        const teen = PROFILES.find(p => p.ageGroup === 'teen' && p.skillLevel === skill)!
        const senior = PROFILES.find(p => p.ageGroup === 'senior' && p.skillLevel === skill)!

        expect(
          senior.ui.fontSize,
          `Senior ${skill} font (${senior.ui.fontSize}) should be > teen ${skill} font (${teen.ui.fontSize})`
        ).toBeGreaterThan(teen.ui.fontSize)
      }
    })
  })

  describe('MCP count progression', () => {
    it('experts have more MCP servers than beginners within the same age group', () => {
      const ageGroups: AgeGroup[] = ['teen', 'young_adult', 'professional', 'pre_senior', 'senior']

      for (const age of ageGroups) {
        const beginner = PROFILES.find(p => p.ageGroup === age && p.skillLevel === 'beginner')!
        const expert = PROFILES.find(p => p.ageGroup === age && p.skillLevel === 'expert')!

        expect(
          expert.mcpServers.length,
          `${age} expert MCPs (${expert.mcpServers.length}) should be >= ${age} beginner MCPs (${beginner.mcpServers.length})`
        ).toBeGreaterThanOrEqual(beginner.mcpServers.length)
      }
    })
  })

  describe('Touch target minimums', () => {
    it('no profile has touch targets below 44px', () => {
      for (const profile of PROFILES) {
        expect(
          profile.ui.touchTargetMin,
          `${profile.slug} has touchTargetMin ${profile.ui.touchTargetMin} which is below 44`
        ).toBeGreaterThanOrEqual(44)
      }
    })
  })

  describe('Senior expert contrast', () => {
    it('senior-expert has "high" contrast (not "enhanced")', () => {
      const seniorExpert = getProfile('senior-expert')
      expect(seniorExpert).toBeDefined()
      expect(seniorExpert!.ui.contrast).toBe('high')
    })
  })
})

describe('getProfile', () => {
  it('returns a profile by slug', () => {
    const p = getProfile('teen-beginner')
    expect(p).toBeDefined()
    expect(p!.slug).toBe('teen-beginner')
    expect(p!.ageGroup).toBe('teen')
    expect(p!.skillLevel).toBe('beginner')
  })

  it('returns undefined for unknown slug', () => {
    const p = getProfile('nonexistent-profile')
    expect(p).toBeUndefined()
  })
})

describe('getProfilesByAge', () => {
  it('returns 3 profiles for each age group', () => {
    const ageGroups: AgeGroup[] = ['teen', 'young_adult', 'professional', 'pre_senior', 'senior']

    for (const age of ageGroups) {
      const profiles = getProfilesByAge(age)
      expect(profiles, `${age} should have 3 profiles`).toHaveLength(3)
      for (const p of profiles) {
        expect(p.ageGroup).toBe(age)
      }
    }
  })
})

describe('getProfilesBySkill', () => {
  it('returns 5 profiles for each skill level', () => {
    const skillLevels: SkillLevel[] = ['beginner', 'intermediate', 'expert']

    for (const skill of skillLevels) {
      const profiles = getProfilesBySkill(skill)
      expect(profiles, `${skill} should have 5 profiles`).toHaveLength(5)
      for (const p of profiles) {
        expect(p.skillLevel).toBe(skill)
      }
    }
  })
})