heroshot 0.0.2-alpha.1 → 0.0.2-alpha.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.
@@ -1,262 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import {
3
- createOverlay,
4
- createToolbar,
5
- deepElementFromPoint,
6
- getSelector,
7
- updateOverlay,
8
- } from '../dom';
9
-
10
- describe('deepElementFromPoint', () => {
11
- let originalElementFromPoint: typeof document.elementFromPoint;
12
-
13
- beforeEach(() => {
14
- document.body.innerHTML = '';
15
- // Store original and create mock
16
- originalElementFromPoint = document.elementFromPoint;
17
- document.elementFromPoint = vi.fn();
18
- });
19
-
20
- afterEach(() => {
21
- document.elementFromPoint = originalElementFromPoint;
22
- });
23
-
24
- it('should return null when no element at point', () => {
25
- vi.mocked(document.elementFromPoint).mockReturnValue(null);
26
- expect(deepElementFromPoint(0, 0)).toBeNull();
27
- });
28
-
29
- it('should return element when no shadow DOM', () => {
30
- const div = document.createElement('div');
31
- vi.mocked(document.elementFromPoint).mockReturnValue(div);
32
- expect(deepElementFromPoint(100, 100)).toBe(div);
33
- });
34
-
35
- it('should pierce shadow DOM', () => {
36
- const host = document.createElement('div');
37
- const shadow = host.attachShadow({ mode: 'open' });
38
- const inner = document.createElement('span');
39
- shadow.append(inner);
40
-
41
- vi.mocked(document.elementFromPoint).mockReturnValue(host);
42
- shadow.elementFromPoint = vi.fn().mockReturnValue(inner);
43
-
44
- expect(deepElementFromPoint(100, 100)).toBe(inner);
45
- });
46
-
47
- it('should stop if shadow returns same element', () => {
48
- const host = document.createElement('div');
49
- const shadow = host.attachShadow({ mode: 'open' });
50
-
51
- vi.mocked(document.elementFromPoint).mockReturnValue(host);
52
- shadow.elementFromPoint = vi.fn().mockReturnValue(host);
53
-
54
- expect(deepElementFromPoint(100, 100)).toBe(host);
55
- });
56
-
57
- it('should stop if shadow returns null', () => {
58
- const host = document.createElement('div');
59
- const shadow = host.attachShadow({ mode: 'open' });
60
-
61
- vi.mocked(document.elementFromPoint).mockReturnValue(host);
62
- shadow.elementFromPoint = vi.fn().mockReturnValue(null);
63
-
64
- expect(deepElementFromPoint(100, 100)).toBe(host);
65
- });
66
- });
67
-
68
- describe('getSelector', () => {
69
- beforeEach(() => {
70
- document.body.innerHTML = '';
71
- });
72
-
73
- it('should return id selector if element has id', () => {
74
- const div = document.createElement('div');
75
- div.id = 'my-element';
76
- document.body.append(div);
77
-
78
- expect(getSelector(div)).toBe('#my-element');
79
- });
80
-
81
- it('should ignore heroshot-prefixed ids', () => {
82
- const div = document.createElement('div');
83
- div.id = 'heroshot-toolbar';
84
- document.body.append(div);
85
-
86
- const result = getSelector(div);
87
- expect(result).not.toContain('#heroshot-toolbar');
88
- expect(result).toContain('div');
89
- });
90
-
91
- it('should include class names in selector', () => {
92
- const div = document.createElement('div');
93
- div.className = 'btn primary';
94
- document.body.append(div);
95
-
96
- expect(getSelector(div)).toContain('.btn.primary');
97
- });
98
-
99
- it('should limit to first 2 classes', () => {
100
- const div = document.createElement('div');
101
- div.className = 'one two three four';
102
- document.body.append(div);
103
-
104
- const result = getSelector(div);
105
- expect(result).toContain('.one.two');
106
- expect(result).not.toContain('.three');
107
- });
108
-
109
- it('should filter out heroshot classes', () => {
110
- const div = document.createElement('div');
111
- div.className = 'heroshot-active btn';
112
- document.body.append(div);
113
-
114
- const result = getSelector(div);
115
- expect(result).not.toContain('heroshot');
116
- expect(result).toContain('.btn');
117
- });
118
-
119
- it('should add nth-of-type for siblings', () => {
120
- const parent = document.createElement('div');
121
- const child1 = document.createElement('span');
122
- const child2 = document.createElement('span');
123
- parent.append(child1);
124
- parent.append(child2);
125
- document.body.append(parent);
126
-
127
- expect(getSelector(child2)).toContain(':nth-of-type(2)');
128
- });
129
-
130
- it('should stop at ancestor with id', () => {
131
- const parent = document.createElement('div');
132
- parent.id = 'container';
133
- const child = document.createElement('span');
134
- parent.append(child);
135
- document.body.append(parent);
136
-
137
- const result = getSelector(child);
138
- expect(result).toBe('#container > span');
139
- });
140
-
141
- it('should generate shadow DOM piercing selector', () => {
142
- const host = document.createElement('div');
143
- host.id = 'host';
144
- const shadow = host.attachShadow({ mode: 'open' });
145
- const inner = document.createElement('span');
146
- inner.className = 'inner';
147
- shadow.append(inner);
148
- document.body.append(host);
149
-
150
- const result = getSelector(inner);
151
- expect(result).toContain('>>>');
152
- expect(result).toContain('#host');
153
- expect(result).toContain('span.inner');
154
- });
155
-
156
- it('should limit path depth to 20', () => {
157
- let current = document.body;
158
- for (let index = 0; index < 30; index++) {
159
- const div = document.createElement('div');
160
- current.append(div);
161
- current = div;
162
- }
163
-
164
- const result = getSelector(current);
165
- const parts = result.split(' > ');
166
- expect(parts.length).toBeLessThanOrEqual(20);
167
- });
168
- });
169
-
170
- describe('createToolbar', () => {
171
- it('should create toolbar element with correct id', () => {
172
- const toolbar = createToolbar();
173
- expect(toolbar.id).toBe('heroshot-toolbar');
174
- });
175
-
176
- it('should contain picker button', () => {
177
- const toolbar = createToolbar();
178
- const button = toolbar.querySelector('#heroshot-picker-btn');
179
- expect(button).not.toBeNull();
180
- });
181
-
182
- it('should contain status element', () => {
183
- const toolbar = createToolbar();
184
- const status = toolbar.querySelector('#heroshot-status');
185
- expect(status).not.toBeNull();
186
- expect(status?.textContent).toContain('Click crosshair');
187
- });
188
-
189
- it('should contain SVG icon', () => {
190
- const toolbar = createToolbar();
191
- const svg = toolbar.querySelector('svg');
192
- expect(svg).not.toBeNull();
193
- });
194
- });
195
-
196
- describe('createOverlay', () => {
197
- it('should create overlay element with correct id', () => {
198
- const overlay = createOverlay();
199
- expect(overlay.id).toBe('heroshot-overlay');
200
- });
201
-
202
- it('should be hidden by default', () => {
203
- const overlay = createOverlay();
204
- expect(overlay.style.display).toBe('none');
205
- });
206
- });
207
-
208
- describe('updateOverlay', () => {
209
- let overlay: HTMLDivElement;
210
-
211
- beforeEach(() => {
212
- overlay = createOverlay();
213
- document.body.append(overlay);
214
- });
215
-
216
- it('should hide overlay when rect is null', () => {
217
- overlay.style.display = 'block';
218
- updateOverlay(overlay, null);
219
- expect(overlay.style.display).toBe('none');
220
- });
221
-
222
- it('should clear previous content when rect is null', () => {
223
- overlay.innerHTML = '<div>old content</div>';
224
- updateOverlay(overlay, null);
225
- expect(overlay.innerHTML).toBe('');
226
- });
227
-
228
- it('should show overlay when rect is provided', () => {
229
- const rect = new DOMRect(100, 100, 200, 150);
230
- updateOverlay(overlay, rect);
231
- expect(overlay.style.display).toBe('block');
232
- });
233
-
234
- it('should create 4 dark overlay areas', () => {
235
- const rect = new DOMRect(100, 100, 200, 150);
236
- updateOverlay(overlay, rect);
237
-
238
- const darkAreas = overlay.querySelectorAll('.heroshot-overlay-dark');
239
- expect(darkAreas.length).toBe(4);
240
- });
241
-
242
- it('should create highlight element', () => {
243
- const rect = new DOMRect(100, 100, 200, 150);
244
- updateOverlay(overlay, rect);
245
-
246
- const highlight = overlay.querySelector('.heroshot-highlight');
247
- expect(highlight).not.toBeNull();
248
- });
249
-
250
- it('should position highlight at rect location', () => {
251
- const rect = new DOMRect(100, 50, 200, 150);
252
- updateOverlay(overlay, rect);
253
-
254
- const highlight = overlay.querySelector('.heroshot-highlight');
255
- expect(highlight).not.toBeNull();
256
- const highlightElement = highlight as HTMLElement;
257
- expect(highlightElement.style.top).toBe('50px');
258
- expect(highlightElement.style.left).toBe('100px');
259
- expect(highlightElement.style.width).toBe('200px');
260
- expect(highlightElement.style.height).toBe('150px');
261
- });
262
- });
@@ -1,74 +0,0 @@
1
- /**
2
- * Heroshot Toolbar Entry Point
3
- *
4
- * Browser-injected UI for selecting elements and managing screenshots.
5
- * Injected by Playwright during `heroshot setup`.
6
- */
7
-
8
- import { mount, unmount } from 'svelte';
9
- import Toolbar from './components/Toolbar.svelte';
10
-
11
- /**
12
- * Initialize the toolbar
13
- */
14
- export function initToolbar(): (() => void) | null {
15
- // Ensure __heroshot namespace exists
16
- if (!globalThis.__heroshot) {
17
- globalThis.__heroshot = {
18
- initialized: false,
19
- screenshots: [],
20
- pendingJob: null,
21
- emit: () => {
22
- // No-op if not injected by CLI
23
- },
24
- };
25
- }
26
-
27
- // Prevent double initialization
28
- if (globalThis.__heroshot.initialized) {
29
- return null;
30
- }
31
- globalThis.__heroshot.initialized = true;
32
-
33
- // Create mount target with Shadow DOM
34
- // We use Shadow DOM to encapsulate styles - this toolbar is injected into
35
- // arbitrary websites, and without style isolation the host page's CSS could
36
- // override our styles. Shadow DOM provides complete style encapsulation,
37
- // eliminating the need for !important declarations.
38
- const host = document.createElement('div');
39
- host.id = 'heroshot-root';
40
- document.body.append(host);
41
-
42
- const shadow = host.attachShadow({ mode: 'closed' });
43
-
44
- // Mount Svelte component into shadow root
45
- let component: ReturnType<typeof mount>;
46
- try {
47
- component = mount(Toolbar, {
48
- target: shadow,
49
- props: {
50
- initialScreenshots: [...globalThis.__heroshot.screenshots],
51
- pendingJob: globalThis.__heroshot.pendingJob,
52
- },
53
- });
54
- } catch {
55
- return null;
56
- }
57
-
58
- /**
59
- * Cleanup function to remove toolbar
60
- */
61
- function cleanup(): void {
62
- void unmount(component);
63
- host.remove();
64
-
65
- if (globalThis.__heroshot) {
66
- globalThis.__heroshot.initialized = false;
67
- }
68
- }
69
-
70
- return cleanup;
71
- }
72
-
73
- // Auto-initialize when script loads
74
- initToolbar();
@@ -1,6 +0,0 @@
1
- import type { Component } from 'svelte';
2
-
3
- declare module '*.svelte' {
4
- const component: Component;
5
- export default component;
6
- }
@@ -1,43 +0,0 @@
1
- /**
2
- * Screenshot item stored in the toolbar
3
- */
4
- export interface ScreenshotItem {
5
- id: string;
6
- name: string;
7
- url: string;
8
- selector: string;
9
- }
10
-
11
- /**
12
- * Job types that CLI sends to toolbar
13
- */
14
- export type ToolbarJob =
15
- | { type: 'highlight'; selector: string }
16
- | { type: 'navigate-and-highlight'; url: string; selector: string };
17
-
18
- /**
19
- * Event types that toolbar sends to CLI
20
- */
21
- export type ToolbarEvent =
22
- | { type: 'screenshot-added'; data: ScreenshotItem }
23
- | { type: 'screenshot-selected'; id: string; url: string; selector: string }
24
- | { type: 'screenshot-removed'; id: string }
25
- | { type: 'job-complete' }
26
- | { type: 'done' };
27
-
28
- /**
29
- * Global heroshot namespace
30
- */
31
- export interface HeroshotGlobal {
32
- initialized: boolean;
33
- screenshots: ScreenshotItem[];
34
- pendingJob: ToolbarJob | null;
35
- emit: (event: ToolbarEvent) => void;
36
- }
37
-
38
- /**
39
- * Extended globalThis interface
40
- */
41
- declare global {
42
- var __heroshot: HeroshotGlobal | undefined;
43
- }
@@ -1,8 +0,0 @@
1
- import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
2
-
3
- export default {
4
- preprocess: vitePreprocess(),
5
- compilerOptions: {
6
- css: 'injected',
7
- },
8
- };
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../tsconfig.json",
3
- "compilerOptions": {
4
- "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
- "types": ["vitest/globals"]
6
- },
7
- "include": ["src/**/*.ts", "src/**/*.svelte"],
8
- "exclude": ["node_modules", "dist"]
9
- }
@@ -1,52 +0,0 @@
1
- import { resolve } from 'node:path';
2
- import { svelte } from '@sveltejs/vite-plugin-svelte';
3
- import { defineConfig } from 'vitest/config';
4
-
5
- export default defineConfig({
6
- root: __dirname,
7
- plugins: [
8
- svelte({
9
- compilerOptions: {
10
- // Generate JS that works without Svelte runtime
11
- css: 'injected',
12
- },
13
- }),
14
- ],
15
- build: {
16
- lib: {
17
- entry: resolve(__dirname, 'src/main.ts'),
18
- name: 'HeroshotToolbar',
19
- formats: ['iife'],
20
- fileName: () => 'toolbar.js',
21
- },
22
- outDir: 'dist',
23
- emptyOutDir: true,
24
- minify: false,
25
- rollupOptions: {
26
- output: {
27
- // Ensure the IIFE is self-executing
28
- extend: true,
29
- // Inline all dynamic imports
30
- inlineDynamicImports: true,
31
- },
32
- },
33
- },
34
- test: {
35
- globals: true,
36
- environment: 'jsdom',
37
- include: ['src/**/*.test.ts'],
38
- coverage: {
39
- provider: 'v8',
40
- reporter: ['text', 'html', 'lcov'],
41
- reportsDirectory: '../coverage/toolbar',
42
- include: ['src/**/*.ts', 'src/**/*.svelte'],
43
- exclude: ['src/**/*.d.ts'],
44
- thresholds: {
45
- lines: 90,
46
- functions: 90,
47
- branches: 90,
48
- statements: 90,
49
- },
50
- },
51
- },
52
- });
package/tsconfig.json DELETED
@@ -1,34 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "lib": ["ES2022"],
7
- "esModuleInterop": true,
8
- "allowSyntheticDefaultImports": true,
9
- "forceConsistentCasingInFileNames": true,
10
- "skipLibCheck": true,
11
- "noEmit": true,
12
-
13
- // Strict type checking
14
- "strict": true,
15
- "noImplicitAny": true,
16
- "strictNullChecks": true,
17
- "strictFunctionTypes": true,
18
- "strictBindCallApply": true,
19
- "strictPropertyInitialization": true,
20
- "noImplicitThis": true,
21
- "alwaysStrict": true,
22
-
23
- // Additional strict checks
24
- "noUnusedLocals": false,
25
- "noUnusedParameters": false,
26
- "noImplicitReturns": true,
27
- "noFallthroughCasesInSwitch": true,
28
- "noUncheckedIndexedAccess": true,
29
- "noImplicitOverride": true,
30
- "noPropertyAccessFromIndexSignature": true
31
- },
32
- "include": ["src/**/*.ts", "tests/**/*.ts"],
33
- "exclude": ["node_modules", "dist"]
34
- }
package/tsup.config.ts DELETED
@@ -1,12 +0,0 @@
1
- import { defineConfig } from 'tsup';
2
-
3
- export default defineConfig({
4
- entry: ['src/cli.ts'],
5
- format: ['esm'],
6
- dts: true,
7
- clean: true,
8
- shims: true,
9
- banner: {
10
- js: '#!/usr/bin/env node',
11
- },
12
- });
package/vitest.config.ts DELETED
@@ -1,15 +0,0 @@
1
- import { defineConfig } from 'vitest/config';
2
-
3
- export default defineConfig({
4
- test: {
5
- globals: true,
6
- include: ['src/**/*.test.ts', 'tests/**/*.test.ts'],
7
- coverage: {
8
- provider: 'v8',
9
- exclude: ['src/index.ts', 'src/types.ts', 'vitest.config.ts', 'eslint.config.js', 'dist/**'],
10
- thresholds: {
11
- lines: 90,
12
- },
13
- },
14
- },
15
- });