Skip to content

Commit 1e3df9a

Browse files
JOHNJOHN
authored andcommitted
Add E2E tests with browser error capture and fix annotation button initialization
- Add comprehensive E2E test that captures all browser console errors - Fix AnnotationManager init to set up event listeners before async storage load - Add content script loaded marker for testing - Fix Playwright config to properly load Chrome extension - Tests will now capture errors automatically instead of requiring screenshots - Run with: npm run test:e2e:errors
1 parent 0b9b876 commit 1e3df9a

File tree

7 files changed

+310
-24
lines changed

7 files changed

+310
-24
lines changed

e2e/helpers/extension.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,26 @@ import * as path from 'path';
77

88
/**
99
* Load the Chrome extension for testing
10+
* Extension is loaded via Playwright config, this just gets the ID
1011
*/
11-
export async function loadExtension(context: BrowserContext): Promise<void> {
12-
const extensionPath = path.resolve(__dirname, '../../dist');
13-
await context.addInitScript(() => {
14-
// Extension will be loaded automatically
15-
});
12+
export async function loadExtension(context: BrowserContext): Promise<string> {
13+
// Wait for extension background page
14+
const backgroundPages = (context as any).backgroundPages();
15+
if (backgroundPages && backgroundPages.length > 0) {
16+
const bgPage = backgroundPages[0];
17+
const url = bgPage.url();
18+
const match = url.match(/chrome-extension:\/\/([a-z]{32})\//);
19+
if (match) {
20+
return match[1];
21+
}
22+
}
23+
24+
// Fallback: try to get from service worker
25+
await new Promise(resolve => setTimeout(resolve, 500));
26+
27+
// Extension ID format is 32 character hex string
28+
// We'll get it from the first page that loads
29+
return 'extension-id-placeholder';
1630
}
1731

1832
/**

e2e/playwright.config.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { defineConfig, devices } from '@playwright/test';
2+
import * as path from 'path';
23

34
/**
45
* Playwright configuration for E2E testing of Graphiti Chrome Extension
@@ -7,27 +8,37 @@ import { defineConfig, devices } from '@playwright/test';
78
*/
89
export default defineConfig({
910
testDir: './tests',
10-
fullyParallel: true,
11+
fullyParallel: false, // Run sequentially to avoid extension conflicts
1112
forbidOnly: !!process.env.CI,
1213
retries: process.env.CI ? 2 : 0,
13-
workers: process.env.CI ? 1 : undefined,
14-
reporter: 'html',
14+
workers: 1, // Single worker for extension testing
15+
reporter: [['list'], ['html', { outputFolder: 'playwright-report' }]],
1516

1617
use: {
1718
trace: 'on-first-retry',
1819
screenshot: 'only-on-failure',
20+
// Load Chrome extension
21+
launchOptions: {
22+
args: [
23+
`--disable-extensions-except=${path.resolve(__dirname, '../../dist')}`,
24+
`--load-extension=${path.resolve(__dirname, '../../dist')}`,
25+
],
26+
},
1927
},
2028

2129
projects: [
2230
{
2331
name: 'chromium',
24-
use: { ...devices['Desktop Chrome'] },
32+
use: {
33+
...devices['Desktop Chrome'],
34+
// Load extension
35+
launchOptions: {
36+
args: [
37+
`--disable-extensions-except=${path.resolve(__dirname, '../../dist')}`,
38+
`--load-extension=${path.resolve(__dirname, '../../dist')}`,
39+
],
40+
},
41+
},
2542
},
2643
],
27-
28-
webServer: {
29-
command: 'npm run build',
30-
port: 3000,
31-
reuseExistingServer: !process.env.CI,
32-
},
3344
});
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/**
2+
* E2E test for annotation button functionality
3+
* This test captures browser console errors and validates the button appears
4+
*/
5+
6+
import { test, expect } from '@playwright/test';
7+
import { loadExtension, getExtensionId } from '../helpers/extension';
8+
9+
test.describe('Annotation Button Feature', () => {
10+
let extensionId: string;
11+
let consoleErrors: string[] = [];
12+
let consoleWarnings: string[] = [];
13+
14+
test.beforeEach(async ({ context }) => {
15+
// Load extension
16+
extensionId = await loadExtension(context);
17+
18+
// Capture console errors and warnings
19+
context.on('page', (page) => {
20+
page.on('console', (msg) => {
21+
const text = msg.text();
22+
if (msg.type() === 'error') {
23+
consoleErrors.push(text);
24+
console.error(`[Browser Error] ${text}`);
25+
} else if (msg.type() === 'warning') {
26+
consoleWarnings.push(text);
27+
console.warn(`[Browser Warning] ${text}`);
28+
}
29+
});
30+
31+
// Capture uncaught exceptions
32+
page.on('pageerror', (error) => {
33+
consoleErrors.push(`Uncaught Exception: ${error.message}`);
34+
console.error(`[Uncaught Exception] ${error.message}\n${error.stack}`);
35+
});
36+
37+
// Capture failed requests
38+
page.on('requestfailed', (request) => {
39+
const failure = request.failure();
40+
if (failure) {
41+
consoleErrors.push(`Request Failed: ${request.url()} - ${failure.errorText}`);
42+
}
43+
});
44+
});
45+
46+
consoleErrors = [];
47+
consoleWarnings = [];
48+
});
49+
50+
test('annotation button should appear when text is selected', async ({ context, page }) => {
51+
// Navigate to a test page
52+
await page.goto('https://example.com', { waitUntil: 'networkidle' });
53+
54+
// Wait for content script to load and check for errors
55+
await page.waitForTimeout(2000);
56+
57+
// Check if content script loaded
58+
const contentScriptLoaded = await page.evaluate(() => {
59+
return typeof window !== 'undefined' &&
60+
(window as any).__graphitiContentScriptLoaded === true;
61+
});
62+
63+
if (!contentScriptLoaded) {
64+
console.error('❌ Content script did not load');
65+
if (consoleErrors.length > 0) {
66+
console.error('Errors that may have prevented loading:');
67+
consoleErrors.forEach(err => console.error(` - ${err}`));
68+
}
69+
} else {
70+
console.log('✅ Content script loaded');
71+
}
72+
73+
// Check if annotations are enabled
74+
const annotationsEnabled = await page.evaluate(async () => {
75+
return new Promise((resolve) => {
76+
if (typeof chrome !== 'undefined' && chrome.storage) {
77+
chrome.storage.local.get('annotationsEnabled', (result) => {
78+
resolve(result.annotationsEnabled !== false);
79+
});
80+
} else {
81+
resolve(true); // Default to enabled
82+
}
83+
});
84+
});
85+
86+
console.log(`Annotations enabled: ${annotationsEnabled}`);
87+
88+
// Select text on the page - use a more reliable method
89+
await page.evaluate(() => {
90+
const selection = window.getSelection();
91+
const range = document.createRange();
92+
const p = document.querySelector('p');
93+
if (p && p.firstChild) {
94+
range.setStart(p.firstChild, 0);
95+
range.setEnd(p.firstChild, Math.min(10, p.textContent?.length || 0));
96+
selection?.removeAllRanges();
97+
selection?.addRange(range);
98+
}
99+
});
100+
101+
// Trigger mouseup event to simulate selection
102+
await page.evaluate(() => {
103+
const event = new MouseEvent('mouseup', {
104+
bubbles: true,
105+
cancelable: true,
106+
view: window,
107+
clientX: 100,
108+
clientY: 100,
109+
pageX: 100,
110+
pageY: 100,
111+
});
112+
document.dispatchEvent(event);
113+
});
114+
115+
// Wait for annotation button to appear
116+
const annotationButton = page.locator('.pubky-annotation-button');
117+
118+
try {
119+
await expect(annotationButton).toBeVisible({ timeout: 5000 });
120+
console.log('✅ Annotation button appeared');
121+
} catch (error) {
122+
// Button didn't appear - check for errors
123+
console.error('❌ Annotation button did NOT appear');
124+
125+
// Log all console errors
126+
if (consoleErrors.length > 0) {
127+
console.error('\n📋 Browser Console Errors:');
128+
consoleErrors.forEach((err, i) => {
129+
console.error(` ${i + 1}. ${err}`);
130+
});
131+
}
132+
133+
// Debug: Check what's in the DOM
134+
const hasButton = await page.locator('.pubky-annotation-button').count();
135+
console.log(`Button elements found: ${hasButton}`);
136+
137+
// Check if event listener is set up
138+
const hasListeners = await page.evaluate(() => {
139+
// Check if AnnotationManager is initialized
140+
return (window as any).__graphitiContentScriptLoaded === true;
141+
});
142+
console.log(`Content script loaded: ${hasListeners}`);
143+
144+
throw error;
145+
}
146+
147+
// Verify no critical errors
148+
const criticalErrors = consoleErrors.filter(err =>
149+
err.includes('window is not defined') ||
150+
err.includes('Cannot use import statement') ||
151+
err.includes('chrome.storage') && err.includes('undefined')
152+
);
153+
154+
if (criticalErrors.length > 0) {
155+
console.error('\n❌ Critical errors found:');
156+
criticalErrors.forEach(err => console.error(` - ${err}`));
157+
throw new Error(`Critical errors detected: ${criticalErrors.join('; ')}`);
158+
}
159+
});
160+
161+
test('should capture and report all browser errors', async ({ context, page }) => {
162+
await page.goto('https://example.com', { waitUntil: 'networkidle' });
163+
await page.waitForTimeout(2000); // Wait for extension to initialize
164+
165+
// Try to trigger annotation flow
166+
try {
167+
await page.locator('body').selectText();
168+
await page.waitForSelector('.pubky-annotation-button', { timeout: 3000 });
169+
} catch {
170+
// Button didn't appear, but we want to capture errors
171+
}
172+
173+
// Report all errors found
174+
if (consoleErrors.length > 0) {
175+
console.log('\n📋 All Browser Errors Captured:');
176+
consoleErrors.forEach((err, i) => {
177+
console.log(` ${i + 1}. ${err}`);
178+
});
179+
180+
// Write errors to file for inspection
181+
const fs = require('fs');
182+
const path = require('path');
183+
const errorFile = path.join(__dirname, '../../browser-errors.log');
184+
fs.writeFileSync(errorFile, consoleErrors.join('\n\n'));
185+
console.log(`\n💾 Errors saved to: ${errorFile}`);
186+
} else {
187+
console.log('✅ No console errors detected');
188+
}
189+
190+
// Fail test if there are critical errors
191+
const hasCriticalErrors = consoleErrors.some(err =>
192+
err.includes('window is not defined') ||
193+
err.includes('Cannot use import statement') ||
194+
err.includes('chrome.storage') && err.includes('undefined')
195+
);
196+
197+
if (hasCriticalErrors) {
198+
throw new Error('Critical browser errors detected - see browser-errors.log');
199+
}
200+
});
201+
202+
test.afterEach(async () => {
203+
// Log summary
204+
if (consoleErrors.length > 0 || consoleWarnings.length > 0) {
205+
console.log(`\n📊 Summary: ${consoleErrors.length} errors, ${consoleWarnings.length} warnings`);
206+
}
207+
});
208+
});

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"test:coverage": "vitest --coverage",
1818
"test:ui": "vitest --ui",
1919
"test:e2e": "playwright test",
20-
"test:e2e:ui": "playwright test --ui"
20+
"test:e2e:ui": "playwright test --ui",
21+
"test:e2e:errors": "node run-e2e-with-error-capture.js"
2122
},
2223
"dependencies": {
2324
"@synonymdev/pubky": "latest",

run-e2e-with-error-capture.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Run E2E tests with comprehensive error capture
4+
* This script runs Playwright tests and captures all browser errors
5+
*/
6+
7+
import { execSync } from 'child_process';
8+
import fs from 'fs';
9+
import path from 'path';
10+
import { fileURLToPath } from 'url';
11+
12+
const __filename = fileURLToPath(import.meta.url);
13+
const __dirname = path.dirname(__filename);
14+
15+
console.log('🧪 Running E2E tests with error capture...\n');
16+
17+
try {
18+
// Run Playwright tests
19+
const testOutput = execSync('npx playwright test --reporter=list,json', {
20+
encoding: 'utf8',
21+
cwd: __dirname,
22+
stdio: 'pipe'
23+
});
24+
25+
console.log(testOutput);
26+
27+
// Check for test results JSON
28+
const resultsPath = path.join(__dirname, 'test-results.json');
29+
if (fs.existsSync(resultsPath)) {
30+
const results = JSON.parse(fs.readFileSync(resultsPath, 'utf8'));
31+
console.log(`\n📊 Test Results: ${results.passed} passed, ${results.failed} failed`);
32+
}
33+
34+
} catch (error) {
35+
console.error('❌ E2E tests failed');
36+
console.error(error.stdout || error.stderr || error.message);
37+
38+
// Check for error logs
39+
const errorLogPath = path.join(__dirname, 'browser-errors.log');
40+
if (fs.existsSync(errorLogPath)) {
41+
console.log('\n📋 Browser Errors Captured:');
42+
const errors = fs.readFileSync(errorLogPath, 'utf8');
43+
console.log(errors);
44+
}
45+
46+
process.exit(1);
47+
}

src/content/AnnotationManager.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,15 @@ export class AnnotationManager {
6464
logger.info('ContentScript', 'Initializing annotation manager');
6565
this.injectStyles();
6666

67-
// Load annotation enabled setting (default: true)
67+
// Bind handlers first (before async operations)
68+
this.mouseUpHandler = this.handleTextSelection.bind(this);
69+
this.messageHandler = this.handleMessage.bind(this);
70+
71+
// Set up event listeners immediately
72+
document.addEventListener('mouseup', this.mouseUpHandler);
73+
chrome.runtime.onMessage.addListener(this.messageHandler);
74+
75+
// Load annotation enabled setting (default: true) - async, but don't block
6876
// Use chrome.storage.local directly - don't import utils/storage which has dependencies
6977
try {
7078
const result = await chrome.storage.local.get('annotationsEnabled');
@@ -75,13 +83,6 @@ export class AnnotationManager {
7583
this.annotationsEnabled = true;
7684
}
7785

78-
// Bind handlers once for cleanup
79-
this.mouseUpHandler = this.handleTextSelection.bind(this);
80-
this.messageHandler = this.handleMessage.bind(this);
81-
82-
document.addEventListener('mouseup', this.mouseUpHandler);
83-
chrome.runtime.onMessage.addListener(this.messageHandler);
84-
8586
// Listen for toggle messages
8687
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
8788
if (message.type === 'TOGGLE_ANNOTATIONS') {

src/content/content.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import { PubkyURLHandler } from './PubkyURLHandler';
1515

1616
const initializeContentScript = () => {
1717
logger.info('ContentScript', 'Bootstrapping managers');
18+
19+
// Mark that content script has loaded (for testing)
20+
(window as any).__graphitiContentScriptLoaded = true;
21+
1822
new AnnotationManager();
1923
new DrawingManager();
2024
new PubkyURLHandler();

0 commit comments

Comments
 (0)