Skip to content

Commit 41afdc1

Browse files
Copilothustcc
andcommitted
Add vitest unit tests and GitHub Actions workflow
Co-authored-by: hustcc <7856674+hustcc@users.noreply.github.com>
1 parent afd04a9 commit 41afdc1

8 files changed

Lines changed: 413 additions & 0 deletions

File tree

.github/workflows/test.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Unit Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- '**'
7+
pull_request:
8+
branches:
9+
- '**'
10+
11+
jobs:
12+
test:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- name: Checkout repository
17+
uses: actions/checkout@v4
18+
19+
- name: Setup Node.js
20+
uses: actions/setup-node@v4
21+
with:
22+
node-version: '18'
23+
24+
- name: Install dependencies
25+
run: npm install
26+
27+
- name: Run tests
28+
run: npm test

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ venv/
2525
ENV/
2626
env/
2727

28+
# Node.js
29+
node_modules/
30+
package-lock.json
31+
npm-debug.log*
32+
yarn-debug.log*
33+
yarn-error.log*
34+
2835
# IDEs
2936
.vscode/
3037
.idea/

__tests__/generate.test.js

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { generateChartUrl, generateMap, httpPost, CHART_TYPE_MAP } from '../skills/chart-visualization/scripts/generate.js';
3+
4+
// Mock global fetch
5+
global.fetch = vi.fn();
6+
7+
describe('generate.js - Chart Visualization Script', () => {
8+
beforeEach(() => {
9+
vi.clearAllMocks();
10+
});
11+
12+
describe('CHART_TYPE_MAP', () => {
13+
it('should contain all expected chart types', () => {
14+
expect(CHART_TYPE_MAP).toHaveProperty('generate_line_chart', 'line');
15+
expect(CHART_TYPE_MAP).toHaveProperty('generate_bar_chart', 'bar');
16+
expect(CHART_TYPE_MAP).toHaveProperty('generate_pie_chart', 'pie');
17+
expect(CHART_TYPE_MAP).toHaveProperty('generate_area_chart', 'area');
18+
expect(CHART_TYPE_MAP).toHaveProperty('generate_scatter_chart', 'scatter');
19+
});
20+
21+
it('should have 25 chart types', () => {
22+
expect(Object.keys(CHART_TYPE_MAP)).toHaveLength(25);
23+
});
24+
25+
it('should map district map correctly', () => {
26+
expect(CHART_TYPE_MAP.generate_district_map).toBe('district-map');
27+
});
28+
29+
it('should map word cloud correctly', () => {
30+
expect(CHART_TYPE_MAP.generate_word_cloud_chart).toBe('word-cloud');
31+
});
32+
});
33+
34+
describe('httpPost', () => {
35+
it('should make POST request with correct payload', async () => {
36+
const mockResponse = { success: true, data: 'test' };
37+
global.fetch.mockResolvedValueOnce({
38+
ok: true,
39+
json: async () => mockResponse,
40+
});
41+
42+
const result = await httpPost('https://example.com', { test: 'data' });
43+
44+
expect(global.fetch).toHaveBeenCalledWith('https://example.com', {
45+
method: 'POST',
46+
headers: {
47+
'Content-Type': 'application/json',
48+
},
49+
body: JSON.stringify({ test: 'data' }),
50+
});
51+
expect(result).toEqual(mockResponse);
52+
});
53+
54+
it('should throw error on failed request', async () => {
55+
global.fetch.mockResolvedValueOnce({
56+
ok: false,
57+
status: 404,
58+
text: async () => 'Not found',
59+
});
60+
61+
await expect(httpPost('https://example.com', {})).rejects.toThrow('HTTP 404: Not found');
62+
});
63+
});
64+
65+
describe('generateChartUrl', () => {
66+
it('should generate chart URL successfully', async () => {
67+
const mockResponse = {
68+
success: true,
69+
resultObj: 'https://example.com/chart.png',
70+
};
71+
global.fetch.mockResolvedValueOnce({
72+
ok: true,
73+
json: async () => mockResponse,
74+
});
75+
76+
const result = await generateChartUrl('line', { data: [1, 2, 3] });
77+
78+
expect(result).toBe('https://example.com/chart.png');
79+
const callArgs = global.fetch.mock.calls[0][1];
80+
const payload = JSON.parse(callArgs.body);
81+
expect(payload.type).toBe('line');
82+
expect(payload.source).toBe('chart-visualization-creator');
83+
expect(payload.data).toEqual([1, 2, 3]);
84+
});
85+
86+
it('should throw error when API returns success: false', async () => {
87+
global.fetch.mockResolvedValueOnce({
88+
ok: true,
89+
json: async () => ({ success: false, errorMessage: 'Test error' }),
90+
});
91+
92+
await expect(generateChartUrl('line', {})).rejects.toThrow('Test error');
93+
});
94+
95+
it('should throw error with default message when errorMessage is missing', async () => {
96+
global.fetch.mockResolvedValueOnce({
97+
ok: true,
98+
json: async () => ({ success: false }),
99+
});
100+
101+
await expect(generateChartUrl('line', {})).rejects.toThrow('Unknown error');
102+
});
103+
});
104+
105+
describe('generateMap', () => {
106+
it('should generate map successfully', async () => {
107+
const mockResponse = {
108+
success: true,
109+
resultObj: { content: [{ type: 'text', text: 'Map URL' }] },
110+
};
111+
global.fetch.mockResolvedValueOnce({
112+
ok: true,
113+
json: async () => mockResponse,
114+
});
115+
116+
const result = await generateMap('generate_district_map', { region: 'test' });
117+
118+
expect(result).toEqual({ content: [{ type: 'text', text: 'Map URL' }] });
119+
const callArgs = global.fetch.mock.calls[0][1];
120+
const payload = JSON.parse(callArgs.body);
121+
expect(payload.tool).toBe('generate_district_map');
122+
expect(payload.source).toBe('chart-visualization-creator');
123+
expect(payload.input).toEqual({ region: 'test' });
124+
});
125+
126+
it('should include serviceId when available', async () => {
127+
process.env.SERVICE_ID = 'test-service-id';
128+
129+
global.fetch.mockResolvedValueOnce({
130+
ok: true,
131+
json: async () => ({ success: true, resultObj: {} }),
132+
});
133+
134+
await generateMap('generate_pin_map', {});
135+
136+
const callArgs = global.fetch.mock.calls[0][1];
137+
const payload = JSON.parse(callArgs.body);
138+
expect(payload.serviceId).toBe('test-service-id');
139+
140+
delete process.env.SERVICE_ID;
141+
});
142+
143+
it('should throw error when API returns success: false', async () => {
144+
global.fetch.mockResolvedValueOnce({
145+
ok: true,
146+
json: async () => ({ success: false, errorMessage: 'Map generation failed' }),
147+
});
148+
149+
await expect(generateMap('generate_district_map', {})).rejects.toThrow('Map generation failed');
150+
});
151+
});
152+
});

