gh-here 3.0.2 ā 3.1.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.
- package/.env +0 -0
- package/.playwright-mcp/fixed-alignment.png +0 -0
- package/.playwright-mcp/fixed-layout.png +0 -0
- package/.playwright-mcp/gh-here-home-header-table.png +0 -0
- package/.playwright-mcp/gh-here-home.png +0 -0
- package/.playwright-mcp/line-selection-multiline.png +0 -0
- package/.playwright-mcp/line-selection-test-after.png +0 -0
- package/.playwright-mcp/line-selection-test-before.png +0 -0
- package/.playwright-mcp/page-2026-01-03T17-58-21-336Z.png +0 -0
- package/lib/constants.js +25 -15
- package/lib/content-search.js +212 -0
- package/lib/error-handler.js +39 -28
- package/lib/file-utils.js +438 -287
- package/lib/git.js +10 -54
- package/lib/gitignore.js +70 -41
- package/lib/renderers.js +15 -19
- package/lib/server.js +70 -193
- package/lib/symbol-parser.js +600 -0
- package/package.json +1 -1
- package/public/app.js +207 -73
- package/public/js/constants.js +50 -34
- package/public/js/content-search-handler.js +551 -0
- package/public/js/file-viewer.js +437 -0
- package/public/js/focus-mode.js +280 -0
- package/public/js/inline-search.js +659 -0
- package/public/js/modal-manager.js +14 -28
- package/public/js/navigation.js +5 -0
- package/public/js/symbol-outline.js +454 -0
- package/public/js/utils.js +152 -94
- package/public/styles.css +2049 -296
- package/.claude/settings.local.json +0 -30
- package/SAMPLE.md +0 -287
- package/lib/validation.js +0 -77
- package/public/app.js.backup +0 -1902
- package/public/js/draft-manager.js +0 -36
- package/public/js/editor-manager.js +0 -159
- package/test.js +0 -138
- package/tests/draftManager.test.js +0 -241
- package/tests/fileTypeDetection.test.js +0 -111
- package/tests/httpService.test.js +0 -268
- package/tests/languageDetection.test.js +0 -145
- package/tests/pathUtils.test.js +0 -136
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Draft management for file editing
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { STORAGE_KEYS } from './constants.js';
|
|
6
|
-
|
|
7
|
-
export const DraftManager = {
|
|
8
|
-
saveDraft(filePath, content) {
|
|
9
|
-
localStorage.setItem(`${STORAGE_KEYS.DRAFT_PREFIX}${filePath}`, content);
|
|
10
|
-
},
|
|
11
|
-
|
|
12
|
-
loadDraft(filePath) {
|
|
13
|
-
return localStorage.getItem(`${STORAGE_KEYS.DRAFT_PREFIX}${filePath}`);
|
|
14
|
-
},
|
|
15
|
-
|
|
16
|
-
clearDraft(filePath) {
|
|
17
|
-
localStorage.removeItem(`${STORAGE_KEYS.DRAFT_PREFIX}${filePath}`);
|
|
18
|
-
},
|
|
19
|
-
|
|
20
|
-
hasDraftChanges(filePath, originalContent) {
|
|
21
|
-
const draft = this.loadDraft(filePath);
|
|
22
|
-
return draft !== null && draft !== originalContent;
|
|
23
|
-
},
|
|
24
|
-
|
|
25
|
-
getAllDrafts() {
|
|
26
|
-
const drafts = {};
|
|
27
|
-
for (let i = 0; i < localStorage.length; i++) {
|
|
28
|
-
const key = localStorage.key(i);
|
|
29
|
-
if (key.startsWith(STORAGE_KEYS.DRAFT_PREFIX)) {
|
|
30
|
-
const filePath = key.replace(STORAGE_KEYS.DRAFT_PREFIX, '');
|
|
31
|
-
drafts[filePath] = localStorage.getItem(key);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return drafts;
|
|
35
|
-
}
|
|
36
|
-
};
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Monaco Editor management
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { CONFIG, EDITOR_OPTIONS } from './constants.js';
|
|
6
|
-
import { getLanguageFromExtension } from './utils.js';
|
|
7
|
-
import { DraftManager } from './draft-manager.js';
|
|
8
|
-
import { showDraftDialog } from './modal-manager.js';
|
|
9
|
-
import { showNotification } from './notification.js';
|
|
10
|
-
|
|
11
|
-
export class EditorManager {
|
|
12
|
-
constructor(theme) {
|
|
13
|
-
this.fileEditor = null;
|
|
14
|
-
this.newFileEditor = null;
|
|
15
|
-
this.theme = theme;
|
|
16
|
-
this.ready = false;
|
|
17
|
-
this.init();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
init() {
|
|
21
|
-
if (typeof require === 'undefined') {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
require.config({ paths: { vs: CONFIG.MONACO_CDN } });
|
|
26
|
-
|
|
27
|
-
require(['vs/editor/editor.main'], () => {
|
|
28
|
-
self.MonacoEnvironment = {
|
|
29
|
-
getWorker: () => undefined
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const monacoTheme = this.theme === 'dark' ? 'vs-dark' : 'vs';
|
|
33
|
-
monaco.editor.setTheme(monacoTheme);
|
|
34
|
-
|
|
35
|
-
this.initializeNewFileEditor();
|
|
36
|
-
this.ready = true;
|
|
37
|
-
window.monacoReady = true;
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
initializeNewFileEditor() {
|
|
42
|
-
const container = document.getElementById('new-file-content');
|
|
43
|
-
if (!container) {
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const monacoTheme = this.theme === 'dark' ? 'vs-dark' : 'vs';
|
|
48
|
-
this.newFileEditor = monaco.editor.create(container, {
|
|
49
|
-
...EDITOR_OPTIONS,
|
|
50
|
-
value: '',
|
|
51
|
-
language: 'plaintext',
|
|
52
|
-
theme: monacoTheme
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async createFileEditor(container, filePath, originalContent) {
|
|
57
|
-
if (!this.ready) {
|
|
58
|
-
await this.waitForReady();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const filename = filePath.split('/').pop() || 'file.txt';
|
|
62
|
-
const language = getLanguageFromExtension(filename);
|
|
63
|
-
const availableLanguages = monaco.languages.getLanguages().map(lang => lang.id);
|
|
64
|
-
const validLanguage = availableLanguages.includes(language) ? language : 'plaintext';
|
|
65
|
-
const monacoTheme = this.theme === 'dark' ? 'vs-dark' : 'vs';
|
|
66
|
-
|
|
67
|
-
let contentToLoad = originalContent;
|
|
68
|
-
|
|
69
|
-
if (DraftManager.hasDraftChanges(filePath, originalContent)) {
|
|
70
|
-
const draftChoice = await showDraftDialog(filePath);
|
|
71
|
-
if (draftChoice === 'load') {
|
|
72
|
-
contentToLoad = DraftManager.loadDraft(filePath);
|
|
73
|
-
} else if (draftChoice === 'discard') {
|
|
74
|
-
DraftManager.clearDraft(filePath);
|
|
75
|
-
}
|
|
76
|
-
} else {
|
|
77
|
-
const draft = DraftManager.loadDraft(filePath);
|
|
78
|
-
if (draft && draft === originalContent) {
|
|
79
|
-
DraftManager.clearDraft(filePath);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (!this.fileEditor) {
|
|
84
|
-
this.fileEditor = monaco.editor.create(container, {
|
|
85
|
-
...EDITOR_OPTIONS,
|
|
86
|
-
value: contentToLoad,
|
|
87
|
-
language: validLanguage,
|
|
88
|
-
theme: monacoTheme
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
this.fileEditor.onDidChangeModelContent(() => {
|
|
92
|
-
DraftManager.saveDraft(filePath, this.fileEditor.getValue());
|
|
93
|
-
});
|
|
94
|
-
} else {
|
|
95
|
-
this.fileEditor.setValue(contentToLoad);
|
|
96
|
-
const model = this.fileEditor.getModel();
|
|
97
|
-
if (model) {
|
|
98
|
-
monaco.editor.setModelLanguage(model, validLanguage);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
setTimeout(() => this.fileEditor.layout(), 50);
|
|
103
|
-
|
|
104
|
-
return this.fileEditor;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
waitForReady() {
|
|
108
|
-
return new Promise(resolve => {
|
|
109
|
-
const check = () => {
|
|
110
|
-
if (this.ready) {
|
|
111
|
-
resolve();
|
|
112
|
-
} else {
|
|
113
|
-
setTimeout(check, 100);
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
check();
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
updateLanguage(filename) {
|
|
121
|
-
if (this.newFileEditor) {
|
|
122
|
-
const language = getLanguageFromExtension(filename);
|
|
123
|
-
const model = this.newFileEditor.getModel();
|
|
124
|
-
if (model) {
|
|
125
|
-
monaco.editor.setModelLanguage(model, language);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
toggleWordWrap(editor) {
|
|
131
|
-
if (!editor) {
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const currentWrap = editor.getOption(monaco.editor.EditorOption.wordWrap);
|
|
136
|
-
const newWrap = currentWrap === 'off' ? 'on' : 'off';
|
|
137
|
-
editor.updateOptions({ wordWrap: newWrap });
|
|
138
|
-
return newWrap === 'on';
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
getValue(editorType = 'file') {
|
|
142
|
-
const editor = editorType === 'file' ? this.fileEditor : this.newFileEditor;
|
|
143
|
-
return editor ? editor.getValue() : '';
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
focus(editorType = 'file') {
|
|
147
|
-
const editor = editorType === 'file' ? this.fileEditor : this.newFileEditor;
|
|
148
|
-
if (editor) {
|
|
149
|
-
editor.focus();
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
layout(editorType = 'file') {
|
|
154
|
-
const editor = editorType === 'file' ? this.fileEditor : this.newFileEditor;
|
|
155
|
-
if (editor) {
|
|
156
|
-
editor.layout();
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
package/test.js
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lightweight smoke tests for gh-here
|
|
3
|
-
* Run with: node test.js
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const { chromium } = require('playwright');
|
|
7
|
-
const { spawn } = require('child_process');
|
|
8
|
-
|
|
9
|
-
const TEST_PORT = 5556; // Use different port to avoid conflicts
|
|
10
|
-
const BASE_URL = `http://localhost:${TEST_PORT}`;
|
|
11
|
-
|
|
12
|
-
async function sleep(ms) {
|
|
13
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async function runTests() {
|
|
17
|
-
console.log('Starting gh-here server...');
|
|
18
|
-
|
|
19
|
-
// Start server
|
|
20
|
-
const server = spawn('node', ['bin/gh-here.js', `--port=${TEST_PORT}`], {
|
|
21
|
-
cwd: __dirname,
|
|
22
|
-
stdio: 'pipe'
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
// Wait for server to start
|
|
26
|
-
await sleep(2000);
|
|
27
|
-
|
|
28
|
-
let browser;
|
|
29
|
-
let failures = 0;
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
console.log('Launching browser...');
|
|
33
|
-
browser = await chromium.launch({ headless: true });
|
|
34
|
-
const context = await browser.newContext();
|
|
35
|
-
const page = await context.newPage();
|
|
36
|
-
|
|
37
|
-
// Test 1: Root page loads
|
|
38
|
-
console.log('\nā Test 1: Root page loads');
|
|
39
|
-
await page.goto(BASE_URL);
|
|
40
|
-
const title = await page.title();
|
|
41
|
-
if (!title.includes('gh-here')) {
|
|
42
|
-
console.error(' ā FAILED: Page title incorrect');
|
|
43
|
-
failures++;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Test 2: File tree exists (may be hidden on root)
|
|
47
|
-
console.log('ā Test 2: File tree element exists');
|
|
48
|
-
const fileTreeExists = await page.$('#file-tree');
|
|
49
|
-
if (!fileTreeExists) {
|
|
50
|
-
console.error(' ā FAILED: File tree element not found');
|
|
51
|
-
failures++;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Test 3: Navigate to a file and check line numbers display correctly
|
|
55
|
-
console.log('ā Test 3: File view with line numbers');
|
|
56
|
-
await page.goto(`${BASE_URL}/?path=lib/renderers.js`);
|
|
57
|
-
await page.waitForSelector('.line-container', { timeout: 5000 });
|
|
58
|
-
|
|
59
|
-
// Check that line numbers are in a vertical column (not nested)
|
|
60
|
-
const lineContainers = await page.$$('.line-container');
|
|
61
|
-
if (lineContainers.length < 10) {
|
|
62
|
-
console.error(' ā FAILED: Not enough line containers found');
|
|
63
|
-
failures++;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Check line 1 and line 2 have sequential numbers
|
|
67
|
-
const line1 = await page.$('.line-container[data-line="1"]');
|
|
68
|
-
const line2 = await page.$('.line-container[data-line="2"]');
|
|
69
|
-
if (!line1 || !line2) {
|
|
70
|
-
console.error(' ā FAILED: Line containers missing data-line attributes');
|
|
71
|
-
failures++;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Verify line numbers are not nested (check display property)
|
|
75
|
-
const line1Display = await page.$eval('.line-container[data-line="1"]',
|
|
76
|
-
el => window.getComputedStyle(el).display
|
|
77
|
-
);
|
|
78
|
-
if (line1Display !== 'block') {
|
|
79
|
-
console.error(` ā FAILED: Line containers should have display:block, got ${line1Display}`);
|
|
80
|
-
failures++;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Test 4: Check if modified files show diff button
|
|
84
|
-
console.log('ā Test 4: Modified files show diff button');
|
|
85
|
-
await page.goto(BASE_URL);
|
|
86
|
-
const diffButtons = await page.$$('.diff-btn');
|
|
87
|
-
// Should have at least one diff button if there are modified files
|
|
88
|
-
console.log(` Found ${diffButtons.length} diff buttons`);
|
|
89
|
-
|
|
90
|
-
// Test 5: Gitignore toggle exists and is clickable
|
|
91
|
-
console.log('ā Test 5: Gitignore toggle button');
|
|
92
|
-
const gitignoreToggle = await page.$('#gitignore-toggle');
|
|
93
|
-
if (!gitignoreToggle) {
|
|
94
|
-
console.error(' ā FAILED: Gitignore toggle button not found');
|
|
95
|
-
failures++;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Test 6: Theme toggle works
|
|
99
|
-
console.log('ā Test 6: Theme toggle');
|
|
100
|
-
const themeToggle = await page.$('#theme-toggle');
|
|
101
|
-
if (!themeToggle) {
|
|
102
|
-
console.error(' ā FAILED: Theme toggle button not found');
|
|
103
|
-
failures++;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Test 7: Search functionality
|
|
107
|
-
console.log('ā Test 7: Search input exists');
|
|
108
|
-
const searchInput = await page.$('#file-search, #root-file-search');
|
|
109
|
-
if (!searchInput) {
|
|
110
|
-
console.error(' ā FAILED: Search input not found');
|
|
111
|
-
failures++;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
console.log('\n' + '='.repeat(50));
|
|
115
|
-
if (failures === 0) {
|
|
116
|
-
console.log('ā All tests passed!');
|
|
117
|
-
} else {
|
|
118
|
-
console.log(`ā ${failures} test(s) failed`);
|
|
119
|
-
}
|
|
120
|
-
console.log('='.repeat(50) + '\n');
|
|
121
|
-
|
|
122
|
-
} catch (error) {
|
|
123
|
-
console.error('\nā Test failed with error:', error.message);
|
|
124
|
-
failures++;
|
|
125
|
-
} finally {
|
|
126
|
-
if (browser) {
|
|
127
|
-
await browser.close();
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Kill server
|
|
131
|
-
server.kill();
|
|
132
|
-
|
|
133
|
-
// Exit with appropriate code
|
|
134
|
-
process.exit(failures > 0 ? 1 : 0);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
runTests();
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
// Tests for DraftManager utility
|
|
2
|
-
// Run with: node tests/draftManager.test.js
|
|
3
|
-
|
|
4
|
-
// Mock localStorage for testing
|
|
5
|
-
class MockLocalStorage {
|
|
6
|
-
constructor() {
|
|
7
|
-
this.store = {};
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
getItem(key) {
|
|
11
|
-
return this.store.hasOwnProperty(key) ? this.store[key] : null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
setItem(key, value) {
|
|
15
|
-
this.store[key] = String(value);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
removeItem(key) {
|
|
19
|
-
delete this.store[key];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
clear() {
|
|
23
|
-
this.store = {};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
key(index) {
|
|
27
|
-
const keys = Object.keys(this.store);
|
|
28
|
-
return keys[index] || null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
get length() {
|
|
32
|
-
return Object.keys(this.store).length;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Create testable version of DraftManager with injected storage
|
|
37
|
-
function createDraftManager(storage) {
|
|
38
|
-
return {
|
|
39
|
-
STORAGE_PREFIX: 'gh-here-draft-',
|
|
40
|
-
|
|
41
|
-
saveDraft(filePath, content) {
|
|
42
|
-
storage.setItem(`${this.STORAGE_PREFIX}${filePath}`, content);
|
|
43
|
-
},
|
|
44
|
-
|
|
45
|
-
loadDraft(filePath) {
|
|
46
|
-
return storage.getItem(`${this.STORAGE_PREFIX}${filePath}`);
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
clearDraft(filePath) {
|
|
50
|
-
storage.removeItem(`${this.STORAGE_PREFIX}${filePath}`);
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
hasDraftChanges(filePath, originalContent) {
|
|
54
|
-
const draft = this.loadDraft(filePath);
|
|
55
|
-
return draft !== null && draft !== originalContent;
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
getAllDrafts() {
|
|
59
|
-
const drafts = {};
|
|
60
|
-
for (let i = 0; i < storage.length; i++) {
|
|
61
|
-
const key = storage.key(i);
|
|
62
|
-
if (key && key.startsWith(this.STORAGE_PREFIX)) {
|
|
63
|
-
const filePath = key.replace(this.STORAGE_PREFIX, '');
|
|
64
|
-
drafts[filePath] = storage.getItem(key);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return drafts;
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Simple test framework
|
|
73
|
-
function test(name, fn) {
|
|
74
|
-
try {
|
|
75
|
-
fn();
|
|
76
|
-
console.log(`ā
${name}`);
|
|
77
|
-
} catch (error) {
|
|
78
|
-
console.log(`ā ${name}: ${error.message}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function assertEqual(actual, expected, message = '') {
|
|
83
|
-
if (actual !== expected) {
|
|
84
|
-
throw new Error(`Expected "${expected}" but got "${actual}". ${message}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function assertNull(actual, message = '') {
|
|
89
|
-
if (actual !== null) {
|
|
90
|
-
throw new Error(`Expected null but got "${actual}". ${message}`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function assertTrue(actual, message = '') {
|
|
95
|
-
if (!actual) {
|
|
96
|
-
throw new Error(`Expected truthy but got "${actual}". ${message}`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function assertFalse(actual, message = '') {
|
|
101
|
-
if (actual) {
|
|
102
|
-
throw new Error(`Expected falsy but got "${actual}". ${message}`);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Test cases
|
|
107
|
-
console.log('š§Ŗ Testing Draft Manager\n');
|
|
108
|
-
|
|
109
|
-
test('saves and loads drafts correctly', () => {
|
|
110
|
-
const storage = new MockLocalStorage();
|
|
111
|
-
const draftManager = createDraftManager(storage);
|
|
112
|
-
|
|
113
|
-
const filePath = '/src/app.js';
|
|
114
|
-
const content = 'console.log("hello world");';
|
|
115
|
-
|
|
116
|
-
// Initially no draft
|
|
117
|
-
assertNull(draftManager.loadDraft(filePath));
|
|
118
|
-
|
|
119
|
-
// Save draft
|
|
120
|
-
draftManager.saveDraft(filePath, content);
|
|
121
|
-
|
|
122
|
-
// Load draft
|
|
123
|
-
assertEqual(draftManager.loadDraft(filePath), content);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test('clears drafts correctly', () => {
|
|
127
|
-
const storage = new MockLocalStorage();
|
|
128
|
-
const draftManager = createDraftManager(storage);
|
|
129
|
-
|
|
130
|
-
const filePath = '/src/app.js';
|
|
131
|
-
const content = 'console.log("test");';
|
|
132
|
-
|
|
133
|
-
// Save and verify
|
|
134
|
-
draftManager.saveDraft(filePath, content);
|
|
135
|
-
assertEqual(draftManager.loadDraft(filePath), content);
|
|
136
|
-
|
|
137
|
-
// Clear and verify
|
|
138
|
-
draftManager.clearDraft(filePath);
|
|
139
|
-
assertNull(draftManager.loadDraft(filePath));
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test('detects draft changes correctly', () => {
|
|
143
|
-
const storage = new MockLocalStorage();
|
|
144
|
-
const draftManager = createDraftManager(storage);
|
|
145
|
-
|
|
146
|
-
const filePath = '/src/app.js';
|
|
147
|
-
const originalContent = 'const x = 1;';
|
|
148
|
-
const draftContent = 'const x = 2;';
|
|
149
|
-
|
|
150
|
-
// No draft initially
|
|
151
|
-
assertFalse(draftManager.hasDraftChanges(filePath, originalContent));
|
|
152
|
-
|
|
153
|
-
// Save draft with different content
|
|
154
|
-
draftManager.saveDraft(filePath, draftContent);
|
|
155
|
-
assertTrue(draftManager.hasDraftChanges(filePath, originalContent));
|
|
156
|
-
|
|
157
|
-
// Save draft with same content
|
|
158
|
-
draftManager.saveDraft(filePath, originalContent);
|
|
159
|
-
assertFalse(draftManager.hasDraftChanges(filePath, originalContent));
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test('gets all drafts correctly', () => {
|
|
163
|
-
const storage = new MockLocalStorage();
|
|
164
|
-
const draftManager = createDraftManager(storage);
|
|
165
|
-
|
|
166
|
-
// Add some non-draft items to storage
|
|
167
|
-
storage.setItem('other-key', 'other-value');
|
|
168
|
-
|
|
169
|
-
// Add drafts
|
|
170
|
-
draftManager.saveDraft('/src/app.js', 'app content');
|
|
171
|
-
draftManager.saveDraft('/src/utils.js', 'utils content');
|
|
172
|
-
draftManager.saveDraft('/README.md', 'readme content');
|
|
173
|
-
|
|
174
|
-
const allDrafts = draftManager.getAllDrafts();
|
|
175
|
-
|
|
176
|
-
// Should only return draft items
|
|
177
|
-
assertEqual(Object.keys(allDrafts).length, 3);
|
|
178
|
-
assertEqual(allDrafts['/src/app.js'], 'app content');
|
|
179
|
-
assertEqual(allDrafts['/src/utils.js'], 'utils content');
|
|
180
|
-
assertEqual(allDrafts['/README.md'], 'readme content');
|
|
181
|
-
|
|
182
|
-
// Should not include non-draft items
|
|
183
|
-
assertEqual(allDrafts['other-key'], undefined);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test('handles multiple drafts independently', () => {
|
|
187
|
-
const storage = new MockLocalStorage();
|
|
188
|
-
const draftManager = createDraftManager(storage);
|
|
189
|
-
|
|
190
|
-
const file1 = '/src/app.js';
|
|
191
|
-
const file2 = '/src/utils.js';
|
|
192
|
-
const content1 = 'app content';
|
|
193
|
-
const content2 = 'utils content';
|
|
194
|
-
|
|
195
|
-
// Save drafts for different files
|
|
196
|
-
draftManager.saveDraft(file1, content1);
|
|
197
|
-
draftManager.saveDraft(file2, content2);
|
|
198
|
-
|
|
199
|
-
// Each file has its own draft
|
|
200
|
-
assertEqual(draftManager.loadDraft(file1), content1);
|
|
201
|
-
assertEqual(draftManager.loadDraft(file2), content2);
|
|
202
|
-
|
|
203
|
-
// Clear one draft, other remains
|
|
204
|
-
draftManager.clearDraft(file1);
|
|
205
|
-
assertNull(draftManager.loadDraft(file1));
|
|
206
|
-
assertEqual(draftManager.loadDraft(file2), content2);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test('uses correct storage prefix', () => {
|
|
210
|
-
const storage = new MockLocalStorage();
|
|
211
|
-
const draftManager = createDraftManager(storage);
|
|
212
|
-
|
|
213
|
-
const filePath = '/src/app.js';
|
|
214
|
-
const content = 'test content';
|
|
215
|
-
|
|
216
|
-
draftManager.saveDraft(filePath, content);
|
|
217
|
-
|
|
218
|
-
// Check that correct key is used in storage
|
|
219
|
-
const expectedKey = 'gh-here-draft-/src/app.js';
|
|
220
|
-
assertEqual(storage.getItem(expectedKey), content);
|
|
221
|
-
|
|
222
|
-
// Verify storage length
|
|
223
|
-
assertEqual(storage.length, 1);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
test('handles empty content correctly', () => {
|
|
227
|
-
const storage = new MockLocalStorage();
|
|
228
|
-
const draftManager = createDraftManager(storage);
|
|
229
|
-
|
|
230
|
-
const filePath = '/empty.js';
|
|
231
|
-
const emptyContent = '';
|
|
232
|
-
|
|
233
|
-
draftManager.saveDraft(filePath, emptyContent);
|
|
234
|
-
assertEqual(draftManager.loadDraft(filePath), ''); // localStorage stores empty string as empty string
|
|
235
|
-
|
|
236
|
-
// Empty content should be detected as different from non-empty
|
|
237
|
-
assertTrue(draftManager.hasDraftChanges(filePath, 'non-empty'), 'Empty draft should be different from non-empty original');
|
|
238
|
-
assertFalse(draftManager.hasDraftChanges(filePath, ''));
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
console.log('\nš Draft Manager tests complete!');
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
// Simple unit tests for file type detection
|
|
2
|
-
// Run with: node tests/fileTypeDetection.test.js
|
|
3
|
-
|
|
4
|
-
// Import the functions from our file-utils module
|
|
5
|
-
const { isImageFile, isBinaryFile, isTextFile } = require('../lib/file-utils');
|
|
6
|
-
|
|
7
|
-
function test(description, testFn) {
|
|
8
|
-
try {
|
|
9
|
-
testFn();
|
|
10
|
-
console.log(`ā
${description}`);
|
|
11
|
-
} catch (error) {
|
|
12
|
-
console.log(`ā ${description}`);
|
|
13
|
-
console.log(` Error: ${error.message}`);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function assert(condition, message) {
|
|
18
|
-
if (!condition) {
|
|
19
|
-
throw new Error(message || 'Assertion failed');
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Test isImageFile function
|
|
24
|
-
test('should detect PNG files as images', () => {
|
|
25
|
-
assert(isImageFile('photo.png'), 'PNG should be detected as image');
|
|
26
|
-
assert(isImageFile('PNG'), 'Extension-only PNG should work');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test('should detect common image formats', () => {
|
|
30
|
-
const imageFiles = ['test.jpg', 'test.jpeg', 'test.gif', 'test.svg', 'test.webp', 'test.bmp', 'test.tiff', 'test.ico'];
|
|
31
|
-
imageFiles.forEach(file => {
|
|
32
|
-
assert(isImageFile(file), `${file} should be detected as image`);
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test('should not detect text files as images', () => {
|
|
37
|
-
const textFiles = ['readme.md', 'script.js', 'style.css', 'config.json'];
|
|
38
|
-
textFiles.forEach(file => {
|
|
39
|
-
assert(!isImageFile(file), `${file} should not be detected as image`);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// Test isBinaryFile function
|
|
44
|
-
test('should detect images as binary files', () => {
|
|
45
|
-
assert(isBinaryFile('photo.png'), 'Images should be binary');
|
|
46
|
-
assert(isBinaryFile('animation.gif'), 'GIFs should be binary');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('should detect archives as binary files', () => {
|
|
50
|
-
const archives = ['file.zip', 'backup.tar', 'compressed.gz', 'archive.rar', 'package.7z'];
|
|
51
|
-
archives.forEach(file => {
|
|
52
|
-
assert(isBinaryFile(file), `${file} should be detected as binary`);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('should detect executables as binary files', () => {
|
|
57
|
-
const executables = ['program.exe', 'binary.bin', 'application.app'];
|
|
58
|
-
executables.forEach(file => {
|
|
59
|
-
assert(isBinaryFile(file), `${file} should be detected as binary`);
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('should detect documents as binary files', () => {
|
|
64
|
-
const docs = ['document.pdf', 'spreadsheet.xlsx', 'presentation.pptx'];
|
|
65
|
-
docs.forEach(file => {
|
|
66
|
-
assert(isBinaryFile(file), `${file} should be detected as binary`);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test('should not detect text files as binary', () => {
|
|
71
|
-
const textFiles = ['readme.md', 'script.js', 'style.css', 'data.json', 'config.yml', 'index.html'];
|
|
72
|
-
textFiles.forEach(file => {
|
|
73
|
-
assert(!isBinaryFile(file), `${file} should not be detected as binary`);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// Test isTextFile function
|
|
78
|
-
test('should detect common text files', () => {
|
|
79
|
-
const textFiles = ['readme.md', 'script.js', 'style.css', 'data.json', 'config.yml', 'index.html', 'app.py', 'main.go'];
|
|
80
|
-
textFiles.forEach(file => {
|
|
81
|
-
assert(isTextFile(file), `${file} should be detected as text`);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('should not detect binary files as text', () => {
|
|
86
|
-
const binaryFiles = ['photo.png', 'archive.zip', 'program.exe', 'document.pdf'];
|
|
87
|
-
binaryFiles.forEach(file => {
|
|
88
|
-
assert(!isTextFile(file), `${file} should not be detected as text`);
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
// Test edge cases
|
|
93
|
-
test('should handle files without extensions', () => {
|
|
94
|
-
assert(isTextFile('README'), 'Files without extensions should default to text');
|
|
95
|
-
assert(isTextFile('Makefile'), 'Common text files without extensions should be text');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('should handle extension-only input', () => {
|
|
99
|
-
assert(isImageFile('png'), 'Should handle bare extensions');
|
|
100
|
-
assert(isBinaryFile('exe'), 'Should handle bare extensions for binary');
|
|
101
|
-
assert(isTextFile('js'), 'Should handle bare extensions for text');
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test('should be case insensitive', () => {
|
|
105
|
-
assert(isImageFile('PHOTO.PNG'), 'Should handle uppercase extensions');
|
|
106
|
-
assert(isBinaryFile('ARCHIVE.ZIP'), 'Should handle uppercase extensions');
|
|
107
|
-
assert(isTextFile('SCRIPT.JS'), 'Should handle uppercase extensions');
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// Run all tests
|
|
111
|
-
console.log('Running file type detection tests...\n');
|