Skip to content

Commit dc89a93

Browse files
committed
Lots of really shitty tests. GOtta get the coverage up.
1 parent 297293e commit dc89a93

15 files changed

+1922
-11
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
"test:runtime": "vitest run tests/unit/background.test.js tests/unit/content_bridge.test.js tests/unit/offscreen.test.js tests/unit/userscript_adapter.test.js tests/unit/script_registry.test.js tests/unit/browser_adapter.test.js",
3636
"test:i18n": "node scripts/check-i18n.js",
3737
"prepare": "husky",
38-
"precommit:run": "npm run lint && npm run format:check && npm run test:i18n && npm run test"
38+
"precommit:run": "npm run lint && npm run format:check && npm run test:i18n && npm run test",
39+
"pre-commit": "npm run lint && npm run format:check && npm run test:i18n && npm run test:coverage"
3940
},
4041
"dependencies": {
4142
"@babel/runtime": "7.28.4",

src/ai_dom_editor/editor/helpers/userscript_handler.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,12 @@ export class UserscriptHandler {
217217
async updateUserscript(scriptName, newCode, newName = null, explanation = null) {
218218
try {
219219
const { scripts = [] } = await chrome.storage.local.get('scripts');
220-
const scriptToUpdate = scripts.find((s) => s.name === scriptName);
220+
221+
// Fallback to currentScript if name doesn't match exactly
222+
let scriptToUpdate = scripts.find((s) => s.name === scriptName);
223+
if (!scriptToUpdate && this.editor.currentScript) {
224+
scriptToUpdate = scripts.find((s) => s.id === this.editor.currentScript.id);
225+
}
221226

222227
if (!scriptToUpdate) {
223228
throw new Error(`Script "${scriptName}" not found for update.`);

src/utils/ai_diff_helper.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { applyPatch as standardApplyPatch, diffLines, createPatch } from 'diff';
44
const dmp = new diff_match_patch();
55
dmp.Match_Threshold = 0.5;
66
dmp.Patch_Margin = 4;
7+
dmp.Match_MaxBits = 1024; // Support longer search patterns
78

89
export class AIDiffHelper {
910
static createUnifiedDiff(oldCode, newCode, fileName = 'script.user.js') {

tests/unit/ai_code_manager.test.js

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { AICodeManager } from '../../src/editor/ai_code_manager.js';
3+
4+
describe('AICodeManager', () => {
5+
let aiCodeManager;
6+
let mockElements;
7+
let mockState;
8+
let mockCodeEditorManager;
9+
10+
beforeEach(() => {
11+
vi.restoreAllMocks();
12+
13+
mockElements = {
14+
aiChatHistory: document.createElement('div'),
15+
aiChatInput: document.createElement('textarea'),
16+
aiSendBtn: document.createElement('button'),
17+
aiClearBtn: document.createElement('button'),
18+
aiModelSelector: document.createElement('select'),
19+
aiConfigWarning: document.createElement('div'),
20+
aiChatContainer: document.createElement('div'),
21+
};
22+
23+
mockState = {
24+
hasUnsavedChanges: false,
25+
};
26+
27+
mockCodeEditorManager = {
28+
getValue: vi.fn().mockReturnValue('// current code'),
29+
setValue: vi.fn(),
30+
};
31+
32+
global.chrome = {
33+
storage: {
34+
local: {
35+
get: vi.fn(async (keys) => {
36+
const data = {
37+
aiDomEditorConfigs: [],
38+
availableModels: [],
39+
lastAiModel: null,
40+
};
41+
if (Array.isArray(keys)) {
42+
const res = {};
43+
keys.forEach((k) => (res[k] = data[k]));
44+
return res;
45+
}
46+
if (typeof keys === 'string') {
47+
return { [keys]: data[keys] };
48+
}
49+
return data;
50+
}),
51+
set: vi.fn().mockResolvedValue(undefined),
52+
},
53+
onChanged: {
54+
addListener: vi.fn(),
55+
},
56+
},
57+
};
58+
59+
global.fetch = vi.fn();
60+
61+
aiCodeManager = new AICodeManager(mockElements, mockState, mockCodeEditorManager);
62+
});
63+
64+
describe('clearHistory', () => {
65+
it('should clear chat history array and UI', () => {
66+
aiCodeManager.chatHistory = [{ role: 'user', text: 'hi' }];
67+
aiCodeManager.clearHistory();
68+
expect(aiCodeManager.chatHistory).toHaveLength(0);
69+
expect(mockElements.aiChatHistory.innerHTML).toContain('ai-message');
70+
});
71+
});
72+
73+
describe('parseAIContent', () => {
74+
it('should parse search-replace patches', () => {
75+
const content = '<<<<<< SEARCH\nold\n======\nnew\n>>>>>> REPLACE';
76+
const result = aiCodeManager.parseAIContent(content);
77+
expect(result.type).toBe('patch');
78+
expect(result.patches).toHaveLength(1);
79+
});
80+
81+
it('should diff full code blocks against current code', () => {
82+
const content = '\`\`\`javascript\nconst x = 2;\n\`\`\`';
83+
mockCodeEditorManager.getValue.mockReturnValue('const x = 1;');
84+
85+
const result = aiCodeManager.parseAIContent(content);
86+
expect(result.type).toBe('patch');
87+
expect(result.patches[0].type).toBe('unified');
88+
});
89+
90+
it('should return plain code if current code is empty', () => {
91+
const content = '\`\`\`javascript\nconst x = 2;\n\`\`\`';
92+
mockCodeEditorManager.getValue.mockReturnValue('');
93+
94+
const result = aiCodeManager.parseAIContent(content);
95+
expect(result.type).toBe('code');
96+
expect(result.code).toBe('const x = 2;');
97+
});
98+
});
99+
100+
describe('loadAIConfig', () => {
101+
it('should update UI when valid config found', async () => {
102+
chrome.storage.local.get.mockResolvedValue({
103+
aiDomEditorConfigs: [{ apiKey: 'key', endpoint: 'end', model: 'gpt-4' }],
104+
});
105+
await aiCodeManager.loadAIConfig();
106+
expect(mockElements.aiConfigWarning.classList.contains('hidden')).toBe(true);
107+
expect(mockElements.aiChatContainer.classList.contains('hidden')).toBe(false);
108+
});
109+
110+
it('should show warning when no config found', async () => {
111+
chrome.storage.local.get.mockResolvedValue({ aiDomEditorConfigs: [] });
112+
await aiCodeManager.loadAIConfig();
113+
expect(mockElements.aiConfigWarning.classList.contains('hidden')).toBe(false);
114+
expect(mockElements.aiChatContainer.classList.contains('hidden')).toBe(true);
115+
});
116+
});
117+
118+
describe('handleSendMessage', () => {
119+
it('should add user message and call AI', async () => {
120+
mockElements.aiChatInput.value = 'hello ai';
121+
vi.spyOn(aiCodeManager, 'callAI').mockImplementation(async () => {});
122+
123+
await aiCodeManager.handleSendMessage();
124+
125+
expect(aiCodeManager.chatHistory[0].text).toBe('hello ai');
126+
expect(aiCodeManager.callAI).toHaveBeenCalledWith('hello ai');
127+
expect(mockElements.aiChatInput.value).toBe('');
128+
});
129+
});
130+
131+
describe('callAI', () => {
132+
it('should handle successful AI response with code', async () => {
133+
chrome.storage.local.get.mockResolvedValue({
134+
aiDomEditorConfigs: [
135+
{ apiKey: 'key', endpoint: 'http://ai.test', model: 'test', provider: 'openai' },
136+
],
137+
});
138+
await aiCodeManager.loadAIConfig();
139+
140+
global.fetch.mockResolvedValue({
141+
ok: true,
142+
json: async () => ({
143+
choices: [
144+
{ message: { content: 'Explanation\n\`\`\`javascript\nconsole.log(1);\n\`\`\`' } },
145+
],
146+
}),
147+
});
148+
149+
await aiCodeManager.callAI('hello');
150+
151+
expect(aiCodeManager.chatHistory).toHaveLength(1);
152+
expect(aiCodeManager.chatHistory[0].data.type).toBe('patch');
153+
});
154+
155+
it('should handle AI error response', async () => {
156+
chrome.storage.local.get.mockResolvedValue({
157+
aiDomEditorConfigs: [
158+
{ apiKey: 'key', endpoint: 'http://ai.test', model: 'test', provider: 'openai' },
159+
],
160+
});
161+
await aiCodeManager.loadAIConfig();
162+
163+
global.fetch.mockResolvedValue({
164+
ok: false,
165+
statusText: 'Service Unavailable',
166+
json: async () => ({ error: { message: 'Failed' } }),
167+
});
168+
169+
await aiCodeManager.callAI('hello');
170+
171+
expect(aiCodeManager.chatHistory).toHaveLength(1);
172+
expect(aiCodeManager.chatHistory[0].text).toContain('Error');
173+
});
174+
});
175+
176+
describe('handleModelChange', () => {
177+
it('should update selectedModel and save to storage', async () => {
178+
const model = { id: 'new-model', provider: 'openai' };
179+
const event = { target: { value: JSON.stringify(model) } };
180+
181+
await aiCodeManager.handleModelChange(event);
182+
183+
expect(aiCodeManager.selectedModel).toEqual(model);
184+
expect(chrome.storage.local.set).toHaveBeenCalledWith({ lastAiModel: model });
185+
});
186+
});
187+
188+
describe('getActiveConfig', () => {
189+
it('should prioritize selectedModel over apiConfig', () => {
190+
aiCodeManager.apiConfig = {
191+
apiKey: 'api-key',
192+
endpoint: 'api-end',
193+
model: 'api-model',
194+
provider: 'openai',
195+
};
196+
aiCodeManager.selectedModel = {
197+
apiKey: 'sel-key',
198+
endpoint: 'sel-end',
199+
id: 'sel-id',
200+
provider: 'anthropic',
201+
};
202+
203+
const active = aiCodeManager.getActiveConfig();
204+
205+
expect(active.apiKey).toBe('sel-key');
206+
expect(active.provider).toBe('anthropic');
207+
expect(active.model).toBe('sel-id');
208+
});
209+
210+
it('should fallback to apiConfig if selectedModel is missing properties', () => {
211+
aiCodeManager.apiConfig = {
212+
apiKey: 'api-key',
213+
endpoint: 'api-end',
214+
model: 'api-model',
215+
provider: 'openai',
216+
};
217+
aiCodeManager.selectedModel = { id: 'sel-id' };
218+
219+
const active = aiCodeManager.getActiveConfig();
220+
221+
expect(active.apiKey).toBe('api-key');
222+
expect(active.model).toBe('sel-id');
223+
});
224+
});
225+
226+
describe('renderModelSelector', () => {
227+
it('should show "No models available" if list is empty', () => {
228+
aiCodeManager.availableModels = [];
229+
aiCodeManager.renderModelSelector(null);
230+
expect(mockElements.aiModelSelector.textContent).toContain('No models available');
231+
});
232+
233+
it('should select the last used model if found', () => {
234+
const model1 = { id: 'm1', provider: 'p1' };
235+
const model2 = { id: 'm2', provider: 'p2' };
236+
aiCodeManager.availableModels = [model1, model2];
237+
aiCodeManager.renderModelSelector(model2);
238+
expect(aiCodeManager.selectedModel).toEqual(model2);
239+
expect(mockElements.aiModelSelector.value).toBe(JSON.stringify(model2));
240+
});
241+
242+
it('should default to first model if last used not found', () => {
243+
const model1 = { id: 'm1', provider: 'p1' };
244+
aiCodeManager.availableModels = [model1];
245+
aiCodeManager.renderModelSelector({ id: 'missing' });
246+
expect(aiCodeManager.selectedModel).toEqual(model1);
247+
});
248+
});
249+
250+
describe('setThinking', () => {
251+
it('should update isThinking state and button disabled property', () => {
252+
aiCodeManager.setThinking(true);
253+
expect(aiCodeManager.isThinking).toBe(true);
254+
expect(mockElements.aiSendBtn.disabled).toBe(true);
255+
256+
aiCodeManager.setThinking(false);
257+
expect(aiCodeManager.isThinking).toBe(false);
258+
expect(mockElements.aiSendBtn.disabled).toBe(false);
259+
});
260+
});
261+
});

0 commit comments

Comments
 (0)