mirror of
https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git
synced 2026-09-18 18:16:16 +00:00
* fix(cli): install all 7 skills via uipro init, not just the orchestrator `uipro init` rendered only the orchestrator (ui-ux-pro-max) and never delivered the 6 sibling skills (banner-design, brand, design, design-system, slides, ui-styling), so users got 1 of 7 skills (#362). - sync-assets.mjs: bundle the 6 sub-skills into cli/assets/skills/ as static copies (source of truth: .claude/skills/), with sync + check coverage. Excludes ui-styling/canvas-fonts (~5.8MB of TTF) and __pycache__/.pyc cruft — a skill registers from its SKILL.md, not its fonts — so the bundle adds ~0.9MB, not ~6.6MB. - template.ts: after rendering the orchestrator, install each bundled sub-skill as a sibling. The skills parent is derived from the platform's skillPath (skills/ for most, prompts/ for copilot, steering/ for kiro) rather than hardcoded. - uninstall.ts: remove the sub-skills too. Verified: check:assets in sync, tsc passes, and a per-platform install harness delivers all 7 skills to the correct parent dir with no fonts. Closes #362 * fix(cli): filter excluded files from target side of check:assets check:assets filtered sourceFiles with isExcludedAssetFile but not targetFiles, so a stray cli/assets/scripts/__pycache__/*.pyc (generated by a local Python run) was reported as an "extra asset file" and failed the gate. Apply the same predicate to targetFiles in both the dirsToSync and sub-skill loops. Verified: check:assets now passes with __pycache__/*.pyc present in the target tree; typecheck passes. * fix(cli): uninstall from each platform's real skills dir, not hardcoded skills/ removeSkillDir() hardcoded <folder>/skills/<name>, but the installer places skills under each platform config's skillPath parent — copilot in .github/prompts/, kiro in .kiro/steering/. So uninstall left those platforms' skills (orchestrator + sub-skills) behind. Derive the install parent from loadPlatformConfig(aiType).folderStructure (same source the installer uses), and keep the legacy <folder>/skills/ cleanup (incl. .shared/) for older installs. Deduped via a Set. Verified: typecheck passes; an install+uninstall harness removes all 7 skills with zero leftovers for claude (.claude/skills), copilot (.github/prompts) and kiro (.kiro/steering). * fix(cli): re-sync bundled sub-skills after #385 stripped ckm- names #385 merged to main and removed the ckm- prefix from the six .claude/skills/*/SKILL.md name fields. This branch's bundled copies under cli/assets/skills/ still carried the old ckm- names, so after the PR merges with main the source no longer matched the bundle and the check-asset-sync CI gate failed (stale asset file: skills/*/SKILL.md). Merge main and regenerate the bundle so cli/assets/skills matches the current .claude/skills source of truth. check:assets and typecheck pass.
267 lines
9.7 KiB
Python
267 lines
9.7 KiB
Python
"""Tests for shadcn_add.py"""
|
|
|
|
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, mock_open, patch
|
|
|
|
import pytest
|
|
|
|
# Add parent directory to path for imports
|
|
import sys
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from shadcn_add import ShadcnInstaller
|
|
|
|
|
|
class TestShadcnInstaller:
|
|
"""Test ShadcnInstaller class."""
|
|
|
|
@pytest.fixture
|
|
def temp_project(self, tmp_path):
|
|
"""Create temporary project structure."""
|
|
project_root = tmp_path / "test-project"
|
|
project_root.mkdir()
|
|
|
|
# Create components.json
|
|
components_json = project_root / "components.json"
|
|
components_json.write_text(
|
|
json.dumps({
|
|
"style": "new-york",
|
|
"aliases": {
|
|
"components": "@/components",
|
|
"utils": "@/lib/utils"
|
|
}
|
|
})
|
|
)
|
|
|
|
# Create components directory
|
|
ui_dir = project_root / "components" / "ui"
|
|
ui_dir.mkdir(parents=True)
|
|
|
|
return project_root
|
|
|
|
def test_init_default_project_root(self):
|
|
"""Test initialization with default project root."""
|
|
installer = ShadcnInstaller()
|
|
assert installer.project_root == Path.cwd()
|
|
assert installer.dry_run is False
|
|
|
|
def test_init_custom_project_root(self, tmp_path):
|
|
"""Test initialization with custom project root."""
|
|
installer = ShadcnInstaller(project_root=tmp_path)
|
|
assert installer.project_root == tmp_path
|
|
|
|
def test_init_dry_run(self):
|
|
"""Test initialization with dry run mode."""
|
|
installer = ShadcnInstaller(dry_run=True)
|
|
assert installer.dry_run is True
|
|
|
|
def test_check_shadcn_config_exists(self, temp_project):
|
|
"""Test checking for existing shadcn config."""
|
|
installer = ShadcnInstaller(project_root=temp_project)
|
|
assert installer.check_shadcn_config() is True
|
|
|
|
def test_check_shadcn_config_not_exists(self, tmp_path):
|
|
"""Test checking for non-existent shadcn config."""
|
|
installer = ShadcnInstaller(project_root=tmp_path)
|
|
assert installer.check_shadcn_config() is False
|
|
|
|
def test_get_installed_components_empty(self, temp_project):
|
|
"""Test getting installed components when none exist."""
|
|
installer = ShadcnInstaller(project_root=temp_project)
|
|
installed = installer.get_installed_components()
|
|
assert installed == []
|
|
|
|
def test_get_installed_components_with_files(self, temp_project):
|
|
"""Test getting installed components when files exist."""
|
|
ui_dir = temp_project / "components" / "ui"
|
|
|
|
# Create component files
|
|
(ui_dir / "button.tsx").write_text("export const Button = () => {}")
|
|
(ui_dir / "card.tsx").write_text("export const Card = () => {}")
|
|
|
|
installer = ShadcnInstaller(project_root=temp_project)
|
|
installed = installer.get_installed_components()
|
|
|
|
assert sorted(installed) == ["button", "card"]
|
|
|
|
def test_get_installed_components_no_config(self, tmp_path):
|
|
"""Test getting installed components without config."""
|
|
installer = ShadcnInstaller(project_root=tmp_path)
|
|
installed = installer.get_installed_components()
|
|
assert installed == []
|
|
|
|
def test_add_components_no_components(self, temp_project):
|
|
"""Test adding components with empty list."""
|
|
installer = ShadcnInstaller(project_root=temp_project)
|
|
success, message = installer.add_components([])
|
|
|
|
assert success is False
|
|
assert "No components specified" in message
|
|
|
|
def test_add_components_no_config(self, tmp_path):
|
|
"""Test adding components without shadcn config."""
|
|
installer = ShadcnInstaller(project_root=tmp_path)
|
|
success, message = installer.add_components(["button"])
|
|
|
|
assert success is False
|
|
assert "not initialized" in message
|
|
|
|
def test_add_components_already_installed(self, temp_project):
|
|
"""Test adding components that are already installed."""
|
|
ui_dir = temp_project / "components" / "ui"
|
|
(ui_dir / "button.tsx").write_text("export const Button = () => {}")
|
|
|
|
installer = ShadcnInstaller(project_root=temp_project)
|
|
success, message = installer.add_components(["button"])
|
|
|
|
assert success is False
|
|
assert "already installed" in message
|
|
assert "button" in message
|
|
|
|
def test_add_components_with_overwrite(self, temp_project):
|
|
"""Test adding components with overwrite flag."""
|
|
ui_dir = temp_project / "components" / "ui"
|
|
(ui_dir / "button.tsx").write_text("export const Button = () => {}")
|
|
|
|
installer = ShadcnInstaller(project_root=temp_project)
|
|
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(
|
|
stdout="Component added successfully",
|
|
returncode=0
|
|
)
|
|
|
|
success, message = installer.add_components(["button"], overwrite=True)
|
|
|
|
assert success is True
|
|
assert "Successfully added" in message
|
|
mock_run.assert_called_once()
|
|
|
|
# Verify --overwrite flag was passed
|
|
call_args = mock_run.call_args[0][0]
|
|
assert "--overwrite" in call_args
|
|
|
|
def test_add_components_dry_run(self, temp_project):
|
|
"""Test adding components in dry run mode."""
|
|
installer = ShadcnInstaller(project_root=temp_project, dry_run=True)
|
|
success, message = installer.add_components(["button", "card"])
|
|
|
|
assert success is True
|
|
assert "Would run:" in message
|
|
assert "button" in message
|
|
assert "card" in message
|
|
|
|
@patch("subprocess.run")
|
|
def test_add_components_success(self, mock_run, temp_project):
|
|
"""Test successful component addition."""
|
|
mock_run.return_value = MagicMock(
|
|
stdout="Components added successfully",
|
|
stderr="",
|
|
returncode=0
|
|
)
|
|
|
|
installer = ShadcnInstaller(project_root=temp_project)
|
|
success, message = installer.add_components(["button", "card"])
|
|
|
|
assert success is True
|
|
assert "Successfully added" in message
|
|
assert "button" in message
|
|
assert "card" in message
|
|
|
|
# Verify correct command was called
|
|
mock_run.assert_called_once()
|
|
call_args = mock_run.call_args[0][0]
|
|
assert call_args[:3] == ["npx", "shadcn@latest", "add"]
|
|
assert "button" in call_args
|
|
assert "card" in call_args
|
|
|
|
@patch("subprocess.run")
|
|
def test_add_components_subprocess_error(self, mock_run, temp_project):
|
|
"""Test component addition with subprocess error."""
|
|
mock_run.side_effect = subprocess.CalledProcessError(
|
|
1, "cmd", stderr="Error occurred"
|
|
)
|
|
|
|
installer = ShadcnInstaller(project_root=temp_project)
|
|
success, message = installer.add_components(["button"])
|
|
|
|
assert success is False
|
|
assert "Failed to add" in message
|
|
|
|
@patch("subprocess.run")
|
|
def test_add_components_npx_not_found(self, mock_run, temp_project):
|
|
"""Test component addition when npx is not found."""
|
|
mock_run.side_effect = FileNotFoundError()
|
|
|
|
installer = ShadcnInstaller(project_root=temp_project)
|
|
success, message = installer.add_components(["button"])
|
|
|
|
assert success is False
|
|
assert "npx not found" in message
|
|
|
|
def test_add_all_components_no_config(self, tmp_path):
|
|
"""Test adding all components without config."""
|
|
installer = ShadcnInstaller(project_root=tmp_path)
|
|
success, message = installer.add_all_components()
|
|
|
|
assert success is False
|
|
assert "not initialized" in message
|
|
|
|
def test_add_all_components_dry_run(self, temp_project):
|
|
"""Test adding all components in dry run mode."""
|
|
installer = ShadcnInstaller(project_root=temp_project, dry_run=True)
|
|
success, message = installer.add_all_components()
|
|
|
|
assert success is True
|
|
assert "Would run:" in message
|
|
assert "--all" in message
|
|
|
|
@patch("subprocess.run")
|
|
def test_add_all_components_success(self, mock_run, temp_project):
|
|
"""Test successful addition of all components."""
|
|
mock_run.return_value = MagicMock(
|
|
stdout="All components added",
|
|
returncode=0
|
|
)
|
|
|
|
installer = ShadcnInstaller(project_root=temp_project)
|
|
success, message = installer.add_all_components()
|
|
|
|
assert success is True
|
|
assert "Successfully added all" in message
|
|
|
|
# Verify --all flag was passed
|
|
call_args = mock_run.call_args[0][0]
|
|
assert "--all" in call_args
|
|
|
|
def test_list_installed_no_config(self, tmp_path):
|
|
"""Test listing installed components without config."""
|
|
installer = ShadcnInstaller(project_root=tmp_path)
|
|
success, message = installer.list_installed()
|
|
|
|
assert success is False
|
|
assert "not initialized" in message
|
|
|
|
def test_list_installed_empty(self, temp_project):
|
|
"""Test listing installed components when none exist."""
|
|
installer = ShadcnInstaller(project_root=temp_project)
|
|
success, message = installer.list_installed()
|
|
|
|
assert success is True
|
|
assert "No components installed" in message
|
|
|
|
def test_list_installed_with_components(self, temp_project):
|
|
"""Test listing installed components when they exist."""
|
|
ui_dir = temp_project / "components" / "ui"
|
|
(ui_dir / "button.tsx").write_text("export const Button = () => {}")
|
|
(ui_dir / "card.tsx").write_text("export const Card = () => {}")
|
|
|
|
installer = ShadcnInstaller(project_root=temp_project)
|
|
success, message = installer.list_installed()
|
|
|
|
assert success is True
|
|
assert "button" in message
|
|
assert "card" in message
|