From 53b5e3020912c3b64ab589d7e14296a1639e1fb0 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 13:27:55 +0000 Subject: [PATCH] CodeRabbit Generated Unit Tests: Add generated unit tests --- packages/cli/test/commands/init.test.ts | 84 +-- .../cli/test/utils/preset-presets.test.ts | 339 ++++++++++++ packages/cli/test/utils/preset.test.ts | 507 ++++++++++++++++++ packages/cli/test/utils/registry-api.test.ts | 124 ++++- packages/cli/test/utils/schema.test.ts | 40 +- 5 files changed, 1034 insertions(+), 60 deletions(-) create mode 100644 packages/cli/test/utils/preset-presets.test.ts create mode 100644 packages/cli/test/utils/preset.test.ts diff --git a/packages/cli/test/commands/init.test.ts b/packages/cli/test/commands/init.test.ts index 9d200635d..c62919dd8 100644 --- a/packages/cli/test/commands/init.test.ts +++ b/packages/cli/test/commands/init.test.ts @@ -3,9 +3,9 @@ import { addDependency } from 'nypm' import path from 'pathe' import { afterEach, describe, expect, it, vi } from 'vitest' -import { initOptionsSchema, resolvePreset, runInit } from '../../src/commands/init' +import { initOptionsSchema, runInit } from '../../src/commands/init' +import { DEFAULT_PRESETS } from '../../src/preset/presets' import * as registry from '../../src/registry' -import { PRESETS } from '../../src/registry/constants' import { getConfig } from '../../src/utils/get-config' vi.mock('nypm') @@ -14,9 +14,6 @@ vi.mock('fs/promises', () => ({ mkdir: vi.fn(), })) vi.mock('ora') -vi.mock('ofetch', () => ({ - ofetch: vi.fn(), -})) it.skip('init config-full', async () => { vi.spyOn(registry, 'getRegistryBaseColor').mockResolvedValue({ @@ -153,6 +150,26 @@ describe('initOptionsSchema', () => { expect(result.preset).toBeUndefined() }) + it('accepts preset as boolean true (interactive mode)', () => { + const result = initOptionsSchema.parse({ ...base, preset: true }) + expect(result.preset).toBe(true) + }) + + it('accepts preset as boolean false', () => { + const result = initOptionsSchema.parse({ ...base, preset: false }) + expect(result.preset).toBe(false) + }) + + it('accepts a preset code string', () => { + const result = initOptionsSchema.parse({ ...base, preset: 'nova' }) + expect(result.preset).toBe('nova') + }) + + it('accepts installStyleIndex boolean flag', () => { + const result = initOptionsSchema.parse({ ...base, installStyleIndex: false }) + expect(result.installStyleIndex).toBe(false) + }) + it('accepts valid style', () => { const result = initOptionsSchema.parse({ ...base, style: 'vega' }) expect(result.style).toBe('vega') @@ -172,47 +189,32 @@ describe('initOptionsSchema', () => { }) }) -describe('resolvePreset', () => { - it('resolves a built-in preset by name', async () => { - const preset = await resolvePreset('reka-vega') - expect(preset).toBeDefined() - expect(preset?.name).toBe('reka-vega') - expect(preset?.base).toBe('reka') - expect(preset?.style).toBe('vega') +describe('default presets', () => { + it('exposes all built-in presets', () => { + const names = Object.keys(DEFAULT_PRESETS) + expect(names).toContain('vega') + expect(names).toContain('nova') + expect(names).toContain('maia') + expect(names).toContain('lyra') + expect(names).toContain('mira') + expect(names).toContain('luma') }) - it('returns null for an unknown preset name', async () => { - const preset = await resolvePreset('not-a-preset') - expect(preset).toBeNull() - }) - - it('resolves all built-in presets by name', async () => { - for (const p of PRESETS) { - const resolved = await resolvePreset(p.name) - expect(resolved).toBeDefined() - expect(resolved?.name).toBe(p.name) + it('each preset has a complete design system config', () => { + for (const preset of Object.values(DEFAULT_PRESETS)) { + expect(preset.base).toBeDefined() + expect(preset.style).toBeDefined() + expect(preset.baseColor).toBeDefined() + expect(preset.theme).toBeDefined() + expect(preset.iconLibrary).toBeDefined() + expect(preset.font).toBeDefined() + expect(preset.menuAccent).toBeDefined() + expect(preset.menuColor).toBeDefined() + expect(preset.radius).toBeDefined() } }) - - it('fetches preset from a URL', async () => { - const mockPreset = PRESETS[0] - const { ofetch } = await import('ofetch') - vi.mocked(ofetch).mockResolvedValue(mockPreset) - - const preset = await resolvePreset('https://example.com/preset.json') - expect(preset).toBeDefined() - expect(preset?.name).toBe(mockPreset.name) - }) - - it('returns null when URL fetch fails', async () => { - const { ofetch } = await import('ofetch') - vi.mocked(ofetch).mockRejectedValue(new Error('Network error')) - - const preset = await resolvePreset('https://example.com/bad-preset.json') - expect(preset).toBeNull() - }) }) afterEach(() => { vi.resetAllMocks() -}) +}) \ No newline at end of file diff --git a/packages/cli/test/utils/preset-presets.test.ts b/packages/cli/test/utils/preset-presets.test.ts new file mode 100644 index 000000000..58f38f1dc --- /dev/null +++ b/packages/cli/test/utils/preset-presets.test.ts @@ -0,0 +1,339 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { + DEFAULT_PRESETS, + promptForBase, + resolveCreateUrl, + resolveInitUrl, +} from '../../src/preset/presets' +import { SHADCN_VUE_URL } from '../../src/registry/constants' + +describe('DEFAULT_PRESETS', () => { + it('exports exactly 6 named presets', () => { + expect(Object.keys(DEFAULT_PRESETS)).toHaveLength(6) + }) + + it('contains all expected preset names', () => { + const names = Object.keys(DEFAULT_PRESETS) + expect(names).toContain('vega') + expect(names).toContain('nova') + expect(names).toContain('maia') + expect(names).toContain('lyra') + expect(names).toContain('mira') + expect(names).toContain('luma') + }) + + it('every preset specifies base as "reka"', () => { + for (const preset of Object.values(DEFAULT_PRESETS)) { + expect(preset.base).toBe('reka') + } + }) + + it('every preset has rtl set to false by default', () => { + for (const preset of Object.values(DEFAULT_PRESETS)) { + expect(preset.rtl).toBe(false) + } + }) + + it('every preset has all required design system fields', () => { + for (const [name, preset] of Object.entries(DEFAULT_PRESETS)) { + expect(preset.title, `${name} title`).toBeDefined() + expect(preset.description, `${name} description`).toBeDefined() + expect(preset.base, `${name} base`).toBeDefined() + expect(preset.style, `${name} style`).toBeDefined() + expect(preset.baseColor, `${name} baseColor`).toBeDefined() + expect(preset.theme, `${name} theme`).toBeDefined() + expect(preset.iconLibrary, `${name} iconLibrary`).toBeDefined() + expect(preset.font, `${name} font`).toBeDefined() + expect(preset.fontHeading, `${name} fontHeading`).toBeDefined() + expect(preset.menuAccent, `${name} menuAccent`).toBeDefined() + expect(preset.menuColor, `${name} menuColor`).toBeDefined() + expect(preset.radius, `${name} radius`).toBeDefined() + } + }) + + it('vega preset uses lucide and inter', () => { + const vega = DEFAULT_PRESETS.vega + expect(vega.iconLibrary).toBe('lucide') + expect(vega.font).toBe('inter') + expect(vega.style).toBe('vega') + }) + + it('nova preset uses lucide and geist-sans', () => { + const nova = DEFAULT_PRESETS.nova + expect(nova.iconLibrary).toBe('lucide') + expect(nova.font).toBe('geist-sans') + expect(nova.style).toBe('nova') + }) + + it('maia preset uses hugeicons and figtree', () => { + const maia = DEFAULT_PRESETS.maia + expect(maia.iconLibrary).toBe('hugeicons') + expect(maia.font).toBe('figtree') + expect(maia.style).toBe('maia') + }) + + it('lyra preset uses phosphor and jetbrains-mono', () => { + const lyra = DEFAULT_PRESETS.lyra + expect(lyra.iconLibrary).toBe('phosphor') + expect(lyra.font).toBe('jetbrains-mono') + expect(lyra.style).toBe('lyra') + }) + + it('mira preset uses hugeicons and inter', () => { + const mira = DEFAULT_PRESETS.mira + expect(mira.iconLibrary).toBe('hugeicons') + expect(mira.font).toBe('inter') + expect(mira.style).toBe('mira') + }) + + it('luma preset uses lucide and inter', () => { + const luma = DEFAULT_PRESETS.luma + expect(luma.iconLibrary).toBe('lucide') + expect(luma.font).toBe('inter') + expect(luma.style).toBe('luma') + }) + + it('every preset has menuAccent set to "subtle" or "bold"', () => { + for (const preset of Object.values(DEFAULT_PRESETS)) { + expect(['subtle', 'bold']).toContain(preset.menuAccent) + } + }) + + it('every preset has menuColor set to "default" or "inverted"', () => { + for (const preset of Object.values(DEFAULT_PRESETS)) { + expect(['default', 'inverted']).toContain(preset.menuColor) + } + }) +}) + +describe('resolveCreateUrl', () => { + it('returns the /create URL with no params when called without arguments', () => { + const url = resolveCreateUrl() + expect(url).toBe(`${SHADCN_VUE_URL}/create`) + }) + + it('returns the /create URL with no params when called with empty object', () => { + const url = resolveCreateUrl({}) + expect(url).toBe(`${SHADCN_VUE_URL}/create`) + }) + + it('includes the command param when provided', () => { + const url = resolveCreateUrl({ command: 'init' }) + const parsed = new URL(url) + expect(parsed.searchParams.get('command')).toBe('init') + }) + + it('includes the template param when provided', () => { + const url = resolveCreateUrl({ template: 'nuxt' }) + const parsed = new URL(url) + expect(parsed.searchParams.get('template')).toBe('nuxt') + }) + + it('includes the base param when provided', () => { + const url = resolveCreateUrl({ base: 'reka' }) + const parsed = new URL(url) + expect(parsed.searchParams.get('base')).toBe('reka') + }) + + it('adds rtl=true param when rtl is true', () => { + const url = resolveCreateUrl({ rtl: true }) + const parsed = new URL(url) + expect(parsed.searchParams.get('rtl')).toBe('true') + }) + + it('does NOT add rtl param when rtl is false', () => { + const url = resolveCreateUrl({ rtl: false }) + const parsed = new URL(url) + expect(parsed.searchParams.has('rtl')).toBe(false) + }) + + it('does NOT add rtl param when rtl is undefined', () => { + const url = resolveCreateUrl({ command: 'init' }) + const parsed = new URL(url) + expect(parsed.searchParams.has('rtl')).toBe(false) + }) + + it('combines all params correctly', () => { + const url = resolveCreateUrl({ + command: 'init', + template: 'nuxt', + rtl: true, + base: 'reka', + }) + const parsed = new URL(url) + expect(parsed.searchParams.get('command')).toBe('init') + expect(parsed.searchParams.get('template')).toBe('nuxt') + expect(parsed.searchParams.get('rtl')).toBe('true') + expect(parsed.searchParams.get('base')).toBe('reka') + }) + + it('uses the SHADCN_VUE_URL base', () => { + const url = resolveCreateUrl() + expect(url.startsWith(SHADCN_VUE_URL)).toBe(true) + }) +}) + +describe('resolveInitUrl', () => { + const basePreset = { + base: 'reka', + style: 'vega', + baseColor: 'neutral', + theme: 'neutral', + iconLibrary: 'lucide', + font: 'inter', + rtl: false, + menuAccent: 'subtle', + menuColor: 'default', + radius: 'default', + } + + it('builds a URL pointing to SHADCN_VUE_URL/init', () => { + const url = resolveInitUrl(basePreset) + const parsed = new URL(url) + expect(parsed.pathname).toBe('/init') + expect(url.startsWith(SHADCN_VUE_URL)).toBe(true) + }) + + it('includes all required preset params', () => { + const url = resolveInitUrl(basePreset) + const parsed = new URL(url) + expect(parsed.searchParams.get('base')).toBe('reka') + expect(parsed.searchParams.get('style')).toBe('vega') + expect(parsed.searchParams.get('baseColor')).toBe('neutral') + expect(parsed.searchParams.get('theme')).toBe('neutral') + expect(parsed.searchParams.get('iconLibrary')).toBe('lucide') + expect(parsed.searchParams.get('font')).toBe('inter') + expect(parsed.searchParams.get('menuAccent')).toBe('subtle') + expect(parsed.searchParams.get('menuColor')).toBe('default') + expect(parsed.searchParams.get('radius')).toBe('default') + }) + + it('always includes track=1', () => { + const url = resolveInitUrl(basePreset) + const parsed = new URL(url) + expect(parsed.searchParams.get('track')).toBe('1') + }) + + it('sets rtl=false when rtl is false', () => { + const url = resolveInitUrl({ ...basePreset, rtl: false }) + const parsed = new URL(url) + expect(parsed.searchParams.get('rtl')).toBe('false') + }) + + it('sets rtl=true when rtl is true', () => { + const url = resolveInitUrl({ ...basePreset, rtl: true }) + const parsed = new URL(url) + expect(parsed.searchParams.get('rtl')).toBe('true') + }) + + it('does NOT include fontHeading param when fontHeading is "inherit"', () => { + const url = resolveInitUrl({ ...basePreset, fontHeading: 'inherit' }) + const parsed = new URL(url) + expect(parsed.searchParams.has('fontHeading')).toBe(false) + }) + + it('does NOT include fontHeading param when fontHeading is omitted', () => { + const url = resolveInitUrl(basePreset) + const parsed = new URL(url) + expect(parsed.searchParams.has('fontHeading')).toBe(false) + }) + + it('includes fontHeading param when fontHeading is a specific font', () => { + const url = resolveInitUrl({ ...basePreset, fontHeading: 'geist-sans' }) + const parsed = new URL(url) + expect(parsed.searchParams.get('fontHeading')).toBe('geist-sans') + }) + + it('includes preset param when options.preset is provided', () => { + const url = resolveInitUrl(basePreset, { preset: 'a0' }) + const parsed = new URL(url) + expect(parsed.searchParams.get('preset')).toBe('a0') + }) + + it('does NOT include preset param when options.preset is not provided', () => { + const url = resolveInitUrl(basePreset) + const parsed = new URL(url) + expect(parsed.searchParams.has('preset')).toBe(false) + }) + + it('includes template param when options.template is provided', () => { + const url = resolveInitUrl(basePreset, { template: 'nuxt' }) + const parsed = new URL(url) + expect(parsed.searchParams.get('template')).toBe('nuxt') + }) + + it('does NOT include template param when options.template is not provided', () => { + const url = resolveInitUrl(basePreset) + const parsed = new URL(url) + expect(parsed.searchParams.has('template')).toBe(false) + }) + + it('combines preset and template options correctly', () => { + const url = resolveInitUrl(basePreset, { preset: 'a0', template: 'nuxt' }) + const parsed = new URL(url) + expect(parsed.searchParams.get('preset')).toBe('a0') + expect(parsed.searchParams.get('template')).toBe('nuxt') + }) + + it('produces different URLs for different styles', () => { + const url1 = resolveInitUrl({ ...basePreset, style: 'vega' }) + const url2 = resolveInitUrl({ ...basePreset, style: 'nova' }) + expect(url1).not.toBe(url2) + }) +}) + +describe('promptForBase', () => { + it('returns "reka" (single supported base)', async () => { + const base = await promptForBase() + expect(base).toBe('reka') + }) + + it('always resolves to "reka" regardless of call context', async () => { + const results = await Promise.all([ + promptForBase(), + promptForBase(), + promptForBase(), + ]) + for (const result of results) { + expect(result).toBe('reka') + } + }) +}) + +describe('SHADCN_VUE_URL from constants', () => { + const originalEnv = process.env.SHADCN_VUE_URL + + afterEach(() => { + // restore env + if (originalEnv === undefined) { + delete process.env.SHADCN_VUE_URL + } + else { + process.env.SHADCN_VUE_URL = originalEnv + } + }) + + it('resolveCreateUrl uses SHADCN_VUE_URL when it points to /create', () => { + // SHADCN_VUE_URL is imported at module load time, so we verify + // the constant used is the expected default value + const url = resolveCreateUrl() + expect(url).toContain('/create') + expect(url.startsWith('https://shadcn-vue.com') || url.startsWith('http')).toBe(true) + }) + + it('resolveInitUrl uses SHADCN_VUE_URL when it points to /init', () => { + const url = resolveInitUrl({ + base: 'reka', + style: 'vega', + baseColor: 'neutral', + theme: 'neutral', + iconLibrary: 'lucide', + font: 'inter', + rtl: false, + menuAccent: 'subtle', + menuColor: 'default', + radius: 'default', + }) + expect(url).toContain('/init') + }) +}) \ No newline at end of file diff --git a/packages/cli/test/utils/preset.test.ts b/packages/cli/test/utils/preset.test.ts new file mode 100644 index 000000000..d62710c32 --- /dev/null +++ b/packages/cli/test/utils/preset.test.ts @@ -0,0 +1,507 @@ +import { describe, expect, it } from 'vitest' +import { + decodePreset, + DEFAULT_PRESET_CONFIG, + encodePreset, + fromBase62, + generateRandomConfig, + generateRandomPreset, + isPresetCode, + isValidPreset, + PRESET_BASE_COLORS, + PRESET_BASES, + PRESET_CHART_COLORS, + PRESET_FONT_HEADINGS, + PRESET_FONTS, + PRESET_ICON_LIBRARIES, + PRESET_MENU_ACCENTS, + PRESET_MENU_COLORS, + PRESET_RADII, + PRESET_STYLES, + PRESET_THEMES, + toBase62, +} from '../../src/preset/preset' + +describe('toBase62', () => { + it('converts 0 to "0"', () => { + expect(toBase62(0)).toBe('0') + }) + + it('converts single digit values', () => { + expect(toBase62(1)).toBe('1') + expect(toBase62(9)).toBe('9') + expect(toBase62(10)).toBe('A') + expect(toBase62(35)).toBe('Z') + expect(toBase62(36)).toBe('a') + expect(toBase62(61)).toBe('z') + }) + + it('converts 62 to "10" (base62 two-digit)', () => { + expect(toBase62(62)).toBe('10') + }) + + it('converts larger numbers', () => { + expect(toBase62(62 * 62)).toBe('100') + expect(toBase62(62 * 62 + 62 + 1)).toBe('111') + }) + + it('roundtrips with fromBase62', () => { + const nums = [0, 1, 61, 62, 100, 3844, 99999, 1234567] + for (const n of nums) { + expect(fromBase62(toBase62(n))).toBe(n) + } + }) +}) + +describe('fromBase62', () => { + it('converts "0" to 0', () => { + expect(fromBase62('0')).toBe(0) + }) + + it('converts single character codes', () => { + expect(fromBase62('A')).toBe(10) + expect(fromBase62('Z')).toBe(35) + expect(fromBase62('a')).toBe(36) + expect(fromBase62('z')).toBe(61) + }) + + it('converts multi-character codes', () => { + expect(fromBase62('10')).toBe(62) + expect(fromBase62('100')).toBe(62 * 62) + }) + + it('returns -1 for a string containing an invalid character', () => { + expect(fromBase62('!')).toBe(-1) + expect(fromBase62('a!')).toBe(-1) + expect(fromBase62(' ')).toBe(-1) + expect(fromBase62('-')).toBe(-1) + }) + + it('returns 0 for empty string', () => { + expect(fromBase62('')).toBe(0) + }) +}) + +describe('DEFAULT_PRESET_CONFIG', () => { + it('has all required fields', () => { + expect(DEFAULT_PRESET_CONFIG.base).toBeDefined() + expect(DEFAULT_PRESET_CONFIG.style).toBeDefined() + expect(DEFAULT_PRESET_CONFIG.baseColor).toBeDefined() + expect(DEFAULT_PRESET_CONFIG.theme).toBeDefined() + expect(DEFAULT_PRESET_CONFIG.chartColor).toBeDefined() + expect(DEFAULT_PRESET_CONFIG.iconLibrary).toBeDefined() + expect(DEFAULT_PRESET_CONFIG.font).toBeDefined() + expect(DEFAULT_PRESET_CONFIG.fontHeading).toBeDefined() + expect(DEFAULT_PRESET_CONFIG.radius).toBeDefined() + expect(DEFAULT_PRESET_CONFIG.menuAccent).toBeDefined() + expect(DEFAULT_PRESET_CONFIG.menuColor).toBeDefined() + }) + + it('uses first value from each value array as default', () => { + expect(DEFAULT_PRESET_CONFIG.menuColor).toBe(PRESET_MENU_COLORS[0]) + expect(DEFAULT_PRESET_CONFIG.menuAccent).toBe(PRESET_MENU_ACCENTS[0]) + expect(DEFAULT_PRESET_CONFIG.radius).toBe(PRESET_RADII[0]) + expect(DEFAULT_PRESET_CONFIG.font).toBe(PRESET_FONTS[0]) + expect(DEFAULT_PRESET_CONFIG.fontHeading).toBe(PRESET_FONT_HEADINGS[0]) + expect(DEFAULT_PRESET_CONFIG.iconLibrary).toBe(PRESET_ICON_LIBRARIES[0]) + expect(DEFAULT_PRESET_CONFIG.theme).toBe(PRESET_THEMES[0]) + expect(DEFAULT_PRESET_CONFIG.baseColor).toBe(PRESET_BASE_COLORS[0]) + expect(DEFAULT_PRESET_CONFIG.style).toBe(PRESET_STYLES[0]) + expect(DEFAULT_PRESET_CONFIG.base).toBe(PRESET_BASES[0]) + }) + + it('default font is "inter"', () => { + expect(DEFAULT_PRESET_CONFIG.font).toBe('inter') + }) + + it('default fontHeading is "inherit"', () => { + expect(DEFAULT_PRESET_CONFIG.fontHeading).toBe('inherit') + }) + + it('default base is "reka"', () => { + expect(DEFAULT_PRESET_CONFIG.base).toBe('reka') + }) +}) + +describe('encodePreset', () => { + it('encodes the default config to a code starting with version "a"', () => { + const code = encodePreset(DEFAULT_PRESET_CONFIG) + expect(code).toMatch(/^a/) + }) + + it('encodes a partial config using defaults for missing fields', () => { + const code = encodePreset({ style: 'nova' }) + const decoded = decodePreset(code) + expect(decoded?.style).toBe('nova') + // other fields should be defaults + expect(decoded?.font).toBe(DEFAULT_PRESET_CONFIG.font) + expect(decoded?.base).toBe(DEFAULT_PRESET_CONFIG.base) + }) + + it('produces the same code for the same config', () => { + const code1 = encodePreset(DEFAULT_PRESET_CONFIG) + const code2 = encodePreset(DEFAULT_PRESET_CONFIG) + expect(code1).toBe(code2) + }) + + it('produces different codes for different configs', () => { + const code1 = encodePreset({ style: 'vega' }) + const code2 = encodePreset({ style: 'nova' }) + expect(code1).not.toBe(code2) + }) + + it('encodes all styles distinctly', () => { + const codes = PRESET_STYLES.map(style => encodePreset({ style })) + const unique = new Set(codes) + expect(unique.size).toBe(PRESET_STYLES.length) + }) + + it('encodes all themes distinctly', () => { + const codes = PRESET_THEMES.map(theme => encodePreset({ theme })) + const unique = new Set(codes) + expect(unique.size).toBe(PRESET_THEMES.length) + }) + + it('encodes all icon libraries distinctly', () => { + const codes = PRESET_ICON_LIBRARIES.map(iconLibrary => + encodePreset({ iconLibrary }), + ) + const unique = new Set(codes) + expect(unique.size).toBe(PRESET_ICON_LIBRARIES.length) + }) + + it('encodes all fonts distinctly', () => { + const codes = PRESET_FONTS.map(font => encodePreset({ font })) + const unique = new Set(codes) + expect(unique.size).toBe(PRESET_FONTS.length) + }) + + it('encodes all radii distinctly', () => { + const codes = PRESET_RADII.map(radius => encodePreset({ radius })) + const unique = new Set(codes) + expect(unique.size).toBe(PRESET_RADII.length) + }) + + it('returns a code with length >= 2', () => { + const code = encodePreset(DEFAULT_PRESET_CONFIG) + expect(code.length).toBeGreaterThanOrEqual(2) + }) + + it('gracefully uses default for unknown style value', () => { + // @ts-expect-error — testing runtime behavior with invalid value + const code = encodePreset({ style: 'nonexistent' }) + const decoded = decodePreset(code) + expect(decoded?.style).toBe(DEFAULT_PRESET_CONFIG.style) + }) +}) + +describe('decodePreset', () => { + it('returns null for empty string', () => { + expect(decodePreset('')).toBeNull() + }) + + it('returns null for a single character', () => { + expect(decodePreset('a')).toBeNull() + }) + + it('returns null for an invalid version character', () => { + expect(decodePreset('z0')).toBeNull() + expect(decodePreset('b0')).toBeNull() + expect(decodePreset('10')).toBeNull() + }) + + it('returns null when base62 part contains invalid characters', () => { + expect(decodePreset('a!')).toBeNull() + expect(decodePreset('a ')).toBeNull() + }) + + it('decodes a freshly encoded default config back to the default', () => { + const code = encodePreset(DEFAULT_PRESET_CONFIG) + const decoded = decodePreset(code) + expect(decoded).not.toBeNull() + expect(decoded?.style).toBe(DEFAULT_PRESET_CONFIG.style) + expect(decoded?.baseColor).toBe(DEFAULT_PRESET_CONFIG.baseColor) + expect(decoded?.theme).toBe(DEFAULT_PRESET_CONFIG.theme) + expect(decoded?.font).toBe(DEFAULT_PRESET_CONFIG.font) + expect(decoded?.fontHeading).toBe(DEFAULT_PRESET_CONFIG.fontHeading) + expect(decoded?.radius).toBe(DEFAULT_PRESET_CONFIG.radius) + expect(decoded?.iconLibrary).toBe(DEFAULT_PRESET_CONFIG.iconLibrary) + expect(decoded?.menuColor).toBe(DEFAULT_PRESET_CONFIG.menuColor) + expect(decoded?.menuAccent).toBe(DEFAULT_PRESET_CONFIG.menuAccent) + expect(decoded?.base).toBe(DEFAULT_PRESET_CONFIG.base) + expect(decoded?.chartColor).toBe(DEFAULT_PRESET_CONFIG.chartColor) + }) + + it('roundtrips any fully specified config', () => { + const config = { + base: 'reka' as const, + style: 'lyra' as const, + baseColor: 'zinc' as const, + theme: 'blue' as const, + chartColor: 'emerald' as const, + iconLibrary: 'phosphor' as const, + font: 'jetbrains-mono' as const, + fontHeading: 'figtree' as const, + radius: 'large' as const, + menuAccent: 'bold' as const, + menuColor: 'inverted' as const, + } + const decoded = decodePreset(encodePreset(config)) + expect(decoded).toMatchObject(config) + }) + + it('roundtrips each style correctly', () => { + for (const style of PRESET_STYLES) { + const decoded = decodePreset(encodePreset({ style })) + expect(decoded?.style).toBe(style) + } + }) + + it('roundtrips each font correctly', () => { + for (const font of PRESET_FONTS) { + const decoded = decodePreset(encodePreset({ font })) + expect(decoded?.font).toBe(font) + } + }) + + it('roundtrips each menuColor correctly', () => { + for (const menuColor of PRESET_MENU_COLORS) { + const decoded = decodePreset(encodePreset({ menuColor })) + expect(decoded?.menuColor).toBe(menuColor) + } + }) + + it('roundtrips each radius correctly', () => { + for (const radius of PRESET_RADII) { + const decoded = decodePreset(encodePreset({ radius })) + expect(decoded?.radius).toBe(radius) + } + }) + + it('roundtrips each baseColor correctly', () => { + for (const baseColor of PRESET_BASE_COLORS) { + const decoded = decodePreset(encodePreset({ baseColor })) + expect(decoded?.baseColor).toBe(baseColor) + } + }) + + it('decodes "a0" as a valid code with all fields at their defaults', () => { + const decoded = decodePreset('a0') + expect(decoded).not.toBeNull() + expect(decoded?.menuColor).toBe(PRESET_MENU_COLORS[0]) + expect(decoded?.menuAccent).toBe(PRESET_MENU_ACCENTS[0]) + }) +}) + +describe('isPresetCode', () => { + it('returns false for empty string', () => { + expect(isPresetCode('')).toBe(false) + }) + + it('returns false for a single character', () => { + expect(isPresetCode('a')).toBe(false) + }) + + it('returns false when length > 10', () => { + // 11 chars starting with valid version + expect(isPresetCode('a12345678901')).toBe(false) + }) + + it('returns false for invalid version prefix', () => { + expect(isPresetCode('z0')).toBe(false) + expect(isPresetCode('b00')).toBe(false) + expect(isPresetCode('!a0')).toBe(false) + }) + + it('returns false when body contains non-base62 characters', () => { + expect(isPresetCode('a!')).toBe(false) + expect(isPresetCode('a ')).toBe(false) + expect(isPresetCode('a-')).toBe(false) + }) + + it('returns true for the smallest valid code "a0"', () => { + expect(isPresetCode('a0')).toBe(true) + }) + + it('returns true for a freshly encoded default preset', () => { + const code = encodePreset(DEFAULT_PRESET_CONFIG) + expect(isPresetCode(code)).toBe(true) + }) + + it('returns true for all encoded presets', () => { + const configs = PRESET_STYLES.map(style => encodePreset({ style })) + for (const code of configs) { + expect(isPresetCode(code)).toBe(true) + } + }) + + it('returns false for plain preset names like "vega"', () => { + expect(isPresetCode('vega')).toBe(false) + }) + + it('returns false for URLs', () => { + expect(isPresetCode('https://example.com/preset')).toBe(false) + }) + + it('accepts codes up to 10 characters', () => { + // construct exactly 10 chars: 'a' + 9 base62 chars + const code = 'a' + '0'.repeat(9) + expect(isPresetCode(code)).toBe(true) + }) +}) + +describe('isValidPreset', () => { + it('returns true for a valid preset code', () => { + const code = encodePreset(DEFAULT_PRESET_CONFIG) + expect(isValidPreset(code)).toBe(true) + }) + + it('returns false for an empty string', () => { + expect(isValidPreset('')).toBe(false) + }) + + it('returns false for an invalid version prefix', () => { + expect(isValidPreset('z0')).toBe(false) + }) + + it('returns false for a single character', () => { + expect(isValidPreset('a')).toBe(false) + }) + + it('returns true for "a0" (all defaults)', () => { + expect(isValidPreset('a0')).toBe(true) + }) +}) + +describe('generateRandomConfig', () => { + it('returns a config with all required fields', () => { + const config = generateRandomConfig() + expect(config.base).toBeDefined() + expect(config.style).toBeDefined() + expect(config.baseColor).toBeDefined() + expect(config.theme).toBeDefined() + expect(config.chartColor).toBeDefined() + expect(config.iconLibrary).toBeDefined() + expect(config.font).toBeDefined() + expect(config.fontHeading).toBeDefined() + expect(config.radius).toBeDefined() + expect(config.menuAccent).toBeDefined() + expect(config.menuColor).toBeDefined() + }) + + it('returns values within the known value arrays', () => { + const config = generateRandomConfig() + expect(PRESET_BASES).toContain(config.base) + expect(PRESET_STYLES).toContain(config.style) + expect(PRESET_BASE_COLORS).toContain(config.baseColor) + expect(PRESET_THEMES).toContain(config.theme) + expect(PRESET_CHART_COLORS).toContain(config.chartColor) + expect(PRESET_ICON_LIBRARIES).toContain(config.iconLibrary) + expect(PRESET_FONTS).toContain(config.font) + expect(PRESET_FONT_HEADINGS).toContain(config.fontHeading) + expect(PRESET_RADII).toContain(config.radius) + expect(PRESET_MENU_ACCENTS).toContain(config.menuAccent) + expect(PRESET_MENU_COLORS).toContain(config.menuColor) + }) + + it('can be encoded and decoded', () => { + const config = generateRandomConfig() + const decoded = decodePreset(encodePreset(config)) + expect(decoded).not.toBeNull() + expect(decoded?.style).toBe(config.style) + expect(decoded?.font).toBe(config.font) + }) +}) + +describe('generateRandomPreset', () => { + it('returns a string that passes isPresetCode', () => { + const code = generateRandomPreset() + expect(isPresetCode(code)).toBe(true) + }) + + it('returns a decodable code', () => { + const code = generateRandomPreset() + const decoded = decodePreset(code) + expect(decoded).not.toBeNull() + }) + + it('does not always return the same value (randomness check)', () => { + const codes = new Set(Array.from({ length: 20 }, () => generateRandomPreset())) + // With 20 samples from many combinations, we expect multiple distinct values + expect(codes.size).toBeGreaterThan(1) + }) +}) + +describe('preset value arrays', () => { + it('PRESET_STYLES contains expected styles', () => { + expect(PRESET_STYLES).toContain('vega') + expect(PRESET_STYLES).toContain('nova') + expect(PRESET_STYLES).toContain('maia') + expect(PRESET_STYLES).toContain('lyra') + expect(PRESET_STYLES).toContain('mira') + expect(PRESET_STYLES).toContain('luma') + }) + + it('PRESET_BASE_COLORS contains expected colors', () => { + expect(PRESET_BASE_COLORS).toContain('neutral') + expect(PRESET_BASE_COLORS).toContain('stone') + expect(PRESET_BASE_COLORS).toContain('zinc') + expect(PRESET_BASE_COLORS).toContain('mauve') + expect(PRESET_BASE_COLORS).toContain('olive') + expect(PRESET_BASE_COLORS).toContain('mist') + expect(PRESET_BASE_COLORS).toContain('taupe') + }) + + it('PRESET_FONTS contains all expected fonts', () => { + expect(PRESET_FONTS).toContain('inter') + expect(PRESET_FONTS).toContain('geist-sans') + expect(PRESET_FONTS).toContain('noto-sans') + expect(PRESET_FONTS).toContain('nunito-sans') + expect(PRESET_FONTS).toContain('figtree') + expect(PRESET_FONTS).toContain('roboto') + expect(PRESET_FONTS).toContain('raleway') + expect(PRESET_FONTS).toContain('dm-sans') + expect(PRESET_FONTS).toContain('public-sans') + expect(PRESET_FONTS).toContain('outfit') + expect(PRESET_FONTS).toContain('jetbrains-mono') + }) + + it('PRESET_FONT_HEADINGS starts with "inherit" and includes all fonts', () => { + expect(PRESET_FONT_HEADINGS[0]).toBe('inherit') + for (const font of PRESET_FONTS) { + expect(PRESET_FONT_HEADINGS).toContain(font) + } + }) + + it('PRESET_MENU_COLORS contains all expected values', () => { + expect(PRESET_MENU_COLORS).toContain('default') + expect(PRESET_MENU_COLORS).toContain('inverted') + expect(PRESET_MENU_COLORS).toContain('default-translucent') + expect(PRESET_MENU_COLORS).toContain('inverted-translucent') + }) + + it('PRESET_MENU_ACCENTS contains "subtle" and "bold"', () => { + expect(PRESET_MENU_ACCENTS).toContain('subtle') + expect(PRESET_MENU_ACCENTS).toContain('bold') + }) + + it('PRESET_RADII contains expected values', () => { + expect(PRESET_RADII).toContain('default') + expect(PRESET_RADII).toContain('none') + expect(PRESET_RADII).toContain('small') + expect(PRESET_RADII).toContain('medium') + expect(PRESET_RADII).toContain('large') + }) + + it('PRESET_ICON_LIBRARIES contains expected libraries', () => { + expect(PRESET_ICON_LIBRARIES).toContain('lucide') + expect(PRESET_ICON_LIBRARIES).toContain('tabler') + expect(PRESET_ICON_LIBRARIES).toContain('hugeicons') + expect(PRESET_ICON_LIBRARIES).toContain('phosphor') + expect(PRESET_ICON_LIBRARIES).toContain('remixicon') + }) + + it('PRESET_CHART_COLORS is the same reference as PRESET_THEMES', () => { + expect(PRESET_CHART_COLORS).toBe(PRESET_THEMES) + }) +}) \ No newline at end of file diff --git a/packages/cli/test/utils/registry-api.test.ts b/packages/cli/test/utils/registry-api.test.ts index 7ac59170c..486999848 100644 --- a/packages/cli/test/utils/registry-api.test.ts +++ b/packages/cli/test/utils/registry-api.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import { getRegistryBase, getRegistryBases, + getRegistryBaseColors, getRegistryFont, getRegistryFonts, getRegistryIconLibraries, @@ -11,6 +12,7 @@ import { getRegistryVisualStyle, getRegistryVisualStyles, } from '../../src/registry/api' +import { BASE_COLORS, SHADCN_VUE_URL } from '../../src/registry/constants' describe('registry API', () => { describe('getRegistryBases', () => { @@ -145,6 +147,19 @@ describe('registry API', () => { expect(fontNames).toContain('figtree') expect(fontNames).toContain('jetbrains-mono') }) + + it('includes all new fonts added in this PR', () => { + const fonts = getRegistryFonts() + const fontNames = fonts.map(f => f.name) + expect(fontNames).toContain('geist-sans') + expect(fontNames).toContain('noto-sans') + expect(fontNames).toContain('nunito-sans') + expect(fontNames).toContain('roboto') + expect(fontNames).toContain('raleway') + expect(fontNames).toContain('dm-sans') + expect(fontNames).toContain('public-sans') + expect(fontNames).toContain('outfit') + }) }) describe('getRegistryFont', () => { @@ -165,6 +180,25 @@ describe('registry API', () => { const unknown = getRegistryFont('unknown-font') expect(unknown).toBeUndefined() }) + + it('returns geist-sans font by name', () => { + const geist = getRegistryFont('geist-sans') + expect(geist).toBeDefined() + expect(geist?.name).toBe('geist-sans') + expect(geist?.label).toBe('Geist') + }) + + it('returns dm-sans font by name', () => { + const dmSans = getRegistryFont('dm-sans') + expect(dmSans).toBeDefined() + expect(dmSans?.name).toBe('dm-sans') + }) + + it('returns outfit font by name', () => { + const outfit = getRegistryFont('outfit') + expect(outfit).toBeDefined() + expect(outfit?.name).toBe('outfit') + }) }) describe('getRegistryPresets', () => { @@ -178,12 +212,12 @@ describe('registry API', () => { it('includes all expected presets', () => { const presets = getRegistryPresets() const presetNames = presets.map(p => p.name) - expect(presetNames).toContain('reka-vega') - expect(presetNames).toContain('reka-nova') - expect(presetNames).toContain('reka-maia') - expect(presetNames).toContain('reka-lyra') - expect(presetNames).toContain('reka-mira') - expect(presetNames).toContain('reka-luma') + expect(presetNames).toContain('vega') + expect(presetNames).toContain('nova') + expect(presetNames).toContain('maia') + expect(presetNames).toContain('lyra') + expect(presetNames).toContain('mira') + expect(presetNames).toContain('luma') }) it('all presets have complete configuration', () => { @@ -205,9 +239,9 @@ describe('registry API', () => { describe('getRegistryPreset', () => { it('returns vega preset by name', () => { - const vega = getRegistryPreset('reka-vega') + const vega = getRegistryPreset('vega') expect(vega).toBeDefined() - expect(vega?.name).toBe('reka-vega') + expect(vega?.name).toBe('vega') expect(vega?.base).toBe('reka') expect(vega?.style).toBe('vega') expect(vega?.iconLibrary).toBe('lucide') @@ -215,32 +249,32 @@ describe('registry API', () => { }) it('returns nova preset by name', () => { - const nova = getRegistryPreset('reka-nova') + const nova = getRegistryPreset('nova') expect(nova).toBeDefined() - expect(nova?.name).toBe('reka-nova') + expect(nova?.name).toBe('nova') expect(nova?.style).toBe('nova') - expect(nova?.iconLibrary).toBe('hugeicons') + expect(nova?.iconLibrary).toBe('lucide') }) it('returns lyra preset by name', () => { - const lyra = getRegistryPreset('reka-lyra') + const lyra = getRegistryPreset('lyra') expect(lyra).toBeDefined() - expect(lyra?.name).toBe('reka-lyra') + expect(lyra?.name).toBe('lyra') expect(lyra?.style).toBe('lyra') expect(lyra?.font).toBe('jetbrains-mono') }) it('returns mira preset by name', () => { - const mira = getRegistryPreset('reka-mira') + const mira = getRegistryPreset('mira') expect(mira).toBeDefined() - expect(mira?.name).toBe('reka-mira') + expect(mira?.name).toBe('mira') expect(mira?.style).toBe('mira') }) it('returns luma preset by name', () => { - const luma = getRegistryPreset('reka-luma') + const luma = getRegistryPreset('luma') expect(luma).toBeDefined() - expect(luma?.name).toBe('reka-luma') + expect(luma?.name).toBe('luma') expect(luma?.style).toBe('luma') expect(luma?.iconLibrary).toBe('lucide') expect(luma?.font).toBe('inter') @@ -293,4 +327,58 @@ describe('registry API', () => { } }) }) -}) + + describe('getRegistryBaseColors', () => { + it('returns the BASE_COLORS array', async () => { + const colors = await getRegistryBaseColors() + expect(colors).toBeDefined() + expect(Array.isArray(colors)).toBe(true) + expect(colors.length).toBeGreaterThan(0) + }) + + it('contains "neutral" base color', async () => { + const colors = await getRegistryBaseColors() + const names = colors.map(c => c.name) + expect(names).toContain('neutral') + }) + + it('contains new base colors added in this PR', async () => { + const colors = await getRegistryBaseColors() + const names = colors.map(c => c.name) + expect(names).toContain('mauve') + expect(names).toContain('olive') + expect(names).toContain('mist') + expect(names).toContain('taupe') + }) + + it('does not contain removed legacy colors', async () => { + const colors = await getRegistryBaseColors() + const names = colors.map(c => c.name) + expect(names).not.toContain('gray') + expect(names).not.toContain('slate') + }) + }) + + describe('constants', () => { + it('SHADCN_VUE_URL defaults to "https://shadcn-vue.com"', () => { + // If overridden by env var, skip; otherwise check default. + if (!process.env.SHADCN_VUE_URL) { + expect(SHADCN_VUE_URL).toBe('https://shadcn-vue.com') + } + else { + expect(SHADCN_VUE_URL).toBe(process.env.SHADCN_VUE_URL) + } + }) + + it('BASE_COLORS contains 7 entries', () => { + expect(BASE_COLORS).toHaveLength(7) + }) + + it('BASE_COLORS entries all have name and label properties', () => { + for (const color of BASE_COLORS) { + expect(color.name).toBeDefined() + expect(color.label).toBeDefined() + } + }) + }) +}) \ No newline at end of file diff --git a/packages/cli/test/utils/schema.test.ts b/packages/cli/test/utils/schema.test.ts index 8823be3ca..817d43f04 100644 --- a/packages/cli/test/utils/schema.test.ts +++ b/packages/cli/test/utils/schema.test.ts @@ -112,6 +112,44 @@ describe('rawConfigSchema', () => { expect(() => rawConfigSchema.parse(validInverted)).not.toThrow() }) + it('accepts "default-translucent" menuColor value', () => { + const config = { + style: 'vega', + typescript: true, + tailwind: { + css: 'src/globals.css', + baseColor: 'neutral', + }, + menuColor: 'default-translucent', + aliases: { + components: '@/components', + utils: '@/lib/utils', + }, + } + + const result = rawConfigSchema.parse(config) + expect(result.menuColor).toBe('default-translucent') + }) + + it('accepts "inverted-translucent" menuColor value', () => { + const config = { + style: 'vega', + typescript: true, + tailwind: { + css: 'src/globals.css', + baseColor: 'neutral', + }, + menuColor: 'inverted-translucent', + aliases: { + components: '@/components', + utils: '@/lib/utils', + }, + } + + const result = rawConfigSchema.parse(config) + expect(result.menuColor).toBe('inverted-translucent') + }) + it('validates menuAccent enum values', () => { const validSubtle = { style: 'vega', @@ -364,4 +402,4 @@ describe('presetSchema', () => { const result = presetSchema.parse(presetWithInvertedColor) expect(result.menuColor).toBe('inverted') }) -}) +}) \ No newline at end of file