stella-coder 3.9.2 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/.mcp.json +11 -0
  2. package/.opencode/skills/banner-design/SKILL.md +196 -0
  3. package/.opencode/skills/banner-design/references/banner-sizes-and-styles.md +118 -0
  4. package/.opencode/skills/brand/SKILL.md +97 -0
  5. package/.opencode/skills/brand/references/approval-checklist.md +169 -0
  6. package/.opencode/skills/brand/references/asset-organization.md +157 -0
  7. package/.opencode/skills/brand/references/brand-guideline-template.md +140 -0
  8. package/.opencode/skills/brand/references/color-palette-management.md +186 -0
  9. package/.opencode/skills/brand/references/consistency-checklist.md +94 -0
  10. package/.opencode/skills/brand/references/logo-usage-rules.md +185 -0
  11. package/.opencode/skills/brand/references/messaging-framework.md +85 -0
  12. package/.opencode/skills/brand/references/typography-specifications.md +214 -0
  13. package/.opencode/skills/brand/references/update.md +118 -0
  14. package/.opencode/skills/brand/references/visual-identity.md +96 -0
  15. package/.opencode/skills/brand/references/voice-framework.md +88 -0
  16. package/.opencode/skills/brand/scripts/extract-colors.cjs +341 -0
  17. package/.opencode/skills/brand/scripts/inject-brand-context.cjs +349 -0
  18. package/.opencode/skills/brand/scripts/sync-brand-to-tokens.cjs +248 -0
  19. package/.opencode/skills/brand/scripts/tests/test_sync_brand_to_tokens.py +52 -0
  20. package/.opencode/skills/brand/scripts/validate-asset.cjs +387 -0
  21. package/.opencode/skills/brand/templates/brand-guidelines-starter.md +275 -0
  22. package/.opencode/skills/design/SKILL.md +313 -0
  23. package/.opencode/skills/design/data/cip/deliverables.csv +51 -0
  24. package/.opencode/skills/design/data/cip/industries.csv +21 -0
  25. package/.opencode/skills/design/data/cip/mockup-contexts.csv +21 -0
  26. package/.opencode/skills/design/data/cip/styles.csv +21 -0
  27. package/.opencode/skills/design/data/icon/styles.csv +16 -0
  28. package/.opencode/skills/design/data/logo/colors.csv +56 -0
  29. package/.opencode/skills/design/data/logo/industries.csv +56 -0
  30. package/.opencode/skills/design/data/logo/styles.csv +56 -0
  31. package/.opencode/skills/design/references/banner-sizes-and-styles.md +118 -0
  32. package/.opencode/skills/design/references/cip-deliverable-guide.md +95 -0
  33. package/.opencode/skills/design/references/cip-design.md +121 -0
  34. package/.opencode/skills/design/references/cip-prompt-engineering.md +84 -0
  35. package/.opencode/skills/design/references/cip-style-guide.md +68 -0
  36. package/.opencode/skills/design/references/design-routing.md +207 -0
  37. package/.opencode/skills/design/references/icon-design.md +122 -0
  38. package/.opencode/skills/design/references/logo-color-psychology.md +101 -0
  39. package/.opencode/skills/design/references/logo-design.md +92 -0
  40. package/.opencode/skills/design/references/logo-prompt-engineering.md +158 -0
  41. package/.opencode/skills/design/references/logo-style-guide.md +109 -0
  42. package/.opencode/skills/design/references/slides-copywriting-formulas.md +84 -0
  43. package/.opencode/skills/design/references/slides-create.md +4 -0
  44. package/.opencode/skills/design/references/slides-html-template.md +295 -0
  45. package/.opencode/skills/design/references/slides-layout-patterns.md +137 -0
  46. package/.opencode/skills/design/references/slides-strategies.md +94 -0
  47. package/.opencode/skills/design/references/slides.md +42 -0
  48. package/.opencode/skills/design/references/social-photos-design.md +329 -0
  49. package/.opencode/skills/design/scripts/cip/core.py +215 -0
  50. package/.opencode/skills/design/scripts/cip/generate.py +484 -0
  51. package/.opencode/skills/design/scripts/cip/render-html.py +424 -0
  52. package/.opencode/skills/design/scripts/cip/search.py +127 -0
  53. package/.opencode/skills/design/scripts/icon/generate.py +487 -0
  54. package/.opencode/skills/design/scripts/logo/core.py +175 -0
  55. package/.opencode/skills/design/scripts/logo/generate.py +362 -0
  56. package/.opencode/skills/design/scripts/logo/search.py +114 -0
  57. package/.opencode/skills/design-system/SKILL.md +244 -0
  58. package/.opencode/skills/design-system/data/slide-backgrounds.csv +11 -0
  59. package/.opencode/skills/design-system/data/slide-charts.csv +26 -0
  60. package/.opencode/skills/design-system/data/slide-color-logic.csv +14 -0
  61. package/.opencode/skills/design-system/data/slide-copy.csv +26 -0
  62. package/.opencode/skills/design-system/data/slide-layout-logic.csv +16 -0
  63. package/.opencode/skills/design-system/data/slide-layouts.csv +26 -0
  64. package/.opencode/skills/design-system/data/slide-strategies.csv +16 -0
  65. package/.opencode/skills/design-system/data/slide-typography.csv +15 -0
  66. package/.opencode/skills/design-system/references/component-specs.md +236 -0
  67. package/.opencode/skills/design-system/references/component-tokens.md +214 -0
  68. package/.opencode/skills/design-system/references/primitive-tokens.md +203 -0
  69. package/.opencode/skills/design-system/references/semantic-tokens.md +215 -0
  70. package/.opencode/skills/design-system/references/states-and-variants.md +241 -0
  71. package/.opencode/skills/design-system/references/tailwind-integration.md +251 -0
  72. package/.opencode/skills/design-system/references/token-architecture.md +224 -0
  73. package/.opencode/skills/design-system/scripts/embed-tokens.cjs +99 -0
  74. package/.opencode/skills/design-system/scripts/fetch-background.py +317 -0
  75. package/.opencode/skills/design-system/scripts/generate-slide.py +770 -0
  76. package/.opencode/skills/design-system/scripts/generate-tokens.cjs +205 -0
  77. package/.opencode/skills/design-system/scripts/html-token-validator.py +327 -0
  78. package/.opencode/skills/design-system/scripts/search-slides.py +218 -0
  79. package/.opencode/skills/design-system/scripts/slide-token-validator.py +35 -0
  80. package/.opencode/skills/design-system/scripts/slide_search_core.py +453 -0
  81. package/.opencode/skills/design-system/scripts/tests/test_validate_tokens.py +48 -0
  82. package/.opencode/skills/design-system/scripts/validate-tokens.cjs +246 -0
  83. package/.opencode/skills/design-system/templates/design-tokens-starter.json +143 -0
  84. package/.opencode/skills/slides/SKILL.md +40 -0
  85. package/.opencode/skills/slides/references/copywriting-formulas.md +84 -0
  86. package/.opencode/skills/slides/references/create.md +4 -0
  87. package/.opencode/skills/slides/references/html-template.md +295 -0
  88. package/.opencode/skills/slides/references/layout-patterns.md +137 -0
  89. package/.opencode/skills/slides/references/slide-strategies.md +94 -0
  90. package/.opencode/skills/ui-styling/LICENSE.txt +202 -0
  91. package/.opencode/skills/ui-styling/SKILL.md +324 -0
  92. package/.opencode/skills/ui-styling/references/canvas-design-system.md +320 -0
  93. package/.opencode/skills/ui-styling/references/shadcn-accessibility.md +471 -0
  94. package/.opencode/skills/ui-styling/references/shadcn-components.md +424 -0
  95. package/.opencode/skills/ui-styling/references/shadcn-theming.md +373 -0
  96. package/.opencode/skills/ui-styling/references/tailwind-customization.md +483 -0
  97. package/.opencode/skills/ui-styling/references/tailwind-responsive.md +382 -0
  98. package/.opencode/skills/ui-styling/references/tailwind-utilities.md +455 -0
  99. package/.opencode/skills/ui-styling/scripts/requirements.txt +17 -0
  100. package/.opencode/skills/ui-styling/scripts/shadcn_add.py +308 -0
  101. package/.opencode/skills/ui-styling/scripts/tailwind_config_gen.py +473 -0
  102. package/.opencode/skills/ui-styling/scripts/tests/coverage-ui.json +1 -0
  103. package/.opencode/skills/ui-styling/scripts/tests/requirements.txt +3 -0
  104. package/.opencode/skills/ui-styling/scripts/tests/test_shadcn_add.py +266 -0
  105. package/.opencode/skills/ui-styling/scripts/tests/test_tailwind_config_gen.py +394 -0
  106. package/.opencode/skills/ui-ux-pro-max/SKILL.md +396 -0
  107. package/.opencode/skills/ui-ux-pro-max/data/_sync_all.py +414 -0
  108. package/.opencode/skills/ui-ux-pro-max/data/app-interface.csv +31 -0
  109. package/.opencode/skills/ui-ux-pro-max/data/charts.csv +26 -0
  110. package/.opencode/skills/ui-ux-pro-max/data/colors.csv +193 -0
  111. package/.opencode/skills/ui-ux-pro-max/data/design.csv +1776 -0
  112. package/.opencode/skills/ui-ux-pro-max/data/draft.csv +1779 -0
  113. package/.opencode/skills/ui-ux-pro-max/data/google-fonts.csv +1924 -0
  114. package/.opencode/skills/ui-ux-pro-max/data/icons.csv +106 -0
  115. package/.opencode/skills/ui-ux-pro-max/data/landing.csv +35 -0
  116. package/.opencode/skills/ui-ux-pro-max/data/motion.csv +17 -0
  117. package/.opencode/skills/ui-ux-pro-max/data/products.csv +193 -0
  118. package/.opencode/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  119. package/.opencode/skills/ui-ux-pro-max/data/stacks/angular.csv +51 -0
  120. package/.opencode/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  121. package/.opencode/skills/ui-ux-pro-max/data/stacks/avalonia.csv +57 -0
  122. package/.opencode/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  123. package/.opencode/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  124. package/.opencode/skills/ui-ux-pro-max/data/stacks/javafx.csv +76 -0
  125. package/.opencode/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  126. package/.opencode/skills/ui-ux-pro-max/data/stacks/laravel.csv +51 -0
  127. package/.opencode/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  128. package/.opencode/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +71 -0
  129. package/.opencode/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  130. package/.opencode/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  131. package/.opencode/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  132. package/.opencode/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  133. package/.opencode/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  134. package/.opencode/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  135. package/.opencode/skills/ui-ux-pro-max/data/stacks/threejs.csv +54 -0
  136. package/.opencode/skills/ui-ux-pro-max/data/stacks/uno.csv +60 -0
  137. package/.opencode/skills/ui-ux-pro-max/data/stacks/uwp.csv +56 -0
  138. package/.opencode/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  139. package/.opencode/skills/ui-ux-pro-max/data/stacks/winui.csv +60 -0
  140. package/.opencode/skills/ui-ux-pro-max/data/stacks/wpf.csv +57 -0
  141. package/.opencode/skills/ui-ux-pro-max/data/styles.csv +85 -0
  142. package/.opencode/skills/ui-ux-pro-max/data/typography.csv +75 -0
  143. package/.opencode/skills/ui-ux-pro-max/data/ui-reasoning.csv +162 -0
  144. package/.opencode/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  145. package/.opencode/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-312.pyc +0 -0
  146. package/.opencode/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-312.pyc +0 -0
  147. package/.opencode/skills/ui-ux-pro-max/scripts/core.py +274 -0
  148. package/.opencode/skills/ui-ux-pro-max/scripts/design_system.py +1329 -0
  149. package/.opencode/skills/ui-ux-pro-max/scripts/search.py +127 -0
  150. package/README.md +25 -30
  151. package/STELLA.md +1 -1
  152. package/dist/antimalware/database.mjs +871 -0
  153. package/dist/antimalware/index.mjs +8 -0
  154. package/dist/antimalware/scanner.mjs +591 -0
  155. package/dist/antimalware/ui.mjs +570 -0
  156. package/dist/banner.mjs +46 -0
  157. package/dist/index.mjs +3073 -0
  158. package/dist/markdown.mjs +100 -0
  159. package/dist/sea-config.json +4 -0
  160. package/dist/security.mjs +237 -0
  161. package/dist/theme.mjs +89 -0
  162. package/dist/tools.mjs +3145 -0
  163. package/install-stella.bat +44 -0
  164. package/package.json +5 -4
  165. package/stella-cli/coding-brain.mjs +753 -0
  166. package/stella-cli/index.mjs +931 -15
  167. package/stella-cli/mcp.mjs +296 -0
  168. package/stella-cli/presentations.mjs +1106 -0
  169. package/stella-cli/subagents.mjs +142 -0
  170. package/stella-cli/telegram-bot.mjs +824 -0
  171. package/stella-cli/tg-bg.bat +13 -0
  172. package/stella-cli/tg-server.bat +15 -0
  173. package/stella-cli/tg-server.mjs +116 -0
  174. package/stella-cli/theme.mjs +6 -4
  175. package/create_cats_pptx.py +0 -121
  176. package/hello.py +0 -2
