Skip to content

Commit e33ee73

Browse files
JOHNJOHN
authored andcommitted
Fix annotation button positioning to appear near selected text
- Use selection's bounding rect instead of just mouse coordinates - Position button to the right and below selection by default - Fall back to left/above if not enough space - Ensure button stays within viewport bounds - Use fixed positioning for better visibility Fixes issue where button appeared at (0,0) instead of near selection
1 parent 3354763 commit e33ee73

File tree

5 files changed

+217
-9
lines changed

5 files changed

+217
-9
lines changed

annotation-button-missing.png

17.9 KB
Loading

annotation-button-screenshot.png

21.5 KB
Loading
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/**
2+
* E2E test to capture screenshot of annotation button
3+
*/
4+
5+
import { test, expect, chromium } from '@playwright/test';
6+
import * as path from 'path';
7+
import * as os from 'os';
8+
import { fileURLToPath } from 'url';
9+
10+
const __filename = fileURLToPath(import.meta.url);
11+
const __dirname = path.dirname(__filename);
12+
13+
test('annotation button screenshot', async () => {
14+
// Create persistent context with extension loaded
15+
const extensionPath = path.resolve(__dirname, '../../dist');
16+
const userDataDir = path.join(os.tmpdir(), `playwright-annotation-test-${Date.now()}`);
17+
18+
const context = await chromium.launchPersistentContext(userDataDir, {
19+
headless: false, // Need headed mode for screenshot
20+
channel: 'chromium',
21+
args: [
22+
`--disable-extensions-except=${extensionPath}`,
23+
`--load-extension=${extensionPath}`,
24+
],
25+
});
26+
27+
try {
28+
// Wait for extension to load
29+
await new Promise(resolve => setTimeout(resolve, 3000));
30+
31+
// Navigate to a test page
32+
const page = await context.newPage();
33+
34+
// Set up console logging
35+
page.on('console', (msg) => {
36+
const text = msg.text();
37+
if (text.includes('Graphiti') || text.includes('ContentScript') || msg.type() === 'error') {
38+
console.log(`[Browser ${msg.type()}] ${text}`);
39+
}
40+
});
41+
42+
await page.goto('https://example.com', { waitUntil: 'networkidle' });
43+
44+
// Wait for content script to initialize - try multiple times
45+
let contentScriptLoaded = false;
46+
for (let i = 0; i < 10; i++) {
47+
await page.waitForTimeout(500);
48+
contentScriptLoaded = await page.evaluate(() => {
49+
return typeof window !== 'undefined' &&
50+
(window as any).__graphitiContentScriptLoaded === true;
51+
});
52+
if (contentScriptLoaded) break;
53+
}
54+
55+
console.log('Content script loaded:', contentScriptLoaded);
56+
57+
if (!contentScriptLoaded) {
58+
// Check for errors
59+
const errors = await page.evaluate(() => {
60+
return (window as any).__graphitiErrors || [];
61+
});
62+
console.log('Errors:', errors);
63+
}
64+
65+
// Enable annotations if needed
66+
await page.evaluate(async () => {
67+
return new Promise((resolve) => {
68+
if (typeof chrome !== 'undefined' && chrome.storage) {
69+
chrome.storage.local.set({ annotationsEnabled: true }, () => {
70+
resolve(true);
71+
});
72+
} else {
73+
resolve(false);
74+
}
75+
});
76+
});
77+
78+
// Wait a bit more
79+
await page.waitForTimeout(1000);
80+
81+
// Find some text to select
82+
const textToSelect = await page.evaluate(() => {
83+
const p = document.querySelector('p');
84+
return p ? p.textContent : null;
85+
});
86+
87+
console.log('Text to select:', textToSelect?.substring(0, 50));
88+
89+
// Select text by simulating mouse selection AND mouseup event
90+
await page.evaluate(() => {
91+
const p = document.querySelector('p');
92+
if (p && p.firstChild) {
93+
const range = document.createRange();
94+
const textNode = p.firstChild;
95+
range.setStart(textNode, 0);
96+
range.setEnd(textNode, Math.min(20, textNode.textContent?.length || 0));
97+
const selection = window.getSelection();
98+
selection?.removeAllRanges();
99+
selection?.addRange(range);
100+
101+
// Trigger mouseup event to trigger handleTextSelection
102+
const mouseUpEvent = new MouseEvent('mouseup', {
103+
bubbles: true,
104+
cancelable: true,
105+
view: window,
106+
pageX: 100,
107+
pageY: 200
108+
});
109+
document.dispatchEvent(mouseUpEvent);
110+
}
111+
});
112+
113+
// Wait for button to appear - wait longer
114+
await page.waitForTimeout(2000);
115+
116+
// Check console logs
117+
const logs = await page.evaluate(() => {
118+
return (window as any).__graphitiLogs || [];
119+
});
120+
if (logs.length > 0) {
121+
console.log('Content script logs:', logs);
122+
}
123+
124+
// Check if button exists
125+
const buttonExists = await page.evaluate(() => {
126+
return document.querySelector('.pubky-annotation-button') !== null;
127+
});
128+
129+
console.log('Button exists:', buttonExists);
130+
131+
// Take screenshot
132+
await page.screenshot({
133+
path: 'annotation-button-screenshot.png',
134+
fullPage: false
135+
});
136+
137+
// Also check console for any errors
138+
const consoleMessages: string[] = [];
139+
page.on('console', (msg) => {
140+
const text = msg.text();
141+
if (msg.type() === 'error' || msg.type() === 'warning') {
142+
consoleMessages.push(text);
143+
}
144+
});
145+
146+
await page.waitForTimeout(1000);
147+
148+
if (consoleMessages.length > 0) {
149+
console.log('Console errors/warnings:', consoleMessages);
150+
}
151+
152+
// Verify button is visible
153+
const button = page.locator('.pubky-annotation-button');
154+
if (await button.count() > 0) {
155+
console.log('✅ Annotation button found!');
156+
await expect(button).toBeVisible();
157+
} else {
158+
console.log('❌ Annotation button not found');
159+
// Take another screenshot to see what's on the page
160+
await page.screenshot({
161+
path: 'annotation-button-missing.png',
162+
fullPage: true
163+
});
164+
}
165+
166+
} finally {
167+
await context.close();
168+
}
169+
});

