@hanzo/browser-extension 1.7.2

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 (134) hide show
  1. package/.github/workflows/docs.yml +44 -0
  2. package/.github/workflows/release.yml +55 -0
  3. package/LICENSE +21 -0
  4. package/README.md +126 -0
  5. package/dist/browser-extension/README.md +43 -0
  6. package/dist/browser-extension/ai-worker.js +30 -0
  7. package/dist/browser-extension/background-firefox.js +617 -0
  8. package/dist/browser-extension/background.js +2737 -0
  9. package/dist/browser-extension/browser-control.js +683 -0
  10. package/dist/browser-extension/cdp-bridge-server.js +444 -0
  11. package/dist/browser-extension/chrome/ai-worker.js +571 -0
  12. package/dist/browser-extension/chrome/background.js +2737 -0
  13. package/dist/browser-extension/chrome/callback.html +17 -0
  14. package/dist/browser-extension/chrome/content-script.js +1270 -0
  15. package/dist/browser-extension/chrome/icon128.png +0 -0
  16. package/dist/browser-extension/chrome/icon16.png +0 -0
  17. package/dist/browser-extension/chrome/icon32.png +0 -0
  18. package/dist/browser-extension/chrome/icon48.png +0 -0
  19. package/dist/browser-extension/chrome/manifest.json +57 -0
  20. package/dist/browser-extension/chrome/popup.css +519 -0
  21. package/dist/browser-extension/chrome/popup.html +242 -0
  22. package/dist/browser-extension/chrome/popup.js +267 -0
  23. package/dist/browser-extension/chrome/sidebar.css +1176 -0
  24. package/dist/browser-extension/chrome/sidebar.html +280 -0
  25. package/dist/browser-extension/chrome/sidebar.js +778 -0
  26. package/dist/browser-extension/cli.js +233 -0
  27. package/dist/browser-extension/content-script.js +1270 -0
  28. package/dist/browser-extension/firefox/ai-worker.js +571 -0
  29. package/dist/browser-extension/firefox/background.js +617 -0
  30. package/dist/browser-extension/firefox/callback.html +17 -0
  31. package/dist/browser-extension/firefox/content-script.js +1270 -0
  32. package/dist/browser-extension/firefox/icon128.png +0 -0
  33. package/dist/browser-extension/firefox/icon16.png +0 -0
  34. package/dist/browser-extension/firefox/icon32.png +0 -0
  35. package/dist/browser-extension/firefox/icon48.png +0 -0
  36. package/dist/browser-extension/firefox/manifest.json +66 -0
  37. package/dist/browser-extension/firefox/popup.css +519 -0
  38. package/dist/browser-extension/firefox/popup.html +242 -0
  39. package/dist/browser-extension/firefox/popup.js +267 -0
  40. package/dist/browser-extension/firefox/sidebar.css +1176 -0
  41. package/dist/browser-extension/firefox/sidebar.html +280 -0
  42. package/dist/browser-extension/firefox/sidebar.js +778 -0
  43. package/dist/browser-extension/hanzo-browser-extension-firefox-1.1.0.zip +0 -0
  44. package/dist/browser-extension/icon128.png +0 -0
  45. package/dist/browser-extension/icon16.png +0 -0
  46. package/dist/browser-extension/icon32.png +0 -0
  47. package/dist/browser-extension/icon48.png +0 -0
  48. package/dist/browser-extension/manifest.json +57 -0
  49. package/dist/browser-extension/package.json +41 -0
  50. package/dist/browser-extension/popup.css +468 -0
  51. package/dist/browser-extension/popup.html +191 -0
  52. package/dist/browser-extension/popup.js +267 -0
  53. package/dist/browser-extension/safari/Info.plist +21 -0
  54. package/dist/browser-extension/safari/ai-worker.js +571 -0
  55. package/dist/browser-extension/safari/background.js +2737 -0
  56. package/dist/browser-extension/safari/callback.html +17 -0
  57. package/dist/browser-extension/safari/content-script.js +1270 -0
  58. package/dist/browser-extension/safari/icon128.png +0 -0
  59. package/dist/browser-extension/safari/icon16.png +0 -0
  60. package/dist/browser-extension/safari/icon32.png +0 -0
  61. package/dist/browser-extension/safari/icon48.png +0 -0
  62. package/dist/browser-extension/safari/popup.css +519 -0
  63. package/dist/browser-extension/safari/popup.html +242 -0
  64. package/dist/browser-extension/safari/popup.js +267 -0
  65. package/dist/browser-extension/safari/sidebar.css +1176 -0
  66. package/dist/browser-extension/safari/sidebar.html +280 -0
  67. package/dist/browser-extension/safari/sidebar.js +778 -0
  68. package/dist/browser-extension/sidebar.css +718 -0
  69. package/dist/browser-extension/sidebar.html +184 -0
  70. package/dist/browser-extension/sidebar.js +778 -0
  71. package/dist/browser-extension/webgpu-ai.js +495 -0
  72. package/dist/cdp-bridge-server.js +411 -0
  73. package/dist/safari/Hanzo AI/Hanzo AI.xcodeproj/project.pbxproj +1047 -0
  74. package/dist/safari/Hanzo AI/Hanzo AI.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  75. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AccentColor.colorset/Contents.json +11 -0
  76. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/Contents.json +98 -0
  77. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-128@1x.png +0 -0
  78. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-128@2x.png +0 -0
  79. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-16@1x.png +0 -0
  80. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-16@2x.png +0 -0
  81. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-256@1x.png +0 -0
  82. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-256@2x.png +0 -0
  83. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-32@1x.png +0 -0
  84. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-32@2x.png +0 -0
  85. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-512@1x.png +0 -0
  86. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/mac-icon-512@2x.png +0 -0
  87. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/AppIcon.appiconset/universal-icon-1024@1x.png +0 -0
  88. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/Contents.json +6 -0
  89. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/LargeIcon.imageset/Contents.json +21 -0
  90. package/dist/safari/Hanzo AI/Shared (App)/Assets.xcassets/LargeIcon.imageset/icon128.png +0 -0
  91. package/dist/safari/Hanzo AI/Shared (App)/Resources/Base.lproj/Main.html +20 -0
  92. package/dist/safari/Hanzo AI/Shared (App)/Resources/Icon.png +0 -0
  93. package/dist/safari/Hanzo AI/Shared (App)/Resources/Script.js +24 -0
  94. package/dist/safari/Hanzo AI/Shared (App)/Resources/Style.css +61 -0
  95. package/dist/safari/Hanzo AI/Shared (App)/ViewController.swift +81 -0
  96. package/dist/safari/Hanzo AI/Shared (Extension)/SafariWebExtensionHandler.swift +42 -0
  97. package/dist/safari/Hanzo AI/iOS (App)/AppDelegate.swift +24 -0
  98. package/dist/safari/Hanzo AI/iOS (App)/Base.lproj/LaunchScreen.storyboard +36 -0
  99. package/dist/safari/Hanzo AI/iOS (App)/Base.lproj/Main.storyboard +38 -0
  100. package/dist/safari/Hanzo AI/iOS (App)/Info.plist +27 -0
  101. package/dist/safari/Hanzo AI/iOS (App)/SceneDelegate.swift +18 -0
  102. package/dist/safari/Hanzo AI/iOS (Extension)/Info.plist +13 -0
  103. package/dist/safari/Hanzo AI/macOS (App)/AppDelegate.swift +21 -0
  104. package/dist/safari/Hanzo AI/macOS (App)/Base.lproj/Main.storyboard +124 -0
  105. package/dist/safari/Hanzo AI/macOS (App)/Info.plist +8 -0
  106. package/dist/safari/Hanzo AI/macOS (Extension)/Info.plist +13 -0
  107. package/docs/_config.yml +13 -0
  108. package/docs/_layouts/default.html +435 -0
  109. package/docs/index.md +54 -0
  110. package/install-extension.sh +75 -0
  111. package/manifest.json +29 -0
  112. package/package.json +32 -0
  113. package/src/ai-worker.js +571 -0
  114. package/src/callback.html +17 -0
  115. package/src/chat-widget.tsx +93 -0
  116. package/src/icon128.png +0 -0
  117. package/src/icon16.png +0 -0
  118. package/src/icon32.png +0 -0
  119. package/src/icon48.png +0 -0
  120. package/src/manifest-firefox.json +66 -0
  121. package/src/manifest.json +57 -0
  122. package/src/package.json +41 -0
  123. package/src/popup.css +519 -0
  124. package/src/popup.html +242 -0
  125. package/src/popup.js +192 -0
  126. package/src/sidebar.css +1176 -0
  127. package/src/sidebar.html +280 -0
  128. package/src/sidebar.js +884 -0
  129. package/src/webgpu.d.ts +131 -0
  130. package/test/claude-integration.test.ts +251 -0
  131. package/test/server-frameworks-edge-cases.test.ts +145 -0
  132. package/test/server-frameworks.test.ts +395 -0
  133. package/test-cdp-connection.js +140 -0
  134. package/tsconfig.json +19 -0