@@ -0,0 +1,266 @@
1
+ """Tests for shadcn_add.py"""
2
+
3
+ import json
4
+ import subprocess
5
+ from pathlib import Path
6
+ from unittest.mock import MagicMock, mock_open, patch
7
+
8
+ import pytest
9
+
10
+ # Add parent directory to path for imports
11
+ import sys
12
+ sys.path.insert(0, str(Path(__file__).parent.parent))
13
+
14
+ from shadcn_add import ShadcnInstaller
15
+
16
+
17
+ class TestShadcnInstaller:
18
+ """Test ShadcnInstaller class."""
19
+
20
+ @pytest.fixture
21
+ def temp_project(self, tmp_path):
22
+ """Create temporary project structure."""
23
+ project_root = tmp_path / "test-project"
24
+ project_root.mkdir()
25
+
26
+ # Create components.json
27
+ components_json = project_root / "components.json"
28
+ components_json.write_text(
29
+ json.dumps({
30
+ "style": "new-york",
31
+ "aliases": {
32
+ "components": "@/components",
33
+ "utils": "@/lib/utils"
34
+ }
35
+ })
36
+ )
37
+
38
+ # Create components directory
39
+ ui_dir = project_root / "components" / "ui"
40
+ ui_dir.mkdir(parents=True)
41
+
42
+ return project_root
43
+
44
+ def test_init_default_project_root(self):
45
+ """Test initialization with default project root."""
46
+ installer = ShadcnInstaller()
47
+ assert installer.project_root == Path.cwd()
48
+ assert installer.dry_run is False
49
+
50
+ def test_init_custom_project_root(self, tmp_path):
51
+ """Test initialization with custom project root."""
52
+ installer = ShadcnInstaller(project_root=tmp_path)
53
+ assert installer.project_root == tmp_path
54
+
55
+ def test_init_dry_run(self):
56
+ """Test initialization with dry run mode."""
57
+ installer = ShadcnInstaller(dry_run=True)
58
+ assert installer.dry_run is True
59
+
60
+ def test_check_shadcn_config_exists(self, temp_project):
61
+ """Test checking for existing shadcn config."""
62
+ installer = ShadcnInstaller(project_root=temp_project)
63
+ assert installer.check_shadcn_config() is True
64
+
65
+ def test_check_shadcn_config_not_exists(self, tmp_path):
66
+ """Test checking for non-existent shadcn config."""
67
+ installer = ShadcnInstaller(project_root=tmp_path)
68
+ assert installer.check_shadcn_config() is False
69
+
70
+ def test_get_installed_components_empty(self, temp_project):
71
+ """Test getting installed components when none exist."""
72
+ installer = ShadcnInstaller(project_root=temp_project)
73
+ installed = installer.get_installed_components()
74
+ assert installed == []
75
+
76
+ def test_get_installed_components_with_files(self, temp_project):
77
+ """Test getting installed components when files exist."""
78
+ ui_dir = temp_project / "components" / "ui"
79
+
80
+ # Create component files
81
+ (ui_dir / "button.tsx").write_text("export const Button = () => {}")
82
+ (ui_dir / "card.tsx").write_text("export const Card = () => {}")
83
+
84
+ installer = ShadcnInstaller(project_root=temp_project)
85
+ installed = installer.get_installed_components()
86
+
87
+ assert sorted(installed) == ["button", "card"]
88
+
89
+ def test_get_installed_components_no_config(self, tmp_path):
90
+ """Test getting installed components without config."""
91
+ installer = ShadcnInstaller(project_root=tmp_path)
92
+ installed = installer.get_installed_components()
93
+ assert installed == []
94
+
95
+ def test_add_components_no_components(self, temp_project):
96
+ """Test adding components with empty list."""
97
+ installer = ShadcnInstaller(project_root=temp_project)
98
+ success, message = installer.add_components([])
99
+
100
+ assert success is False
101
+ assert "No components specified" in message
102
+
103
+ def test_add_components_no_config(self, tmp_path):
104
+ """Test adding components without shadcn config."""
105
+ installer = ShadcnInstaller(project_root=tmp_path)
106
+ success, message = installer.add_components(["button"])
107
+
108
+ assert success is False
109
+ assert "not initialized" in message
110
+
111
+ def test_add_components_already_installed(self, temp_project):
112
+ """Test adding components that are already installed."""
113
+ ui_dir = temp_project / "components" / "ui"
114
+ (ui_dir / "button.tsx").write_text("export const Button = () => {}")
115
+
116
+ installer = ShadcnInstaller(project_root=temp_project)
117
+ success, message = installer.add_components(["button"])
118
+
119
+ assert success is False
120
+ assert "already installed" in message
121
+ assert "button" in message
122
+
123
+ def test_add_components_with_overwrite(self, temp_project):
124
+ """Test adding components with overwrite flag."""
125
+ ui_dir = temp_project / "components" / "ui"
126
+ (ui_dir / "button.tsx").write_text("export const Button = () => {}")
127
+
128
+ installer = ShadcnInstaller(project_root=temp_project)
129
+
130
+ with patch("subprocess.run") as mock_run:
131
+ mock_run.return_value = MagicMock(
132
+ stdout="Component added successfully",
133
+ returncode=0
134
+ )
135
+
136
+ success, message = installer.add_components(["button"], overwrite=True)
137
+
138
+ assert success is True
139
+ assert "Successfully added" in message
140
+ mock_run.assert_called_once()
141
+
142
+ # Verify --overwrite flag was passed
143
+ call_args = mock_run.call_args[0][0]
144
+ assert "--overwrite" in call_args
145
+
146
+ def test_add_components_dry_run(self, temp_project):
147
+ """Test adding components in dry run mode."""
148
+ installer = ShadcnInstaller(project_root=temp_project, dry_run=True)
149
+ success, message = installer.add_components(["button", "card"])
150
+
151
+ assert success is True
152
+ assert "Would run:" in message
153
+ assert "button" in message
154
+ assert "card" in message
155
+
156
+ @patch("subprocess.run")
157
+ def test_add_components_success(self, mock_run, temp_project):
158
+ """Test successful component addition."""
159
+ mock_run.return_value = MagicMock(
160
+ stdout="Components added successfully",
161
+ stderr="",
162
+ returncode=0
163
+ )
164
+
165
+ installer = ShadcnInstaller(project_root=temp_project)
166
+ success, message = installer.add_components(["button", "card"])
167
+
168
+ assert success is True
169
+ assert "Successfully added" in message
170
+ assert "button" in message
171
+ assert "card" in message
172
+
173
+ # Verify correct command was called
174
+ mock_run.assert_called_once()
175
+ call_args = mock_run.call_args[0][0]
176
+ assert call_args[:3] == ["npx", "shadcn@latest", "add"]
177
+ assert "button" in call_args
178
+ assert "card" in call_args
179
+
180
+ @patch("subprocess.run")
181
+ def test_add_components_subprocess_error(self, mock_run, temp_project):
182
+ """Test component addition with subprocess error."""
183
+ mock_run.side_effect = subprocess.CalledProcessError(
184
+ 1, "cmd", stderr="Error occurred"
185
+ )
186
+
187
+ installer = ShadcnInstaller(project_root=temp_project)
188
+ success, message = installer.add_components(["button"])
189
+
190
+ assert success is False
191
+ assert "Failed to add" in message
192
+
193
+ @patch("subprocess.run")
194
+ def test_add_components_npx_not_found(self, mock_run, temp_project):
195
+ """Test component addition when npx is not found."""
196
+ mock_run.side_effect = FileNotFoundError()
197
+
198
+ installer = ShadcnInstaller(project_root=temp_project)
199
+ success, message = installer.add_components(["button"])
200
+
201
+ assert success is False
202
+ assert "npx not found" in message
203
+
204
+ def test_add_all_components_no_config(self, tmp_path):
205
+ """Test adding all components without config."""
206
+ installer = ShadcnInstaller(project_root=tmp_path)
207
+ success, message = installer.add_all_components()
208
+
209
+ assert success is False
210
+ assert "not initialized" in message
211
+
212
+ def test_add_all_components_dry_run(self, temp_project):
213
+ """Test adding all components in dry run mode."""
214
+ installer = ShadcnInstaller(project_root=temp_project, dry_run=True)
215
+ success, message = installer.add_all_components()
216
+
217
+ assert success is True
218
+ assert "Would run:" in message
219
+ assert "--all" in message
220
+
221
+ @patch("subprocess.run")
222
+ def test_add_all_components_success(self, mock_run, temp_project):
223
+ """Test successful addition of all components."""
224
+ mock_run.return_value = MagicMock(
225
+ stdout="All components added",
226
+ returncode=0
227
+ )
228
+
229
+ installer = ShadcnInstaller(project_root=temp_project)
230
+ success, message = installer.add_all_components()
231
+
232
+ assert success is True
233
+ assert "Successfully added all" in message
234
+
235
+ # Verify --all flag was passed
236
+ call_args = mock_run.call_args[0][0]
237
+ assert "--all" in call_args
238
+
239
+ def test_list_installed_no_config(self, tmp_path):
240
+ """Test listing installed components without config."""
241
+ installer = ShadcnInstaller(project_root=tmp_path)
242
+ success, message = installer.list_installed()
243
+
244
+ assert success is False
245
+ assert "not initialized" in message
246
+
247
+ def test_list_installed_empty(self, temp_project):
248
+ """Test listing installed components when none exist."""
249
+ installer = ShadcnInstaller(project_root=temp_project)
250
+ success, message = installer.list_installed()
251
+
252
+ assert success is True
253
+ assert "No components installed" in message
254
+
255
+ def test_list_installed_with_components(self, temp_project):
256
+ """Test listing installed components when they exist."""
257
+ ui_dir = temp_project / "components" / "ui"
258
+ (ui_dir / "button.tsx").write_text("export const Button = () => {}")
259
+ (ui_dir / "card.tsx").write_text("export const Card = () => {}")
260
+
261
+ installer = ShadcnInstaller(project_root=temp_project)
262
+ success, message = installer.list_installed()
263
+
264
+ assert success is True
265
+ assert "button" in message
266
+ assert "card" in message
@@ -0,0 +1,394 @@
1
+ """Tests for tailwind_config_gen.py"""
2
+
3
+ import shutil
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ # Add parent directory to path for imports
10
+ import sys
11
+ sys.path.insert(0, str(Path(__file__).parent.parent))
12
+
13
+ from tailwind_config_gen import TailwindConfigGenerator
14
+
15
+
16
+ class TestTailwindConfigGenerator:
17
+ """Test TailwindConfigGenerator class."""
18
+
19
+ def test_init_default_typescript(self):
20
+ """Test initialization with default settings."""
21
+ generator = TailwindConfigGenerator()
22
+ assert generator.typescript is True
23
+ assert generator.framework == "react"
24
+
25
+ def test_init_javascript(self):
26
+ """Test initialization for JavaScript config."""
27
+ generator = TailwindConfigGenerator(typescript=False)
28
+ assert generator.typescript is False
29
+
30
+ def test_init_framework(self):
31
+ """Test initialization with different frameworks."""
32
+ for framework in ["react", "vue", "svelte", "nextjs"]:
33
+ generator = TailwindConfigGenerator(framework=framework)
34
+ assert generator.framework == framework
35
+
36
+ def test_default_output_path_typescript(self):
37
+ """Test default output path for TypeScript."""
38
+ generator = TailwindConfigGenerator(typescript=True)
39
+ assert generator.output_path.name == "tailwind.config.ts"
40
+
41
+ def test_default_output_path_javascript(self):
42
+ """Test default output path for JavaScript."""
43
+ generator = TailwindConfigGenerator(typescript=False)
44
+ assert generator.output_path.name == "tailwind.config.js"
45
+
46
+ def test_custom_output_path(self, tmp_path):
47
+ """Test custom output path."""
48
+ custom_path = tmp_path / "custom-config.ts"
49
+ generator = TailwindConfigGenerator(output_path=custom_path)
50
+ assert generator.output_path == custom_path
51
+
52
+ def test_base_config_structure(self):
53
+ """Test base configuration structure."""
54
+ generator = TailwindConfigGenerator()
55
+ config = generator.config
56
+
57
+ assert "darkMode" in config
58
+ assert "content" in config
59
+ assert "theme" in config
60
+ assert "plugins" in config
61
+ assert "extend" in config["theme"]
62
+
63
+ def test_default_content_paths_react(self):
64
+ """Test default content paths for React."""
65
+ generator = TailwindConfigGenerator(framework="react")
66
+ paths = generator.config["content"]
67
+
68
+ assert any("src/**/*.{js,jsx,ts,tsx}" in p for p in paths)
69
+ assert any("index.html" in p for p in paths)
70
+
71
+ def test_default_content_paths_nextjs(self):
72
+ """Test default content paths for Next.js."""
73
+ generator = TailwindConfigGenerator(framework="nextjs")
74
+ paths = generator.config["content"]
75
+
76
+ assert any("app/**" in p for p in paths)
77
+ assert any("pages/**" in p for p in paths)
78
+ assert any("components/**" in p for p in paths)
79
+
80
+ def test_default_content_paths_vue(self):
81
+ """Test default content paths for Vue."""
82
+ generator = TailwindConfigGenerator(framework="vue")
83
+ paths = generator.config["content"]
84
+
85
+ assert any("vue" in p for p in paths)
86
+
87
+ def test_add_colors(self):
88
+ """Test adding custom colors."""
89
+ generator = TailwindConfigGenerator()
90
+ colors = {
91
+ "brand": "#3b82f6",
92
+ "accent": "#8b5cf6"
93
+ }
94
+ generator.add_colors(colors)
95
+
96
+ assert "colors" in generator.config["theme"]["extend"]
97
+ assert generator.config["theme"]["extend"]["colors"]["brand"] == "#3b82f6"
98
+ assert generator.config["theme"]["extend"]["colors"]["accent"] == "#8b5cf6"
99
+
100
+ def test_add_colors_multiple_times(self):
101
+ """Test adding colors multiple times."""
102
+ generator = TailwindConfigGenerator()
103
+
104
+ generator.add_colors({"brand": "#3b82f6"})
105
+ generator.add_colors({"accent": "#8b5cf6"})
106
+
107
+ colors = generator.config["theme"]["extend"]["colors"]
108
+ assert "brand" in colors
109
+ assert "accent" in colors
110
+
111
+ def test_add_color_palette(self):
112
+ """Test adding full color palette."""
113
+ generator = TailwindConfigGenerator()
114
+ generator.add_color_palette("brand", "#3b82f6")
115
+
116
+ brand = generator.config["theme"]["extend"]["colors"]["brand"]
117
+
118
+ assert isinstance(brand, dict)
119
+ assert "50" in brand
120
+ assert "500" in brand
121
+ assert "950" in brand
122
+ assert "var(--color-brand" in brand["500"]
123
+
124
+ def test_add_fonts(self):
125
+ """Test adding custom fonts."""
126
+ generator = TailwindConfigGenerator()
127
+ fonts = {
128
+ "sans": ["Inter", "system-ui", "sans-serif"],
129
+ "display": ["Playfair Display", "serif"]
130
+ }
131
+ generator.add_fonts(fonts)
132
+
133
+ font_family = generator.config["theme"]["extend"]["fontFamily"]
134
+ assert font_family["sans"] == ["Inter", "system-ui", "sans-serif"]
135
+ assert font_family["display"] == ["Playfair Display", "serif"]
136
+
137
+ def test_add_spacing(self):
138
+ """Test adding custom spacing."""
139
+ generator = TailwindConfigGenerator()
140
+ spacing = {
141
+ "18": "4.5rem",
142
+ "navbar": "4rem"
143
+ }
144
+ generator.add_spacing(spacing)
145
+
146
+ spacing_config = generator.config["theme"]["extend"]["spacing"]
147
+ assert spacing_config["18"] == "4.5rem"
148
+ assert spacing_config["navbar"] == "4rem"
149
+
150
+ def test_add_breakpoints(self):
151
+ """Test adding custom breakpoints."""
152
+ generator = TailwindConfigGenerator()
153
+ breakpoints = {
154
+ "3xl": "1920px",
155
+ "tablet": "768px"
156
+ }
157
+ generator.add_breakpoints(breakpoints)
158
+
159
+ screens = generator.config["theme"]["extend"]["screens"]
160
+ assert screens["3xl"] == "1920px"
161
+ assert screens["tablet"] == "768px"
162
+
163
+ def test_add_plugins(self):
164
+ """Test adding plugins."""
165
+ generator = TailwindConfigGenerator()
166
+ plugins = ["@tailwindcss/typography", "@tailwindcss/forms"]
167
+ generator.add_plugins(plugins)
168
+
169
+ assert "@tailwindcss/typography" in generator.config["plugins"]
170
+ assert "@tailwindcss/forms" in generator.config["plugins"]
171
+
172
+ def test_add_plugins_no_duplicates(self):
173
+ """Test that adding same plugin twice doesn't duplicate."""
174
+ generator = TailwindConfigGenerator()
175
+ generator.add_plugins(["@tailwindcss/typography"])
176
+ generator.add_plugins(["@tailwindcss/typography"])
177
+
178
+ count = generator.config["plugins"].count("@tailwindcss/typography")
179
+ assert count == 1
180
+
181
+ def test_recommend_plugins(self):
182
+ """Test plugin recommendations."""
183
+ generator = TailwindConfigGenerator()
184
+ recommendations = generator.recommend_plugins()
185
+
186
+ assert isinstance(recommendations, list)
187
+ assert "tailwindcss-animate" in recommendations
188
+
189
+ def test_recommend_plugins_nextjs(self):
190
+ """Test plugin recommendations for Next.js."""
191
+ generator = TailwindConfigGenerator(framework="nextjs")
192
+ recommendations = generator.recommend_plugins()
193
+
194
+ assert "@tailwindcss/typography" in recommendations
195
+
196
+ def test_generate_typescript_config(self):
197
+ """Test generating TypeScript configuration."""
198
+ generator = TailwindConfigGenerator(typescript=True)
199
+ config = generator.generate_config_string()
200
+
201
+ assert "import type { Config } from 'tailwindcss'" in config
202
+ assert "const config: Config" in config
203
+ assert "export default config" in config
204
+
205
+ def test_generate_javascript_config(self):
206
+ """Test generating JavaScript configuration."""
207
+ generator = TailwindConfigGenerator(typescript=False)
208
+ config = generator.generate_config_string()
209
+
210
+ assert "module.exports" in config
211
+ assert "@type" in config
212
+
213
+ def test_generate_config_with_colors(self):
214
+ """Test generating config with custom colors."""
215
+ generator = TailwindConfigGenerator()
216
+ generator.add_colors({"brand": "#3b82f6"})
217
+ config = generator.generate_config_string()
218
+
219
+ assert "colors" in config
220
+ assert "brand" in config
221
+
222
+ def test_generate_config_with_plugins(self):
223
+ """Test generating config with plugins."""
224
+ generator = TailwindConfigGenerator()
225
+ generator.add_plugins(["tailwindcss-animate"])
226
+ config = generator.generate_config_string()
227
+
228
+ assert "plugins:" in config
229
+ assert "require('tailwindcss-animate')" in config
230
+
231
+ def test_validate_config_valid(self):
232
+ """Test validating valid configuration."""
233
+ generator = TailwindConfigGenerator()
234
+ valid, message = generator.validate_config()
235
+
236
+ assert valid is True
237
+
238
+ def test_validate_config_no_content(self):
239
+ """Test validating config with no content paths."""
240
+ generator = TailwindConfigGenerator()
241
+ generator.config["content"] = []
242
+
243
+ valid, message = generator.validate_config()
244
+
245
+ assert valid is False
246
+ assert "No content paths" in message
247
+
248
+ def test_validate_config_empty_theme(self):
249
+ """Test validating config with empty theme extensions."""
250
+ generator = TailwindConfigGenerator()
251
+ # Default has empty theme.extend
252
+
253
+ valid, message = generator.validate_config()
254
+
255
+ assert valid is True
256
+ assert "Warning" in message
257
+
258
+ def test_write_config(self, tmp_path):
259
+ """Test writing configuration to file."""
260
+ output_path = tmp_path / "tailwind.config.ts"
261
+ generator = TailwindConfigGenerator(output_path=output_path)
262
+
263
+ success, message = generator.write_config()
264
+
265
+ assert success is True
266
+ assert output_path.exists()
267
+ assert "written to" in message
268
+
269
+ def test_write_config_creates_content(self, tmp_path):
270
+ """Test that written config contains expected content."""
271
+ output_path = tmp_path / "tailwind.config.ts"
272
+ generator = TailwindConfigGenerator(output_path=output_path)
273
+ generator.add_colors({"brand": "#3b82f6"})
274
+
275
+ generator.write_config()
276
+
277
+ content = output_path.read_text()
278
+ assert "import type { Config }" in content
279
+ assert "brand" in content
280
+
281
+ def test_write_config_invalid_path(self):
282
+ """Test writing config to invalid path."""
283
+ generator = TailwindConfigGenerator(output_path=Path("/invalid/path/config.ts"))
284
+
285
+ success, message = generator.write_config()
286
+
287
+ assert success is False
288
+ assert "Failed to write" in message
289
+
290
+ def test_full_configuration_typescript(self, tmp_path):
291
+ """Test generating complete TypeScript configuration."""
292
+ output_path = tmp_path / "tailwind.config.ts"
293
+ generator = TailwindConfigGenerator(
294
+ typescript=True,
295
+ framework="nextjs",
296
+ output_path=output_path
297
+ )
298
+
299
+ # Add various customizations
300
+ generator.add_colors({"brand": "#3b82f6", "accent": "#8b5cf6"})
301
+ generator.add_fonts({"sans": ["Inter", "sans-serif"]})
302
+ generator.add_spacing({"navbar": "4rem"})
303
+ generator.add_breakpoints({"3xl": "1920px"})
304
+ generator.add_plugins(["tailwindcss-animate"])
305
+
306
+ success, _ = generator.write_config()
307
+ assert success is True
308
+
309
+ content = output_path.read_text()
310
+
311
+ # Verify all customizations are present
312
+ assert "brand" in content
313
+ assert "accent" in content
314
+ assert "Inter" in content
315
+ assert "navbar" in content
316
+ assert "3xl" in content
317
+ assert "tailwindcss-animate" in content
318
+
319
+ def test_full_configuration_javascript(self, tmp_path):
320
+ """Test generating complete JavaScript configuration."""
321
+ output_path = tmp_path / "tailwind.config.js"
322
+ generator = TailwindConfigGenerator(
323
+ typescript=False,
324
+ framework="react",
325
+ output_path=output_path
326
+ )
327
+
328
+ generator.add_colors({"primary": "#3b82f6"})
329
+ generator.add_plugins(["@tailwindcss/forms"])
330
+
331
+ success, _ = generator.write_config()
332
+ assert success is True
333
+
334
+ content = output_path.read_text()
335
+
336
+ assert "module.exports" in content
337
+ assert "primary" in content
338
+ assert "@tailwindcss/forms" in content
339
+
340
+
341
+ def _strip_to_object(config_str: str) -> str:
342
+ """Reduce a generated TS/JS config to a bare assignable object so it can be
343
+ handed to `node --check` without a TypeScript loader."""
344
+ lines = []
345
+ for line in config_str.splitlines():
346
+ if line.startswith("import type"):
347
+ continue
348
+ if line.strip() == "export default config":
349
+ continue
350
+ line = line.replace("const config: Config =", "const config =")
351
+ line = line.replace("module.exports =", "const config =")
352
+ lines.append(line)
353
+ return "\n".join(lines)
354
+
355
+
356
+ class TestGeneratedConfigIsValidJs:
357
+ """Regression guard for the missing-comma bug between the ``theme`` block and
358
+ ``plugins`` that produced syntactically invalid config files. The data-shape
359
+ tests above all passed while the emitted string was unparseable, so these
360
+ tests validate the serialized output itself."""
361
+
362
+ @pytest.mark.parametrize("typescript", [True, False])
363
+ def test_property_before_plugins_is_comma_terminated(self, typescript):
364
+ """The property preceding ``plugins`` must end with a comma (pure-Python
365
+ check, so the regression is caught even where node is unavailable)."""
366
+ generator = TailwindConfigGenerator(typescript=typescript)
367
+ generator.add_colors({"brand": "#6366F1"})
368
+ generator.add_breakpoints({"3xl": "1920px"})
369
+ config = generator.generate_config_string()
370
+
371
+ assert "}\n plugins:" not in config, "missing comma before plugins"
372
+ assert "},\n plugins:" in config
373
+
374
+ @pytest.mark.parametrize("typescript", [True, False])
375
+ def test_node_check_parses_generated_config(self, typescript, tmp_path):
376
+ """The emitted config parses as valid JS via ``node --check``."""
377
+ node = shutil.which("node")
378
+ if not node:
379
+ pytest.skip("node not available")
380
+
381
+ generator = TailwindConfigGenerator(typescript=typescript)
382
+ generator.add_colors({"brand": "#6366F1", "accent": "#10B981"})
383
+ generator.add_fonts({"sans": ["Inter"]})
384
+ generator.add_breakpoints({"3xl": "1920px"})
385
+ generator.add_plugins(["tailwindcss-animate"])
386
+
387
+ snippet = _strip_to_object(generator.generate_config_string())
388
+ path = tmp_path / "config.cjs"
389
+ path.write_text(snippet)
390
+
391
+ result = subprocess.run(
392
+ [node, "--check", str(path)], capture_output=True, text=True
393
+ )
394
+ assert result.returncode == 0, result.stderr