@framingui/mcp-server 0.5.6 → 0.6.1

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 (85) hide show
  1. package/README.md +24 -40
  2. package/dist/__tests__/cache.test.d.ts +6 -0
  3. package/dist/__tests__/cache.test.d.ts.map +1 -0
  4. package/dist/__tests__/cache.test.js +86 -0
  5. package/dist/__tests__/cache.test.js.map +1 -0
  6. package/dist/__tests__/data-client.test.d.ts +13 -0
  7. package/dist/__tests__/data-client.test.d.ts.map +1 -0
  8. package/dist/__tests__/data-client.test.js +415 -0
  9. package/dist/__tests__/data-client.test.js.map +1 -0
  10. package/dist/api/api-result.d.ts +30 -0
  11. package/dist/api/api-result.d.ts.map +1 -0
  12. package/dist/api/api-result.js +50 -0
  13. package/dist/api/api-result.js.map +1 -0
  14. package/dist/api/data-client.d.ts +89 -9
  15. package/dist/api/data-client.d.ts.map +1 -1
  16. package/dist/api/data-client.js +255 -68
  17. package/dist/api/data-client.js.map +1 -1
  18. package/dist/auth/cache.d.ts +5 -0
  19. package/dist/auth/cache.d.ts.map +1 -1
  20. package/dist/auth/cache.js +11 -0
  21. package/dist/auth/cache.js.map +1 -1
  22. package/dist/data/recipe-resolver.d.ts.map +1 -1
  23. package/dist/data/recipe-resolver.js +6 -4
  24. package/dist/data/recipe-resolver.js.map +1 -1
  25. package/dist/data/template-matcher.d.ts +7 -1
  26. package/dist/data/template-matcher.d.ts.map +1 -1
  27. package/dist/data/template-matcher.js +19 -5
  28. package/dist/data/template-matcher.js.map +1 -1
  29. package/dist/generators/core-resolver.d.ts +7 -13
  30. package/dist/generators/core-resolver.d.ts.map +1 -1
  31. package/dist/generators/core-resolver.js +7 -84
  32. package/dist/generators/core-resolver.js.map +1 -1
  33. package/dist/generators/css-generator.d.ts +8 -26
  34. package/dist/generators/css-generator.d.ts.map +1 -1
  35. package/dist/generators/css-generator.js +17 -265
  36. package/dist/generators/css-generator.js.map +1 -1
  37. package/dist/generators/index.d.ts +2 -2
  38. package/dist/generators/index.d.ts.map +1 -1
  39. package/dist/generators/index.js +4 -2
  40. package/dist/generators/index.js.map +1 -1
  41. package/dist/schemas/mcp-schemas.d.ts +385 -385
  42. package/dist/schemas/mcp-schemas.d.ts.map +1 -1
  43. package/dist/tools/export-screen.d.ts.map +1 -1
  44. package/dist/tools/export-screen.js +6 -9
  45. package/dist/tools/export-screen.js.map +1 -1
  46. package/dist/tools/get-screen-generation-context.d.ts +1 -0
  47. package/dist/tools/get-screen-generation-context.d.ts.map +1 -1
  48. package/dist/tools/get-screen-generation-context.js +40 -26
  49. package/dist/tools/get-screen-generation-context.js.map +1 -1
  50. package/dist/tools/list-components.d.ts +2 -1
  51. package/dist/tools/list-components.d.ts.map +1 -1
  52. package/dist/tools/list-components.js +25 -14
  53. package/dist/tools/list-components.js.map +1 -1
  54. package/dist/tools/list-icon-libraries.d.ts.map +1 -1
  55. package/dist/tools/list-icon-libraries.js +5 -8
  56. package/dist/tools/list-icon-libraries.js.map +1 -1
  57. package/dist/tools/list-screen-templates.d.ts +2 -1
  58. package/dist/tools/list-screen-templates.d.ts.map +1 -1
  59. package/dist/tools/list-screen-templates.js +29 -25
  60. package/dist/tools/list-screen-templates.js.map +1 -1
  61. package/dist/tools/list-themes.d.ts.map +1 -1
  62. package/dist/tools/list-themes.js +6 -1
  63. package/dist/tools/list-themes.js.map +1 -1
  64. package/dist/tools/list-tokens.d.ts +2 -1
  65. package/dist/tools/list-tokens.d.ts.map +1 -1
  66. package/dist/tools/list-tokens.js +12 -36
  67. package/dist/tools/list-tokens.js.map +1 -1
  68. package/dist/tools/preview-component.d.ts +1 -0
  69. package/dist/tools/preview-component.d.ts.map +1 -1
  70. package/dist/tools/preview-component.js +33 -28
  71. package/dist/tools/preview-component.js.map +1 -1
  72. package/dist/tools/preview-icon-library.d.ts.map +1 -1
  73. package/dist/tools/preview-icon-library.js +5 -7
  74. package/dist/tools/preview-icon-library.js.map +1 -1
  75. package/dist/tools/preview-screen-template.d.ts +1 -0
  76. package/dist/tools/preview-screen-template.d.ts.map +1 -1
  77. package/dist/tools/preview-screen-template.js +18 -9
  78. package/dist/tools/preview-screen-template.js.map +1 -1
  79. package/dist/tools/preview-theme.d.ts.map +1 -1
  80. package/dist/tools/preview-theme.js +6 -15
  81. package/dist/tools/preview-theme.js.map +1 -1
  82. package/dist/tools/validate-screen-definition.d.ts.map +1 -1
  83. package/dist/tools/validate-screen-definition.js +53 -12
  84. package/dist/tools/validate-screen-definition.js.map +1 -1
  85. package/package.json +3 -3