@@ -0,0 +1,131 @@
1
+ // Ambient WebGPU type declarations for browser extension
2
+ // Full types available via @webgpu/types package if needed
3
+
4
+ interface GPU {
5
+ requestAdapter(options?: GPURequestAdapterOptions): Promise<GPUAdapter | null>;
6
+ }
7
+
8
+ interface GPURequestAdapterOptions {
9
+ powerPreference?: 'low-power' | 'high-performance';
10
+ }
11
+
12
+ interface GPUAdapter {
13
+ features: ReadonlySet<string>;
14
+ requestDevice(descriptor?: GPUDeviceDescriptor): Promise<GPUDevice>;
15
+ }
16
+
17
+ interface GPUDeviceDescriptor {
18
+ requiredFeatures?: string[];
19
+ requiredLimits?: Record<string, number>;
20
+ }
21
+
22
+ interface GPUDevice {
23
+ queue: GPUQueue;
24
+ createBuffer(descriptor: GPUBufferDescriptor): GPUBuffer;
25
+ createShaderModule(descriptor: GPUShaderModuleDescriptor): GPUShaderModule;
26
+ createComputePipeline(descriptor: GPUComputePipelineDescriptor): GPUComputePipeline;
27
+ createBindGroup(descriptor: GPUBindGroupDescriptor): GPUBindGroup;
28
+ createBindGroupLayout(descriptor: GPUBindGroupLayoutDescriptor): GPUBindGroupLayout;
29
+ createCommandEncoder(): GPUCommandEncoder;
30
+ }
31
+
32
+ interface GPUQueue {
33
+ submit(commandBuffers: GPUCommandBuffer[]): void;
34
+ writeBuffer(buffer: GPUBuffer, offset: number, data: BufferSource): void;
35
+ }
36
+
37
+ interface GPUBuffer {
38
+ size: number;
39
+ mapAsync(mode: number): Promise<void>;
40
+ getMappedRange(): ArrayBuffer;
41
+ unmap(): void;
42
+ }
43
+
44
+ interface GPUShaderModule {}
45
+
46
+ interface GPUComputePipeline {}
47
+
48
+ interface GPUBindGroup {}
49
+
50
+ interface GPUBindGroupLayout {}
51
+
52
+ interface GPUCommandEncoder {
53
+ beginComputePass(): GPUComputePassEncoder;
54
+ copyBufferToBuffer(src: GPUBuffer, srcOff: number, dst: GPUBuffer, dstOff: number, size: number): void;
55
+ finish(): GPUCommandBuffer;
56
+ }
57
+
58
+ interface GPUComputePassEncoder {
59
+ setPipeline(pipeline: GPUComputePipeline): void;
60
+ setBindGroup(index: number, bindGroup: GPUBindGroup): void;
61
+ dispatchWorkgroups(x: number, y?: number, z?: number): void;
62
+ end(): void;
63
+ }
64
+
65
+ interface GPUCommandBuffer {}
66
+
67
+ interface GPUBufferDescriptor {
68
+ size: number;
69
+ usage: number;
70
+ }
71
+
72
+ interface GPUShaderModuleDescriptor {
73
+ code: string;
74
+ }
75
+
76
+ interface GPUComputePipelineDescriptor {
77
+ layout: 'auto' | GPUPipelineLayout;
78
+ compute: { module: GPUShaderModule; entryPoint: string };
79
+ }
80
+
81
+ interface GPUPipelineLayout {}
82
+
83
+ interface GPUBindGroupDescriptor {
84
+ layout: GPUBindGroupLayout;
85
+ entries: GPUBindGroupEntry[];
86
+ }
87
+
88
+ interface GPUBindGroupEntry {
89
+ binding: number;
90
+ resource: { buffer: GPUBuffer } | GPUExternalTexture;
91
+ }
92
+
93
+ interface GPUExternalTexture {}
94
+
95
+ interface GPUBindGroupLayoutDescriptor {
96
+ entries: GPUBindGroupLayoutEntry[];
97
+ }
98
+
99
+ interface GPUBindGroupLayoutEntry {
100
+ binding: number;
101
+ visibility: number;
102
+ buffer?: { type: string };
103
+ }
104
+
105
+ // WebGPU constants
106
+ declare const GPUBufferUsage: {
107
+ MAP_READ: number;
108
+ MAP_WRITE: number;
109
+ COPY_SRC: number;
110
+ COPY_DST: number;
111
+ INDEX: number;
112
+ VERTEX: number;
113
+ UNIFORM: number;
114
+ STORAGE: number;
115
+ };
116
+
117
+ declare const GPUShaderStage: {
118
+ VERTEX: number;
119
+ FRAGMENT: number;
120
+ COMPUTE: number;
121
+ };
122
+
123
+ declare const GPUMapMode: {
124
+ READ: number;
125
+ WRITE: number;
126
+ };
127
+
128
+ // Extend Navigator
129
+ interface Navigator {
130
+ gpu: GPU;
131
+ }
@@ -0,0 +1,251 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { BrowserExtensionServer } from '../src/browser-extension-server';
3
+ import WebSocket from 'ws';
4
+ import * as path from 'path';
5
+ import * as fs from 'fs';
6
+
7
+ describe('Claude Code Browser Extension Integration', () => {
8
+ let server: BrowserExtensionServer;
9
+ let ws: WebSocket;
10
+ const TEST_PORT = 3002;
11
+
12
+ beforeEach(async () => {
13
+ // Start server
14
+ server = new BrowserExtensionServer(TEST_PORT, process.cwd());
15
+
16
+ // Wait for server to be ready
17
+ await new Promise(resolve => setTimeout(resolve, 100));
18
+
19
+ // Connect WebSocket
20
+ ws = new WebSocket(`ws://localhost:${TEST_PORT}/browser-extension`);
21
+ await new Promise(resolve => ws.on('open', resolve));
22
+ });
23
+
24
+ afterEach(() => {
25
+ ws.close();
26
+ server.close();
27
+ });
28
+
29
+ describe('React source-map integration', () => {
30
+ it('should handle React component with __source', async () => {
31
+ const elementSelected = new Promise((resolve) => {
32
+ server.on('elementSelected', resolve);
33
+ });
34
+
35
+ // Simulate React element click with source-map data
36
+ ws.send(JSON.stringify({
37
+ event: 'elementSelected',
38
+ framework: 'react',
39
+ domPath: 'div#root > div.App > button.submit-btn',
40
+ source: {
41
+ file: 'src/components/SubmitButton.tsx',
42
+ line: 42,
43
+ column: 12
44
+ }
45
+ }));
46
+
47
+ const result = await elementSelected;
48
+
49
+ expect(result).toMatchObject({
50
+ file: expect.stringContaining('SubmitButton.tsx'),
51
+ line: 42,
52
+ column: 12,
53
+ framework: 'react'
54
+ });
55
+ });
56
+
57
+ it('should resolve webpack:// protocol paths', async () => {
58
+ const elementSelected = new Promise((resolve) => {
59
+ server.on('elementSelected', resolve);
60
+ });
61
+
62
+ ws.send(JSON.stringify({
63
+ event: 'elementSelected',
64
+ framework: 'react',
65
+ domPath: 'header > nav > a',
66
+ source: {
67
+ file: 'webpack://my-app/./src/components/Navigation.jsx',
68
+ line: 15
69
+ }
70
+ }));
71
+
72
+ const result = await elementSelected;
73
+
74
+ expect(result).toMatchObject({
75
+ file: expect.stringContaining('Navigation.jsx'),
76
+ line: 15,
77
+ framework: 'react'
78
+ });
79
+ });
80
+ });
81
+
82
+ describe('Vue source-map integration', () => {
83
+ it('should handle Vue component with __file', async () => {
84
+ const elementSelected = new Promise((resolve) => {
85
+ server.on('elementSelected', resolve);
86
+ });
87
+
88
+ ws.send(JSON.stringify({
89
+ event: 'elementSelected',
90
+ framework: 'vue',
91
+ domPath: 'div#app > main > custom-button',
92
+ source: {
93
+ file: 'src/components/CustomButton.vue',
94
+ line: 28
95
+ }
96
+ }));
97
+
98
+ const result = await elementSelected;
99
+
100
+ expect(result).toMatchObject({
101
+ file: expect.stringContaining('CustomButton.vue'),
102
+ line: 28,
103
+ framework: 'vue'
104
+ });
105
+ });
106
+ });
107
+
108
+ describe('Fallback to data-hanzo-id', () => {
109
+ it('should handle elements without source-map data', async () => {
110
+ // Create the .hanzo directory and id-map.json for test
111
+ const hanzoDir = path.join(process.cwd(), '.hanzo');
112
+ const mapFile = path.join(hanzoDir, 'id-map.json');
113
+
114
+ if (!fs.existsSync(hanzoDir)) {
115
+ fs.mkdirSync(hanzoDir, { recursive: true });
116
+ }
117
+
118
+ // Write test id map
119
+ fs.writeFileSync(mapFile, JSON.stringify({
120
+ 'hanzo-abc123': {
121
+ file: path.join(process.cwd(), 'src/legacy/OldComponent.js'),
122
+ line: 55
123
+ }
124
+ }));
125
+
126
+ const elementSelected = new Promise((resolve) => {
127
+ server.once('elementSelected', resolve);
128
+ });
129
+
130
+ ws.send(JSON.stringify({
131
+ event: 'elementSelected',
132
+ framework: null,
133
+ domPath: 'body > div.legacy > span',
134
+ fallbackId: 'hanzo-abc123'
135
+ }));
136
+
137
+ const result = await elementSelected;
138
+
139
+ expect(result).toMatchObject({
140
+ file: expect.stringContaining('OldComponent.js'),
141
+ line: 55
142
+ });
143
+
144
+ // Cleanup
145
+ if (fs.existsSync(mapFile)) {
146
+ fs.unlinkSync(mapFile);
147
+ }
148
+ });
149
+ });
150
+
151
+ describe('Claude Code MCP integration', () => {
152
+ it('should format events for Claude Code consumption', async () => {
153
+ const events: any[] = [];
154
+
155
+ server.on('elementSelected', (data) => {
156
+ events.push({
157
+ tool: 'browser_navigate_to_source',
158
+ arguments: {
159
+ file: data.file,
160
+ line: data.line,
161
+ column: data.column,
162
+ reason: `User clicked ${data.framework ? data.framework.charAt(0).toUpperCase() + data.framework.slice(1) : 'HTML'} element at ${data.domPath}`
163
+ }
164
+ });
165
+ });
166
+
167
+ // Send multiple events
168
+ const testCases = [
169
+ {
170
+ framework: 'react',
171
+ source: { file: 'App.tsx', line: 10, column: 4 },
172
+ domPath: 'div#root > div.App'
173
+ },
174
+ {
175
+ framework: 'vue',
176
+ source: { file: 'Home.vue', line: 45 },
177
+ domPath: 'div#app > router-view > div.home'
178
+ }
179
+ ];
180
+
181
+ for (const testCase of testCases) {
182
+ ws.send(JSON.stringify({
183
+ event: 'elementSelected',
184
+ ...testCase
185
+ }));
186
+ await new Promise(resolve => setTimeout(resolve, 50));
187
+ }
188
+
189
+ expect(events).toHaveLength(2);
190
+ expect(events[0].tool).toBe('browser_navigate_to_source');
191
+ expect(events[0].arguments.reason).toContain('React element');
192
+ expect(events[1].arguments.reason).toContain('Vue element');
193
+ });
194
+ });
195
+
196
+ describe('Error handling', () => {
197
+ it('should handle malformed messages gracefully', async () => {
198
+ // Server should not crash on bad input
199
+ ws.send('invalid json');
200
+ ws.send(JSON.stringify({ invalid: 'event' }));
201
+ ws.send(JSON.stringify({ event: 'unknown' }));
202
+
203
+ // Give time for messages to process
204
+ await new Promise(resolve => setTimeout(resolve, 100));
205
+
206
+ // Server should still be responsive
207
+ const elementSelected = new Promise((resolve) => {
208
+ server.on('elementSelected', resolve);
209
+ });
210
+
211
+ ws.send(JSON.stringify({
212
+ event: 'elementSelected',
213
+ framework: 'react',
214
+ source: { file: 'test.tsx', line: 1 }
215
+ }));
216
+
217
+ const result = await elementSelected;
218
+ expect(result).toBeDefined();
219
+ });
220
+ });
221
+
222
+ describe('Performance', () => {
223
+ it('should handle rapid clicks efficiently', async () => {
224
+ const events: any[] = [];
225
+ server.on('elementSelected', (data) => events.push(data));
226
+
227
+ // Send 100 events rapidly
228
+ const startTime = Date.now();
229
+
230
+ for (let i = 0; i < 100; i++) {
231
+ ws.send(JSON.stringify({
232
+ event: 'elementSelected',
233
+ framework: 'react',
234
+ domPath: `div#item-${i}`,
235
+ source: {
236
+ file: `Component${i}.tsx`,
237
+ line: i + 1
238
+ }
239
+ }));
240
+ }
241
+
242
+ // Wait for all events to process
243
+ await new Promise(resolve => setTimeout(resolve, 500));
244
+
245
+ const duration = Date.now() - startTime;
246
+
247
+ expect(events).toHaveLength(100);
248
+ expect(duration).toBeLessThan(1000); // Should process 100 events in under 1 second
249
+ });
250
+ });
251
+ });
@@ -0,0 +1,145 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { ServerFrameworkDetector } from '../src/server-frameworks';
3
+ import { JSDOM } from 'jsdom';
4
+
5
+ describe('Server Framework Detection - Edge Cases', () => {
6
+ let detector: ServerFrameworkDetector;
7
+ let dom: JSDOM;
8
+ let document: Document;
9
+
10
+ beforeEach(() => {
11
+ detector = new ServerFrameworkDetector();
12
+ dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
13
+ document = dom.window.document;
14
+ // @ts-ignore
15
+ global.document = document;
16
+ });
17
+
18
+ describe('Django Debug Toolbar extraction', () => {
19
+ it('should handle Django with toolbar but no view info', () => {
20
+ // Create Django debug toolbar
21
+ const toolbar = document.createElement('div');
22
+ toolbar.id = 'djDebug';
23
+ document.body.appendChild(toolbar);
24
+
25
+ const element = document.createElement('div');
26
+ const result = detector.detectDjango(element);
27
+
28
+ expect(result).toMatchObject({
29
+ framework: 'django'
30
+ });
31
+ });
32
+ });
33
+
34
+ describe('Code coverage for edge cases', () => {
35
+ it('should cover Django toolbar with specific panel', () => {
36
+ // Cover the Django Debug Toolbar code path
37
+ const toolbar = document.createElement('div');
38
+ toolbar.id = 'djDebugToolbar';
39
+ const panel = document.createElement('div');
40
+ panel.className = 'djDebugPanelContent';
41
+ const viewInfo = document.createElement('div');
42
+ viewInfo.className = 'view-info';
43
+ viewInfo.textContent = 'View: myapp.views.index';
44
+ panel.appendChild(viewInfo);
45
+ toolbar.appendChild(panel);
46
+ document.body.appendChild(toolbar);
47
+
48
+ const element = document.createElement('div');
49
+ const info = detector['extractDjangoViewInfo']();
50
+
51
+ // Just verify it doesn't crash and returns an object
52
+ expect(info).toBeDefined();
53
+ expect(typeof info).toBe('object');
54
+ });
55
+ });
56
+
57
+ describe('Error handling', () => {
58
+ it('should handle missing Werkzeug traceback gracefully', () => {
59
+ const debuggerEl = document.createElement('div');
60
+ debuggerEl.id = 'werkzeug-debugger';
61
+ document.body.appendChild(debuggerEl);
62
+
63
+ const element = document.createElement('div');
64
+ const result = detector.detectFlask(element);
65
+
66
+ expect(result).toMatchObject({
67
+ framework: 'flask'
68
+ });
69
+ expect(result?.file).toBeUndefined();
70
+ expect(result?.line).toBeUndefined();
71
+ });
72
+ });
73
+
74
+ describe('Rails edge cases', () => {
75
+ it('should handle Rails template without controller extraction', () => {
76
+ document.head.innerHTML = '<meta name="csrf-token" content="abc123">';
77
+ const element = document.createElement('div');
78
+ element.innerHTML = '<!-- BEGIN app/views/show.html.erb --><p>Content</p>';
79
+
80
+ const result = detector.detectRails(element);
81
+
82
+ expect(result).toMatchObject({
83
+ framework: 'rails',
84
+ template: 'app/views/show.html.erb',
85
+ controller: '',
86
+ action: 'show'
87
+ });
88
+ });
89
+
90
+ it('should handle Rails without any comments', () => {
91
+ document.head.innerHTML = '<meta name="csrf-token" content="abc123">';
92
+ const element = document.createElement('div');
93
+
94
+ const result = detector.detectRails(element);
95
+
96
+ expect(result).toMatchObject({
97
+ framework: 'rails'
98
+ });
99
+ });
100
+ });
101
+
102
+ describe('Real browser environment', () => {
103
+ it('should handle createTreeWalker in real browser', () => {
104
+ // Mock NodeFilter
105
+ global.NodeFilter = { SHOW_COMMENT: 8 };
106
+
107
+ // Create a more realistic DOM structure
108
+ const element = document.createElement('div');
109
+ const parent = document.createElement('div');
110
+ parent.appendChild(element);
111
+ document.body.appendChild(parent);
112
+
113
+ // Simulate TreeWalker behavior
114
+ const originalCreateTreeWalker = document.createTreeWalker;
115
+ let walkerCalled = false;
116
+ document.createTreeWalker = function() {
117
+ walkerCalled = true;
118
+ return {
119
+ nextNode: () => null
120
+ };
121
+ } as any;
122
+
123
+ const comments = detector['extractHTMLComments'](element);
124
+
125
+ expect(walkerCalled).toBe(true);
126
+ expect(comments).toEqual([]);
127
+
128
+ // Restore
129
+ document.createTreeWalker = originalCreateTreeWalker;
130
+ });
131
+ });
132
+
133
+ describe('All framework detection - null cases', () => {
134
+ it('should handle all detectors returning null', () => {
135
+ const element = document.createElement('div');
136
+
137
+ // Ensure no framework markers are present
138
+ document.head.innerHTML = '';
139
+ document.body.innerHTML = '<div></div>';
140
+
141
+ const result = detector.detect(element);
142
+ expect(result).toBeNull();
143
+ });
144
+ });
145
+ });