src/content/AnnotationManager.ts

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -428,11 +428,24 @@ export class AnnotationManager {
428428

429429
const range = selection.getRangeAt(0);
430430
this.currentSelection = { range, text: validation.sanitized || selectedText };
431+
432+
// Get the position from the selection's bounding rect, not just mouse coordinates
433+
// This ensures the button appears near the selected text even if mouse moved
434+
const rect = range.getBoundingClientRect();
435+
const scrollX = window.pageXOffset || document.documentElement.scrollLeft;
436+
const scrollY = window.pageYOffset || document.documentElement.scrollTop;
437+
438+
// Use selection rect if available, otherwise fall back to mouse position
439+
const x = rect.width > 0 ? rect.right + scrollX : (event.pageX || rect.left + scrollX);
440+
const y = rect.height > 0 ? rect.bottom + scrollY + 5 : (event.pageY || rect.top + scrollY);
441+
431442
logger.info('ContentScript', 'Showing annotation button', {
432-
x: event.pageX,
433-
y: event.pageY
443+
x,
444+
y,
445+
rect: { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom },
446+
mouse: { pageX: event.pageX, pageY: event.pageY }
434447
});
435-
this.showAnnotationButton(event.pageX, event.pageY);
448+
this.showAnnotationButton(x, y);
436449
}
437450

438451
private showAnnotationButton(x: number, y: number) {
@@ -446,8 +459,38 @@ export class AnnotationManager {
446459
</svg>
447460
Add Annotation
448461
`;
449-
button.style.left = `${x - 80}px`;
450-
button.style.top = `${y + 10}px`;
462+
463+
// Position button to the right and slightly below the selection
464+
// Account for button width (approximately 140px) and add some padding
465+
const buttonWidth = 140;
466+
const buttonHeight = 40;
467+
const padding = 10;
468+
469+
// Calculate position relative to viewport
470+
const viewportX = x - (window.pageXOffset || document.documentElement.scrollLeft);
471+
const viewportY = y - (window.pageYOffset || document.documentElement.scrollTop);
472+
473+
// Position button to the right of selection, or to the left if not enough space
474+
let leftPos = viewportX + padding;
475+
if (leftPos + buttonWidth > window.innerWidth) {
476+
// Not enough space on right, position to the left
477+
leftPos = viewportX - buttonWidth - padding;
478+
}
479+
480+
// Position button below selection, or above if not enough space
481+
let topPos = viewportY + padding;
482+
if (topPos + buttonHeight > window.innerHeight) {
483+
// Not enough space below, position above
484+
topPos = viewportY - buttonHeight - padding;
485+
}
486+
487+
// Ensure button stays within viewport bounds
488+
leftPos = Math.max(10, Math.min(leftPos, window.innerWidth - buttonWidth - 10));
489+
topPos = Math.max(10, Math.min(topPos, window.innerHeight - buttonHeight - 10));
490+
491+
button.style.left = `${leftPos}px`;
492+
button.style.top = `${topPos}px`;
493+
button.style.position = 'fixed'; // Use fixed positioning relative to viewport
451494

452495
// Use both mousedown and click to ensure the modal opens
453496
// mousedown fires before mouseup, so it prevents the button from being hidden

test-results/.last-run.json

Lines changed: 0 additions & 4 deletions
This file was deleted.

0 commit comments

Comments
 (0)