package/README.md CHANGED
@@ -102,9 +102,9 @@ npx @framingui/mcp-server init
102
102
  | -------------------------------- | --------------------- |
103
103
  | `npx @framingui/mcp-server` | MCP stdio 서버 시작 |
104
104
  | `npx @framingui/mcp-server init` | 프로젝트 초기 설정 |
105
- | `framingui-mcp login` | 브라우저 OAuth 로그인 |
106
- | `framingui-mcp logout` | 로그아웃 |
107
- | `framingui-mcp status` | 인증 상태 확인 |
105
+ | `framingui-mcp login` | 브라우저 OAuth 로그인 |
106
+ | `framingui-mcp logout` | 로그아웃 |
107
+ | `framingui-mcp status` | 인증 상태 확인 |
108
108
 
109
109
  ## Development Quick Start
110
110
 
@@ -824,9 +824,10 @@ packages/mcp-server/
824
824
  │ │ ├── preview-component.ts # Component preview (SPEC-MCP-003)
825
825
  │ │ ├── list-screen-templates.ts # Template listing (SPEC-MCP-003)
826
826
  │ │ └── preview-screen-template.ts # Template preview (SPEC-MCP-003)
827
- │ ├── data/ # Static data registries (SPEC-MCP-003)
828
- │ │ ├── component-registry.ts # Component metadata registry
829
- │ │ └── component-metadata.json # Static component metadata
827
+ │ ├── data/ # Data utilities (non-API helpers)
828
+ │ │ ├── template-matcher.ts # Template matching logic
829
+ │ │ ├── hint-generator.ts # AI hint generation
830
+ │ │ └── recipe-resolver.ts # Recipe resolution
830
831
  │ ├── storage/ # Blueprint storage
831
832
  │ │ ├── blueprint-storage.ts
832
833
  │ │ └── timestamp-manager.ts
@@ -883,43 +884,26 @@ packages/mcp-server/
883
884
 
884
885
  **Test Results**:
885
886
 
886
- - 22 test files
887
- - 214 test cases
887
+ - 29 test files
888
+ - 290 test cases
888
889
  - 100% pass rate
889
890
  - Zero failures
890
891
 
891
- ## Integration with @framingui/core
892
+ ## Architecture: API-Based Data Sources (v0.6.0)
892
893
 
893
- All MCP tools reuse `@framingui/core` functions:
894
+ Since v0.6.0, the MCP server fetches all data from the framingui.com API via `data-client.ts`. This removes `@framingui/core` and `@framingui/ui` from production dependencies, enabling truly standalone npm installation.
894
895
 
895
- **Blueprint & Theme Tools**:
896
+ **Data Client** (`src/api/data-client.ts`):
896
897
 
897
- - `loadTheme()` - Theme loading
898
- - `listThemes()` - Theme enumeration
899
- - `createBlueprint()` - Blueprint creation
900
- - `validateBlueprint()` - Schema validation
901
- - `generateCSSVariables()` - CSS variable extraction
902
- - `render()` - Code generation
898
+ - `fetchThemeList()`, `fetchTheme(id)` Theme data
899
+ - `fetchIconLibraries()`, `fetchIconLibrary(id)` Icon libraries
900
+ - `fetchTemplateList()`, `fetchTemplate(id)` Screen templates
901
+ - `fetchComponentList()`, `fetchComponent(id)` Component catalog
902
+ - `fetchTokenList(type?)` Layout tokens
903
+ - `fetchCSSVariables(themeId)` CSS generation
904
+ - `fetchScreenExamples()` — Screen examples
903
905
 