__tests__/search.test.js

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { searchIcons } from '../skills/icon-retrieval/scripts/search.js';
3+
4+
// Mock global fetch
5+
global.fetch = vi.fn();
6+
7+
describe('search.js - Icon Retrieval Script', () => {
8+
beforeEach(() => {
9+
vi.clearAllMocks();
10+
});
11+
12+
describe('searchIcons', () => {
13+
it('should search icons and return results', async () => {
14+
const mockApiResponse = {
15+
status: true,
16+
data: {
17+
success: true,
18+
data: ['https://example.com/icon1.svg', 'https://example.com/icon2.svg'],
19+
},
20+
};
21+
22+
const mockSvgContent = '<svg>Icon</svg>';
23+
24+
// Mock API call
25+
global.fetch
26+
.mockResolvedValueOnce({
27+
ok: true,
28+
json: async () => mockApiResponse,
29+
})
30+
// Mock SVG fetches
31+
.mockResolvedValueOnce({
32+
ok: true,
33+
text: async () => mockSvgContent,
34+
})
35+
.mockResolvedValueOnce({
36+
ok: true,
37+
text: async () => mockSvgContent,
38+
});
39+
40+
const results = await searchIcons('test', 2);
41+
42+
expect(results).toHaveLength(2);
43+
expect(results[0].url).toBe('https://example.com/icon1.svg');
44+
expect(results[0].svg).toBe(mockSvgContent);
45+
expect(results[1].url).toBe('https://example.com/icon2.svg');
46+
expect(results[1].svg).toBe(mockSvgContent);
47+
});
48+
49+
it('should use default topK of 5', async () => {
50+
global.fetch.mockResolvedValueOnce({
51+
ok: true,
52+
json: async () => ({
53+
status: true,
54+
data: { success: true, data: [] },
55+
}),
56+
});
57+
58+
await searchIcons('test');
59+
60+
const callUrl = global.fetch.mock.calls[0][0];
61+
expect(callUrl).toContain('topK=5');
62+
});
63+
64+
it('should use custom topK value', async () => {
65+
global.fetch.mockResolvedValueOnce({
66+
ok: true,
67+
json: async () => ({
68+
status: true,
69+
data: { success: true, data: [] },
70+
}),
71+
});
72+
73+
await searchIcons('test', 10);
74+
75+
const callUrl = global.fetch.mock.calls[0][0];
76+
expect(callUrl).toContain('topK=10');
77+
});
78+
79+
it('should throw error on API failure (status: false)', async () => {
80+
global.fetch.mockResolvedValueOnce({
81+
ok: true,
82+
json: async () => ({
83+
status: false,
84+
message: 'API error',
85+
}),
86+
});
87+
88+
await expect(searchIcons('test')).rejects.toThrow('API error');
89+
});
90+
91+
it('should throw error when data.success is false', async () => {
92+
global.fetch.mockResolvedValueOnce({
93+
ok: true,
94+
json: async () => ({
95+
status: true,
96+
data: { success: false },
97+
message: 'Data error',
98+
}),
99+
});
100+
101+
await expect(searchIcons('test')).rejects.toThrow('Data error');
102+
});
103+
104+
it('should throw default error message when message is missing', async () => {
105+
global.fetch.mockResolvedValueOnce({
106+
ok: true,
107+
json: async () => ({
108+
status: false,
109+
}),
110+
});
111+
112+
await expect(searchIcons('test')).rejects.toThrow('API request failed');
113+
});
114+
115+
it('should throw error on HTTP error', async () => {
116+
global.fetch.mockResolvedValueOnce({
117+
ok: false,
118+
status: 500,
119+
text: async () => 'Server error',
120+
});
121+
122+
await expect(searchIcons('test')).rejects.toThrow('HTTP 500: Server error');
123+
});
124+
125+
it('should handle failed SVG fetches gracefully', async () => {
126+
const mockApiResponse = {
127+
status: true,
128+
data: {
129+
success: true,
130+
data: ['https://example.com/icon1.svg', 'https://example.com/icon2.svg'],
131+
},
132+
};
133+
134+
// Mock API call
135+
global.fetch
136+
.mockResolvedValueOnce({
137+
ok: true,
138+
json: async () => mockApiResponse,
139+
})
140+
// First SVG fetch succeeds
141+
.mockResolvedValueOnce({
142+
ok: true,
143+
text: async () => '<svg>Icon1</svg>',
144+
})
145+
// Second SVG fetch fails
146+
.mockResolvedValueOnce({
147+
ok: false,
148+
status: 404,
149+
});
150+
151+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
152+
153+
const results = await searchIcons('test', 2);
154+
155+
expect(results).toHaveLength(1);
156+
expect(results[0].url).toBe('https://example.com/icon1.svg');
157+
expect(consoleErrorSpy).toHaveBeenCalledWith(
158+
expect.stringContaining('Warning: Failed to fetch SVG')
159+
);
160+
161+
consoleErrorSpy.mockRestore();
162+
});
163+
164+
it('should encode query parameters correctly', async () => {
165+
global.fetch.mockResolvedValueOnce({
166+
ok: true,
167+
json: async () => ({
168+
status: true,
169+
data: { success: true, data: [] },
170+
}),
171+
});
172+
173+
await searchIcons('test & special', 5);
174+
175+
const callUrl = global.fetch.mock.calls[0][0];
176+
expect(callUrl).toContain('test');
177+
expect(callUrl).toContain('special');
178+
// URLSearchParams should encode the ampersand
179+
expect(callUrl).toContain('%26');
180+
});
181+
182+
it('should return empty array when no icons found', async () => {
183+
global.fetch.mockResolvedValueOnce({
184+
ok: true,
185+
json: async () => ({
186+
status: true,
187+
data: { success: true, data: [] },
188+
}),
189+
});
190+
191+
const results = await searchIcons('test');
192+
193+
expect(results).toEqual([]);
194+
});
195+
});
196+
});

0 commit comments

Comments
 (0)