@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.
- package/README.md +24 -40
- package/dist/__tests__/cache.test.d.ts +6 -0
- package/dist/__tests__/cache.test.d.ts.map +1 -0
- package/dist/__tests__/cache.test.js +86 -0
- package/dist/__tests__/cache.test.js.map +1 -0
- package/dist/__tests__/data-client.test.d.ts +13 -0
- package/dist/__tests__/data-client.test.d.ts.map +1 -0
- package/dist/__tests__/data-client.test.js +415 -0
- package/dist/__tests__/data-client.test.js.map +1 -0
- package/dist/api/api-result.d.ts +30 -0
- package/dist/api/api-result.d.ts.map +1 -0
- package/dist/api/api-result.js +50 -0
- package/dist/api/api-result.js.map +1 -0
- package/dist/api/data-client.d.ts +89 -9
- package/dist/api/data-client.d.ts.map +1 -1
- package/dist/api/data-client.js +255 -68
- package/dist/api/data-client.js.map +1 -1
- package/dist/auth/cache.d.ts +5 -0
- package/dist/auth/cache.d.ts.map +1 -1
- package/dist/auth/cache.js +11 -0
- package/dist/auth/cache.js.map +1 -1
- package/dist/data/recipe-resolver.d.ts.map +1 -1
- package/dist/data/recipe-resolver.js +6 -4
- package/dist/data/recipe-resolver.js.map +1 -1
- package/dist/data/template-matcher.d.ts +7 -1
- package/dist/data/template-matcher.d.ts.map +1 -1
- package/dist/data/template-matcher.js +19 -5
- package/dist/data/template-matcher.js.map +1 -1
- package/dist/generators/core-resolver.d.ts +7 -13
- package/dist/generators/core-resolver.d.ts.map +1 -1
- package/dist/generators/core-resolver.js +7 -84
- package/dist/generators/core-resolver.js.map +1 -1
- package/dist/generators/css-generator.d.ts +8 -26
- package/dist/generators/css-generator.d.ts.map +1 -1
- package/dist/generators/css-generator.js +17 -265
- package/dist/generators/css-generator.js.map +1 -1
- package/dist/generators/index.d.ts +2 -2
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +4 -2
- package/dist/generators/index.js.map +1 -1
- package/dist/schemas/mcp-schemas.d.ts +385 -385
- package/dist/schemas/mcp-schemas.d.ts.map +1 -1
- package/dist/tools/export-screen.d.ts.map +1 -1
- package/dist/tools/export-screen.js +6 -9
- package/dist/tools/export-screen.js.map +1 -1
- package/dist/tools/get-screen-generation-context.d.ts +1 -0
- package/dist/tools/get-screen-generation-context.d.ts.map +1 -1
- package/dist/tools/get-screen-generation-context.js +40 -26
- package/dist/tools/get-screen-generation-context.js.map +1 -1
- package/dist/tools/list-components.d.ts +2 -1
- package/dist/tools/list-components.d.ts.map +1 -1
- package/dist/tools/list-components.js +25 -14
- package/dist/tools/list-components.js.map +1 -1
- package/dist/tools/list-icon-libraries.d.ts.map +1 -1
- package/dist/tools/list-icon-libraries.js +5 -8
- package/dist/tools/list-icon-libraries.js.map +1 -1
- package/dist/tools/list-screen-templates.d.ts +2 -1
- package/dist/tools/list-screen-templates.d.ts.map +1 -1
- package/dist/tools/list-screen-templates.js +29 -25
- package/dist/tools/list-screen-templates.js.map +1 -1
- package/dist/tools/list-themes.d.ts.map +1 -1
- package/dist/tools/list-themes.js +6 -1
- package/dist/tools/list-themes.js.map +1 -1
- package/dist/tools/list-tokens.d.ts +2 -1
- package/dist/tools/list-tokens.d.ts.map +1 -1
- package/dist/tools/list-tokens.js +12 -36
- package/dist/tools/list-tokens.js.map +1 -1
- package/dist/tools/preview-component.d.ts +1 -0
- package/dist/tools/preview-component.d.ts.map +1 -1
- package/dist/tools/preview-component.js +33 -28
- package/dist/tools/preview-component.js.map +1 -1
- package/dist/tools/preview-icon-library.d.ts.map +1 -1
- package/dist/tools/preview-icon-library.js +5 -7
- package/dist/tools/preview-icon-library.js.map +1 -1
- package/dist/tools/preview-screen-template.d.ts +1 -0
- package/dist/tools/preview-screen-template.d.ts.map +1 -1
- package/dist/tools/preview-screen-template.js +18 -9
- package/dist/tools/preview-screen-template.js.map +1 -1
- package/dist/tools/preview-theme.d.ts.map +1 -1
- package/dist/tools/preview-theme.js +6 -15
- package/dist/tools/preview-theme.js.map +1 -1
- package/dist/tools/validate-screen-definition.d.ts.map +1 -1
- package/dist/tools/validate-screen-definition.js +53 -12
- package/dist/tools/validate-screen-definition.js.map +1 -1
- 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`
|
|
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/ #
|
|
828
|
-
│ │ ├──
|
|
829
|
-
│ │
|
|
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
|
-
-
|
|
887
|
-
-
|
|
887
|
+
- 29 test files
|
|
888
|
+
- 290 test cases
|
|
888
889
|
- 100% pass rate
|
|
889
890
|
- Zero failures
|
|
890
891
|
|
|
891
|
-
##
|
|
892
|
+
## Architecture: API-Based Data Sources (v0.6.0)
|
|
892
893
|
|
|
893
|
-
|
|
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
|
-
**
|
|
896
|
+
**Data Client** (`src/api/data-client.ts`):
|
|
896
897
|
|
|
897
|
-
- `
|
|
898
|
-
- `
|
|
899
|
-
- `
|
|
900
|
-
- `
|
|
901
|
-
- `
|
|
902
|
-
- `
|
|
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
|
-
|
|
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**:
|
|
1025
|
-
**Last Updated**: 2026-
|
|
1026
|
-
**SPEC**: SPEC-MCP-002 v2.0.0, SPEC-LAYOUT-002 Phase 4, SPEC-MCP-003 v1.0.0
|
|
1027
|
-
**Total 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 @@
|
|
|
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
|