904
- **Screen Generation Tools** (SPEC-LAYOUT-002):
905
-
906
- - `validateScreenDefinition()` - Screen validation
907
- - `resolveScreen()` - Layout and component resolution
908
- - `generateStyledComponents()` - CSS-in-JS generation
909
- - `generateTailwindClasses()` - Tailwind CSS generation
910
- - `generateReactComponent()` - React component generation
911
- - `getAllShellTokens()` - Shell token listing
912
- - `getAllPageLayoutTokens()` - Page token listing
913
- - `getAllSectionPatternTokens()` - Section token listing
914
-
915
- **Component & Template Discovery** (SPEC-MCP-003):
916
-
917
- - `templateRegistry` from `@framingui` - Template metadata and search
918
- - Component metadata registry - Static component catalog with 30+ components
919
- - Component type definitions - TypeScript interfaces for props and variants
920
- - Template structure definitions - Skeleton, layout, and customization schemas
921
-
922
- **Zero code duplication** - Single source of truth maintained.
906
+ All functions use `MemoryCache` (10-min TTL) with `getStale()` fallback for network resilience.
923
907
 
924
908
  ## Documentation
925
909
 
@@ -1021,7 +1005,7 @@ MIT
1021
1005
 
1022
1006
  ---
1023
1007
 
1024
- **Version**: 3.0.0 (stdio-based MCP standard + Component & Template Discovery)
1025
- **Last Updated**: 2026-02-01
1026
- **SPEC**: SPEC-MCP-002 v2.0.0, SPEC-LAYOUT-002 Phase 4, SPEC-MCP-003 v1.0.0
1027
- **Total Tools**: 13 (9 existing + 4 new discovery tools)
1008
+ **Version**: 0.6.0 (API-based data sources no workspace dependencies)
1009
+ **Last Updated**: 2026-03-06
1010
+ **SPEC**: SPEC-MCP-002 v2.0.0, SPEC-LAYOUT-002 Phase 4, SPEC-MCP-003 v1.0.0, SPEC-MCP-007 v1.0.0
1011
+ **Total Tools**: 17
@@ -0,0 +1,6 @@
1
+ /**
2
+ * MemoryCache 테스트
3
+ * SPEC-MCP-007:S-004 - getStale() 메서드: TTL 만료 후에도 캐시 값 반환
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=cache.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/cache.test.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * MemoryCache 테스트
3
+ * SPEC-MCP-007:S-004 - getStale() 메서드: TTL 만료 후에도 캐시 값 반환
4
+ */
5
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
6
+ // vitest에서 .js 확장자는 dist 파일을 참조할 수 있으므로 .ts 확장자로 직접 import
7
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
8
+ // @ts-ignore
9
+ import { MemoryCache } from '../auth/cache.ts';
10
+ describe('MemoryCache', () => {
11
+ let cache;
12
+ beforeEach(() => {
13
+ cache = new MemoryCache();
14
+ vi.useRealTimers();
15
+ });
16
+ describe('set / get (기본 동작)', () => {
17
+ it('TTL 이내에 get()으로 값을 반환한다', () => {
18
+ cache.set('key1', 'value1', 10_000);
19
+ expect(cache.get('key1')).toBe('value1');
20
+ });
21
+ it('존재하지 않는 키에 대해 null을 반환한다', () => {
22
+ expect(cache.get('non-existent')).toBeNull();
23
+ });
24
+ it('TTL 만료 후 get()은 null을 반환한다', () => {
25
+ vi.useFakeTimers();
26
+ cache.set('key2', 'expired', 1_000);
27
+ // TTL(1초) 이후로 시간 이동
28
+ vi.advanceTimersByTime(1_001);
29
+ expect(cache.get('key2')).toBeNull();
30
+ vi.useRealTimers();
31
+ });
32
+ });
33
+ describe('getStale()', () => {
34
+ it('TTL 이내에 getStale()은 값을 반환한다', () => {
35
+ cache.set('key3', 'fresh', 10_000);
36
+ expect(cache.getStale('key3')).toBe('fresh');
37
+ });
38
+ it('TTL 만료 후에도 getStale()은 값을 반환한다 (stale 허용)', () => {
39
+ vi.useFakeTimers();
40
+ cache.set('key4', 'stale-value', 500);
41
+ // TTL 만료
42
+ vi.advanceTimersByTime(1_000);
43
+ // getStale()은 만료 후에도 값을 반환해야 한다
44
+ // 주의: get()을 먼저 호출하면 만료된 항목이 삭제됨
45
+ expect(cache.getStale('key4')).toBe('stale-value');
46
+ // get()은 만료된 항목을 삭제하고 null을 반환한다
47
+ const freshCache = new MemoryCache();
48
+ freshCache.set('key4b', 'stale-value-b', 500);
49
+ vi.advanceTimersByTime(1_000);
50
+ expect(freshCache.get('key4b')).toBeNull();
51
+ // get() 호출 후에는 항목이 삭제되어 getStale()도 null 반환
52
+ expect(freshCache.getStale('key4b')).toBeNull();
53
+ vi.useRealTimers();
54
+ });
55
+ it('키가 설정된 적 없으면 getStale()은 null을 반환한다', () => {
56
+ expect(cache.getStale('never-set')).toBeNull();
57
+ });
58
+ it('delete() 후 getStale()은 null을 반환한다', () => {
59
+ cache.set('key5', 'to-delete', 10_000);
60
+ cache.delete('key5');
61
+ expect(cache.getStale('key5')).toBeNull();
62
+ });
63
+ });
64
+ describe('delete / clear', () => {
65
+ it('delete()로 특정 키를 제거한다', () => {
66
+ cache.set('key6', 'val6', 10_000);
67
+ cache.delete('key6');
68
+ expect(cache.get('key6')).toBeNull();
69
+ });
70
+ it('clear()로 모든 캐시를 비운다', () => {
71
+ cache.set('a', 'aa', 10_000);
72
+ cache.set('b', 'bb', 10_000);
73
+ cache.clear();
74
+ expect(cache.size()).toBe(0);
75
+ });
76
+ });
77
+ describe('size()', () => {
78
+ it('항목 추가 수만큼 size()가 증가한다', () => {
79
+ expect(cache.size()).toBe(0);
80
+ cache.set('s1', 'v1', 10_000);
81
+ cache.set('s2', 'v2', 10_000);
82
+ expect(cache.size()).toBe(2);
83
+ });
84
+ });
85
+ });
86
+ //# sourceMappingURL=cache.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.test.js","sourceRoot":"","sources":["../../src/__tests__/cache.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * data-client.ts 테스트 (v2 - ApiResult 기반)
3
+ * SPEC-MCP-007 Phase 2 - fetchTemplateList, fetchTemplate, fetchComponentList,
4
+ * fetchComponent, fetchTokenList, fetchCSSVariables, fetchScreenExamples
5
+ *
6
+ * 테스트 시나리오:
7
+ * 1. 캐시 미스: fetch 호출 → ApiResult 성공 반환
8
+ * 2. API 실패 + 캐시 없음: ApiResult 에러 반환
9
+ * 3. 인증 없음: fetch 호출 없이 NOT_AUTHENTICATED 에러 반환
10
+ * 4. URL 파라미터 전달 확인
11
+ */
12
+ export {};
13
+ //# sourceMappingURL=data-client.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-client.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/data-client.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
@@ -0,0 +1,415 @@
1
+ /**
2
+ * data-client.ts 테스트 (v2 - ApiResult 기반)
3
+ * SPEC-MCP-007 Phase 2 - fetchTemplateList, fetchTemplate, fetchComponentList,
4
+ * fetchComponent, fetchTokenList, fetchCSSVariables, fetchScreenExamples
5
+ *
6
+ * 테스트 시나리오:
7
+ * 1. 캐시 미스: fetch 호출 → ApiResult 성공 반환
8
+ * 2. API 실패 + 캐시 없음: ApiResult 에러 반환
9
+ * 3. 인증 없음: fetch 호출 없이 NOT_AUTHENTICATED 에러 반환
10
+ * 4. URL 파라미터 전달 확인
11
+ */
12
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
13
+ // ─────────────────────────────────────────────────────────────────────────────
14
+ // vi.mock 호이스팅: factory 안에서 참조할 mock 함수를 vi.hoisted()로 선언
15
+ // ─────────────────────────────────────────────────────────────────────────────
16
+ const { mockGetAuthData, mockGetRawApiKey, mockResolveFraminguiApiUrl } = vi.hoisted(() => ({
17
+ mockGetAuthData: vi.fn(),
18
+ mockGetRawApiKey: vi.fn(),
19
+ mockResolveFraminguiApiUrl: vi.fn(),
20
+ }));
21
+ // data-client.ts가 .js 확장자로 import하므로 mock도 동일 경로 사용
22
+ vi.mock('../auth/state.js', () => ({
23
+ getAuthData: mockGetAuthData,
24
+ getRawApiKey: mockGetRawApiKey,
25
+ }));
26
+ vi.mock('../utils/api-url.js', () => ({
27
+ resolveFraminguiApiUrl: mockResolveFraminguiApiUrl,
28
+ }));
29
+ vi.mock('../utils/logger.js', () => ({
30
+ info: vi.fn(),
31
+ error: vi.fn(),
32
+ }));
33
+ // MemoryCache를 mock으로 교체 - 캐시가 항상 miss인 상태로 테스트
34
+ vi.mock('../auth/cache.js', () => {
35
+ return {
36
+ MemoryCache: vi.fn().mockImplementation(() => ({
37
+ get: vi.fn().mockReturnValue(null),
38
+ getStale: vi.fn().mockReturnValue(null),
39
+ set: vi.fn(),
40
+ delete: vi.fn(),
41
+ clear: vi.fn(),
42
+ size: vi.fn().mockReturnValue(0),
43
+ })),
44
+ };
45
+ });
46
+ // global fetch 목업
47
+ const mockFetch = vi.fn();
48
+ global.fetch = mockFetch;
49
+ // ─────────────────────────────────────────────────────────────────────────────
50
+ // 테스트 대상 import
51
+ // ─────────────────────────────────────────────────────────────────────────────
52
+ import { fetchTemplateList, fetchTemplate, fetchComponentList, fetchComponent, fetchTokenList, fetchCSSVariables, fetchScreenExamples, } from '../api/data-client.js';
53
+ // ─────────────────────────────────────────────────────────────────────────────
54
+ // 헬퍼
55
+ // ─────────────────────────────────────────────────────────────────────────────
56
+ function makeOkResponse(body) {
57
+ return {
58
+ ok: true,
59
+ status: 200,
60
+ json: vi.fn().mockResolvedValue(body),
61
+ };
62
+ }
63
+ function makeErrorResponse(status, body) {
64
+ return {
65
+ ok: false,
66
+ status,
67
+ statusText: 'Error',
68
+ json: vi.fn().mockResolvedValue(body),
69
+ };
70
+ }
71
+ // ─────────────────────────────────────────────────────────────────────────────
72
+ // 공통 beforeEach
73
+ // ─────────────────────────────────────────────────────────────────────────────
74
+ beforeEach(() => {
75
+ vi.clearAllMocks();
76
+ mockGetAuthData.mockReturnValue({ valid: true });
77
+ mockGetRawApiKey.mockReturnValue('tk_live_test_key_00000000000000000000000000000000000000000000000000000000000000000');
78
+ mockResolveFraminguiApiUrl.mockReturnValue({ apiUrl: 'https://framingui.com' });
79
+ });
80
+ // ─────────────────────────────────────────────────────────────────────────────
81
+ // fetchTemplateList
82
+ // ─────────────────────────────────────────────────────────────────────────────
83
+ describe('fetchTemplateList()', () => {
84
+ it('API 성공 시 ok: true와 템플릿 목록을 반환한다', async () => {
85
+ const templates = [
86
+ {
87
+ id: 'dashboard',
88
+ name: 'Dashboard',
89
+ category: 'web',
90
+ description: '',
91
+ requiredComponentsCount: 3,
92
+ },
93
+ ];
94
+ mockFetch.mockResolvedValueOnce(makeOkResponse({ success: true, templates }));
95
+ const result = await fetchTemplateList();
96
+ expect(result.ok).toBe(true);
97
+ if (result.ok) {
98
+ expect(result.data).toHaveLength(1);
99
+ expect(result.data[0].id).toBe('dashboard');
100
+ }
101
+ expect(mockFetch).toHaveBeenCalledOnce();
102
+ });
103
+ it('category 파라미터를 쿼리스트링으로 전달한다', async () => {
104
+ mockFetch.mockResolvedValueOnce(makeOkResponse({ success: true, templates: [] }));
105
+ await fetchTemplateList({ category: 'mobile' });
106
+ const calledUrl = mockFetch.mock.calls[0][0];
107
+ expect(calledUrl).toContain('category=mobile');
108
+ });
109
+ it('search 파라미터를 쿼리스트링으로 전달한다', async () => {
110
+ mockFetch.mockResolvedValueOnce(makeOkResponse({ success: true, templates: [] }));
111
+ await fetchTemplateList({ search: 'login' });
112
+ const calledUrl = mockFetch.mock.calls[0][0];
113
+ expect(calledUrl).toContain('search=login');
114
+ });
115
+ it('API 응답 success: false 시 에러를 반환한다', async () => {
116
+ mockFetch.mockResolvedValueOnce(makeOkResponse({ success: false, error: 'not found' }));
117
+ const result = await fetchTemplateList();
118
+ expect(result.ok).toBe(false);
119
+ if (!result.ok) {
120
+ expect(result.error.code).toBe('SERVER_ERROR');
121
+ }
122
+ });
123
+ it('fetch 네트워크 오류 시 NETWORK_ERROR를 반환한다', async () => {
124
+ mockFetch.mockRejectedValueOnce(new Error('Network error'));
125
+ const result = await fetchTemplateList();
126
+ expect(result.ok).toBe(false);
127
+ if (!result.ok) {
128
+ expect(result.error.code).toBe('NETWORK_ERROR');
129
+ }
130
+ });
131
+ it('인증 없는 상태에서 NOT_AUTHENTICATED를 반환한다', async () => {
132
+ mockGetAuthData.mockReturnValue(null);
133
+ const result = await fetchTemplateList();
134
+ expect(result.ok).toBe(false);
135
+ if (!result.ok) {
136
+ expect(result.error.code).toBe('NOT_AUTHENTICATED');
137
+ }
138
+ expect(mockFetch).not.toHaveBeenCalled();
139
+ });
140
+ });
141
+ // ─────────────────────────────────────────────────────────────────────────────
142
+ // fetchTemplate
143
+ // ─────────────────────────────────────────────────────────────────────────────
144
+ describe('fetchTemplate()', () => {
145
+ it('API 성공 시 ok: true와 템플릿 상세를 반환한다', async () => {
146
+ const template = { id: 'dashboard', name: 'Dashboard', sections: [] };
147
+ mockFetch.mockResolvedValueOnce(makeOkResponse({ success: true, template }));
148
+ const result = await fetchTemplate('dashboard');
149
+ expect(result.ok).toBe(true);
150
+ if (result.ok) {
151
+ expect(result.data.id).toBe('dashboard');
152
+ }
153
+ });
154
+ it('API 응답 success: false 시 에러를 반환한다', async () => {
155
+ mockFetch.mockResolvedValueOnce(makeOkResponse({ success: false, error: 'not found' }));
156
+ const result = await fetchTemplate('nonexistent');
157
+ expect(result.ok).toBe(false);
158
+ });
159
+ it('fetch 네트워크 오류 시 NETWORK_ERROR를 반환한다', async () => {
160
+ mockFetch.mockRejectedValueOnce(new Error('timeout'));
161
+ const result = await fetchTemplate('dashboard');
162
+ expect(result.ok).toBe(false);
163
+ if (!result.ok) {
164
+ expect(result.error.code).toBe('NETWORK_ERROR');
165
+ }
166
+ });
167
+ it('ID가 URL 인코딩되어 전달된다', async () => {
168
+ mockFetch.mockResolvedValueOnce(makeOkResponse({ success: true, template: { id: 'my template' } }));
169
+ await fetchTemplate('my template');
170
+ const calledUrl = mockFetch.mock.calls[0][0];
171
+ expect(calledUrl).toContain('my%20template');
172
+ });
173
+ });
174
+ // ─────────────────────────────────────────────────────────────────────────────
175
+ // fetchComponentList
176
+ // ─────────────────────────────────────────────────────────────────────────────
177
+ describe('fetchComponentList()', () => {
178
+ it('API 성공 시 ok: true와 컴포넌트 목록을 반환한다', async () => {
179
+ const components = [
180
+ {
181
+ id: 'button',
182
+ name: 'Button',
183
+ category: 'core',
184
+ tier: 1,
185
+ description: '',
186
+ variantsCount: 6,
187
+ hasSubComponents: false,
188
+ },
189
+ ];
190
+ mockFetch.mockResolvedValueOnce(makeOkResponse({ success: true, components }));
191
+ const result = await fetchComponentList();
192
+ expect(result.ok).toBe(true);
193
+ if (result.ok) {
194
+ expect(result.data).toHaveLength(1);
195
+ expect(result.data[0].id).toBe('button');
196
+ }
197
+ });
198
+ it('API 응답 success: false 시 에러를 반환한다', async () => {
199
+ mockFetch.mockResolvedValueOnce(makeOkResponse({ success: false }));
200
+ const result = await fetchComponentList();
201
+ expect(result.ok).toBe(false);
202
+ });
203
+ it('fetch 네트워크 오류 시 NETWORK_ERROR를 반환한다', async () => {
204
+ mockFetch.mockRejectedValueOnce(new Error('Network error'));
205
+ const result = await fetchComponentList();
206
+ expect(result.ok).toBe(false);
207
+ if (!result.ok) {
208
+ expect(result.error.code).toBe('NETWORK_ERROR');
209
+ }
210
+ });
211
+ });
212
+ // ─────────────────────────────────────────────────────────────────────────────
213
+ // fetchComponent
214
+ // ─────────────────────────────────────────────────────────────────────────────
215
+ describe('fetchComponent()', () => {
216
+ it('API 성공 시 ok: true와 컴포넌트 상세를 반환한다', async () => {
217
+ const component = {
218
+ id: 'button',
219
+ name: 'Button',
220
+ category: 'core',
221
+ tier: 1,
222
+ description: 'Button desc',
223
+ variantsCount: 6,
224
+ hasSubComponents: false,
225
+ props: [],
226
+ variants: [],
227
+ };
228
+ mockFetch.mockResolvedValueOnce(makeOkResponse({ success: true, component }));
229
+ const result = await fetchComponent('button');
230
+ expect(result.ok).toBe(true);
231
+ if (result.ok) {
232
+ expect(result.data.id).toBe('button');
233
+ expect(result.data.props).toBeDefined();
234
+ }
235
+ });
236
+ it('API 응답 success: false 시 에러를 반환한다', async () => {
237
+ mockFetch.mockResolvedValueOnce(makeOkResponse({ success: false, error: 'not found' }));
238
+ const result = await fetchComponent('nonexistent');
239
+ expect(result.ok).toBe(false);
240
+ });
241
+ it('fetch 네트워크 오류 시 NETWORK_ERROR를 반환한다', async () => {
242
+ mockFetch.mockRejectedValueOnce(new Error('timeout'));
243
+ const result = await fetchComponent('button');
244
+ expect(result.ok).toBe(false);
245
+ if (!result.ok) {
246
+ expect(result.error.code).toBe('NETWORK_ERROR');
247
+ }
248
+ });
249
+ });
250
+ // ─────────────────────────────────────────────────────────────────────────────
251
+ // fetchTokenList
252
+ // ─────────────────────────────────────────────────────────────────────────────
253
+ describe('fetchTokenList()', () => {
254
+ it('type=all 요청 시 ok: true와 shells/pages/sections를 반환한다', async () => {
255
+ const responseBody = {
256
+ success: true,
257
+ shells: [{ id: 'shell.web.dashboard' }],
258
+ pages: [{ id: 'page.dashboard' }],
259
+ sections: [{ id: 'section.container' }],
260
+ metadata: { total: 3 },
261
+ };
262
+ mockFetch.mockResolvedValueOnce(makeOkResponse(responseBody));
263
+ const result = await fetchTokenList('all');
264
+ expect(result.ok).toBe(true);
265
+ if (result.ok) {
266
+ expect(result.data.metadata.total).toBe(3);
267
+ expect(result.data.shells).toHaveLength(1);
268
+ expect(result.data.pages).toHaveLength(1);
269
+ expect(result.data.sections).toHaveLength(1);
270
+ }
271
+ });
272
+ it('type=shell 쿼리파라미터가 전달된다', async () => {
273
+ mockFetch.mockResolvedValueOnce(makeOkResponse({ success: true, shells: [], metadata: { total: 0 } }));
274
+ await fetchTokenList('shell');
275
+ const calledUrl = mockFetch.mock.calls[0][0];
276
+ expect(calledUrl).toContain('type=shell');
277
+ });
278
+ it('API 응답 success: false 시 에러를 반환한다', async () => {
279
+ mockFetch.mockResolvedValueOnce(makeOkResponse({ success: false }));
280
+ const result = await fetchTokenList('all');
281
+ expect(result.ok).toBe(false);
282
+ });
283
+ it('tokenType 미지정 시 type=all로 요청된다', async () => {
284
+ mockFetch.mockResolvedValueOnce(makeOkResponse({ success: true, metadata: { total: 0 } }));
285
+ await fetchTokenList();
286
+ const calledUrl = mockFetch.mock.calls[0][0];
287
+ expect(calledUrl).toContain('type=all');
288
+ });
289
+ });
290
+ // ─────────────────────────────────────────────────────────────────────────────
291
+ // fetchCSSVariables
292
+ // ─────────────────────────────────────────────────────────────────────────────
293
+ describe('fetchCSSVariables()', () => {
294
+ it('API 성공 시 ok: true와 CSS 문자열을 반환한다', async () => {
295
+ const css = ':root { --color-brand-500: oklch(0.5 0.2 240); }';
296
+ mockFetch.mockResolvedValueOnce(makeOkResponse({ success: true, css }));
297
+ const result = await fetchCSSVariables('square-minimalism');
298
+ expect(result.ok).toBe(true);
299
+ if (result.ok) {
300
+ expect(result.data).toBe(css);
301
+ expect(result.data).toContain(':root');
302
+ }
303
+ });
304
+ it('API 응답 success: false 시 에러를 반환한다', async () => {
305
+ mockFetch.mockResolvedValueOnce(makeOkResponse({ success: false, error: 'not found' }));
306
+ const result = await fetchCSSVariables('nonexistent');
307
+ expect(result.ok).toBe(false);
308
+ });
309
+ it('fetch 네트워크 오류 시 NETWORK_ERROR를 반환한다', async () => {
310
+ mockFetch.mockRejectedValueOnce(new Error('timeout'));
311
+ const result = await fetchCSSVariables('square-minimalism');
312
+ expect(result.ok).toBe(false);
313
+ if (!result.ok) {
314
+ expect(result.error.code).toBe('NETWORK_ERROR');
315
+ }
316
+ });
317
+ it('themeId와 /css 경로가 URL에 포함된다', async () => {
318
+ mockFetch.mockResolvedValueOnce(makeOkResponse({ success: true, css: ':root {}' }));
319
+ await fetchCSSVariables('dark-boldness');
320
+ const calledUrl = mockFetch.mock.calls[0][0];
321
+ expect(calledUrl).toContain('dark-boldness');
322
+ expect(calledUrl).toContain('/css');
323
+ });
324
+ });
325
+ // ─────────────────────────────────────────────────────────────────────────────
326
+ // fetchScreenExamples
327
+ // ─────────────────────────────────────────────────────────────────────────────
328
+ describe('fetchScreenExamples()', () => {
329
+ it('API 성공 시 ok: true와 스크린 예제 목록을 반환한다', async () => {
330
+ const examples = [
331
+ { name: 'Team Grid', description: 'Grid layout', definition: { id: 'team-grid' } },
332
+ { name: 'Login Form', description: 'Auth screen', definition: { id: 'login-screen' } },
333
+ ];
334
+ mockFetch.mockResolvedValueOnce(makeOkResponse({ success: true, examples }));
335
+ const result = await fetchScreenExamples();
336
+ expect(result.ok).toBe(true);
337
+ if (result.ok) {
338
+ expect(result.data).toHaveLength(2);
339
+ expect(result.data[0].name).toBe('Team Grid');
340
+ expect(result.data[1].definition.id).toBe('login-screen');
341
+ }
342
+ });
343
+ it('API 응답 success: false 시 에러를 반환한다', async () => {
344
+ mockFetch.mockResolvedValueOnce(makeOkResponse({ success: false }));
345
+ const result = await fetchScreenExamples();
346
+ expect(result.ok).toBe(false);
347
+ });
348
+ it('fetch 네트워크 오류 시 NETWORK_ERROR를 반환한다', async () => {
349
+ mockFetch.mockRejectedValueOnce(new Error('Network error'));
350
+ const result = await fetchScreenExamples();
351
+ expect(result.ok).toBe(false);
352
+ if (!result.ok) {
353
+ expect(result.error.code).toBe('NETWORK_ERROR');
354
+ }
355
+ });
356
+ it('응답 예제에 name, description, definition이 포함된다', async () => {
357
+ const examples = [
358
+ {
359
+ name: 'Dashboard',
360
+ description: 'Analytics',
361
+ definition: { id: 'dashboard-overview', shell: 'shell.web.dashboard' },
362
+ },
363
+ ];
364
+ mockFetch.mockResolvedValueOnce(makeOkResponse({ success: true, examples }));
365
+ const result = await fetchScreenExamples();
366
+ expect(result.ok).toBe(true);
367
+ if (result.ok) {
368
+ expect(result.data[0]).toHaveProperty('name');
369
+ expect(result.data[0]).toHaveProperty('description');
370
+ expect(result.data[0]).toHaveProperty('definition');
371
+ expect(result.data[0].definition.shell).toBe('shell.web.dashboard');
372
+ }
373
+ });
374
+ });
375
+ // ─────────────────────────────────────────────────────────────────────────────
376
+ // HTTP 에러 코드 분류 테스트
377
+ // ─────────────────────────────────────────────────────────────────────────────
378
+ describe('HTTP 에러 코드 분류', () => {
379
+ it('401 응답 시 AUTH_FAILED 에러를 반환한다', async () => {
380
+ mockFetch.mockResolvedValueOnce(makeErrorResponse(401, { error: 'unauthorized' }));
381
+ const result = await fetchTemplate('pebble');
382
+ expect(result.ok).toBe(false);
383
+ if (!result.ok) {
384
+ expect(result.error.code).toBe('AUTH_FAILED');
385
+ expect(result.error.status).toBe(401);
386
+ }
387
+ });
388
+ it('403 응답 시 FORBIDDEN 에러를 반환한다', async () => {
389
+ mockFetch.mockResolvedValueOnce(makeErrorResponse(403, { error: 'Theme "pebble" is not included in your license.' }));
390
+ const result = await fetchTemplate('pebble');
391
+ expect(result.ok).toBe(false);
392
+ if (!result.ok) {
393
+ expect(result.error.code).toBe('FORBIDDEN');
394
+ expect(result.error.message).toContain('not included');
395
+ }
396
+ });
397
+ it('429 응답 시 RATE_LIMITED 에러를 반환한다', async () => {
398
+ mockFetch.mockResolvedValueOnce(makeErrorResponse(429, { error: 'Too many requests' }));
399
+ const result = await fetchTemplate('pebble');
400
+ expect(result.ok).toBe(false);
401
+ if (!result.ok) {
402
+ expect(result.error.code).toBe('RATE_LIMITED');
403
+ }
404
+ });
405
+ it('500 응답 시 SERVER_ERROR 에러를 반환한다', async () => {
406
+ mockFetch.mockResolvedValueOnce(makeErrorResponse(500, { error: 'Internal server error' }));
407
+ const result = await fetchTemplate('pebble');
408
+ expect(result.ok).toBe(false);
409
+ if (!result.ok) {
410
+ expect(result.error.code).toBe('SERVER_ERROR');
411
+ expect(result.error.status).toBe(500);
412
+ }
413
+ });
414
+ });
415
+ //# sourceMappingURL=data-client.test.js.map