diff --git a/.claude/agents/corpus-fixer.md b/.claude/agents/corpus-fixer.md new file mode 100644 index 0000000..44ede88 --- /dev/null +++ b/.claude/agents/corpus-fixer.md @@ -0,0 +1,39 @@ +--- +name: corpus-fixer +description: Use this agent when you need to fix or improve the detection logic for a specific Gitcasso corpus by testing changes in the corpus:view development environment. Examples: Context: User has identified issues with comment spot detection in a specific corpus and wants to test fixes. user: 'The comment detection is missing some spots in corpus ABC123, can you help fix the enhancer logic?' assistant: 'I'll use the corpus-fixer agent to investigate and fix the detection issues in that corpus.' Since the user wants to fix detection logic for a specific corpus, use the corpus-fixer agent to run the corpus:view environment and test changes. Context: User wants to validate that recent changes to an enhancer are working correctly. user: 'I made some changes to the GitHub enhancer, can you test it against corpus XYZ789?' assistant: 'Let me use the corpus-fixer agent to test your enhancer changes against that specific corpus.' The user wants to test enhancer changes against a specific corpus, so use the corpus-fixer agent to validate the changes in the corpus:view environment. +model: inherit +--- + +You are an expert Gitcasso corpus debugging specialist with deep knowledge of browser extension development. You operate exclusively within the `browser-extension` directory and specialize in using the corpus:view development environment to diagnose and fix detection logic issues. + +Your primary workflow: + +1. **Environment Setup**: Always start by reading the documentation at the top of the `corpus-view.ts` file to understand the dev environment. + +2. **Launch Development Environment**: Execute `pnpm corpus:view` to bring up the corpus:view development environment. Ensure the environment starts successfully before proceeding. + +3. **Browser Navigation**: Use the Playwright MCP to interact with the development environment. Navigate to the specific Gitcasso corpus that needs investigation or fixing. + +4. **Code Synchronization**: Always click the button with id `gitcasso-rebuild-btn` to ensure you're testing against the latest code changes. Wait for the rebuild to complete before analyzing results. + +5. **Detection Analysis**: Examine the detected spots in the `gitcasso-comment-spots` element. Analyze what spots are being detected, what might be missing, and identify patterns in the detection logic that need improvement. + +6. **Enhancer Modification**: Based on your analysis, make targeted changes to the specific enhancer's detection logic. Focus on: + - Improving selector accuracy + - Handling edge cases in the DOM structure + - Optimizing detection algorithms for the specific site pattern + - Ensuring compatibility with dynamic content loading + +7. **Iterative Testing**: After making changes, rebuild and test again to validate improvements. Continue this cycle until the detection logic works correctly for the target corpus. + +8. **Documentation**: Clearly explain what issues you found, what changes you made, and why those changes improve the detection logic. + +Key principles: +- Always work incrementally - make small, targeted changes and test frequently +- Focus on the specific corpus mentioned by the user unless told otherwise +- Pay attention to browser console errors and network issues that might affect detection +- Consider how your changes might impact other sites or corpus entries +- Be methodical in your debugging approach - document what you try and what results you observe +- Understand that corpus can be either HAR files (for initial page loads) or HTML snapshots (for post-interaction states) + +You have expertise in CSS selectors, DOM manipulation, JavaScript debugging, and understanding how different websites structure their comment systems. Use this knowledge to create robust, reliable detection logic that works across various edge cases. diff --git a/.claude/agents/har-fixer.md b/.claude/agents/har-fixer.md deleted file mode 100644 index e752310..0000000 --- a/.claude/agents/har-fixer.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: har-fixer -description: Use this agent when you need to fix or improve the detection logic for a specific Gitcasso snapshot by testing changes in the har:view development environment. Examples: Context: User has identified issues with comment spot detection in a specific snapshot and wants to test fixes. user: 'The comment detection is missing some spots in snapshot ABC123, can you help fix the enhancer logic?' assistant: 'I'll use the har-fixer agent to investigate and fix the detection issues in that snapshot.' Since the user wants to fix detection logic for a specific snapshot, use the har-fixer agent to run the har:view environment and test changes. Context: User wants to validate that recent changes to an enhancer are working correctly. user: 'I made some changes to the GitHub enhancer, can you test it against snapshot XYZ789?' assistant: 'Let me use the har-fixer agent to test your enhancer changes against that specific snapshot.' The user wants to test enhancer changes against a specific snapshot, so use the har-fixer agent to validate the changes in the har:view environment. -model: inherit ---- - -You are an expert Gitcasso snapshot debugging specialist with deep knowledge of browser extension development. You operate exclusively within the `browser-extension` directory and specialize in using the har:view development environment to diagnose and fix detection logic issues. - -Your primary workflow: - -1. **Environment Setup**: Always start by reading the documentation at the top of the `har-view.ts` file to understand the dev environment. - -2. **Launch Development Environment**: Execute `pnpm har:view` to bring up the har:view development environment. Ensure the environment starts successfully before proceeding. - -3. **Browser Navigation**: Use the Playwright MCP to interact with the development environment. Navigate to the specific Gitcasso snapshot that needs investigation or fixing. - -4. **Code Synchronization**: Always click the button with id `gitcasso-rebuild-btn` to ensure you're testing against the latest code changes. Wait for the rebuild to complete before analyzing results. - -5. **Detection Analysis**: Examine the detected spots in the `gitcasso-comment-spots` element. Analyze what spots are being detected, what might be missing, and identify patterns in the detection logic that need improvement. - -6. **Enhancer Modification**: Based on your analysis, make targeted changes to the specific enhancer's detection logic. Focus on: - - Improving selector accuracy - - Handling edge cases in the DOM structure - - Optimizing detection algorithms for the specific site pattern - - Ensuring compatibility with dynamic content loading - -7. **Iterative Testing**: After making changes, rebuild and test again to validate improvements. Continue this cycle until the detection logic works correctly for the target snapshot. - -8. **Documentation**: Clearly explain what issues you found, what changes you made, and why those changes improve the detection logic. - -Key principles: -- Always work incrementally - make small, targeted changes and test frequently -- Focus on the specific snapshot mentioned by the user unless told otherwise -- Pay attention to browser console errors and network issues that might affect detection -- Consider how your changes might impact other sites or snapshots -- Be methodical in your debugging approach - document what you try and what results you observe - -You have expertise in CSS selectors, DOM manipulation, JavaScript debugging, and understanding how different websites structure their comment systems. Use this knowledge to create robust, reliable detection logic that works across various edge cases. diff --git a/.claude/commands/finish-wc.md b/.claude/commands/finish-wc.md index b8021a7..7f8127c 100644 --- a/.claude/commands/finish-wc.md +++ b/.claude/commands/finish-wc.md @@ -1 +1,5 @@ -Run `git status` to see the changes in the working copy. Complete whatever tasks are necessary to complete this change. Make sure that `pnpm -r precommit` succeeds. Don't fix `precommit` just be reverting the changes, the goal is to complete the change. \ No newline at end of file +- run `git status` to see the changes in the working copy +- **figure out the intent of the change** +- complete whatever tasks are necessary to complete this change +- make sure that `pnpm precommit` succeeds when you are done +- don't fix `precommit` by reverting the changes, it is more important to complete the initial change \ No newline at end of file diff --git a/browser-extension/README.md b/browser-extension/README.md index 79275aa..d4cfe15 100644 --- a/browser-extension/README.md +++ b/browser-extension/README.md @@ -69,17 +69,42 @@ When the `textarea` gets removed from the page, the `TextareaRegistry` is notifi ## Testing - `npm run playground` gives you a test environment where you can tinker with the popup with various test data, supports hot reload -- `npm run har:view` gives you recordings of various web pages which you can see with and without enhancement by the browser extension - -### Recording new HAR files - -- the har recordings live in `tests/har`, they are complete recordings of the network requests of a single page load -- you can add or change URLs in `tests/har-index.ts` -- `npx playwright codegen https://github.com/login --save-storage=playwright/.auth/gh.json` will store new auth tokens - - login manually, then close the browser - - ***these cookies are very sensitive! we only run this script using a test account that has no permissions or memberships to anything, recommend you do the same!*** -- `pnpm run har:record` this records new snapshots using those auth tokens (it needs args, run it with no args for docs) - - DO NOT COMMIT AND PUSH NEW OR CHANGED `har` files! - - we try to sanitize these (see `har-record.ts` for details) but there may be important PII in them - - if you need new HAR files for something, let us know and we will generate them ourselves using a dummy account - - IF YOUR PR CHANGES OR ADDS HAR FILES WE WILL CLOSE IT. Ask for HAR files and we'll be happy to generate clean ones you can test against. +- `npm run corpus:view` gives you recordings of various web pages which you can see with and without enhancement by the browser extension + +### Test Corpus + +We maintain a corpus of test pages in two formats for testing the browser extension: + +#### HAR Corpus (Automated) + +- For testing initial page loads and network requests +- HAR recordings live in `tests/corpus/har/`, complete recordings of the network requests of a single page load +- You can add or change URLs in `tests/corpus/_corpus-index.ts` +- **Recording new HAR files:** + - `npx playwright codegen https://github.com/login --save-storage=playwright/.auth/gh.json` will store new auth tokens + - login manually, then close the browser + - ***these cookies are very sensitive! we only run this script using a test account that has no permissions or memberships to anything, recommend you do the same!*** + - `pnpm run corpus:record:har` records new HAR files using those auth tokens (it needs args, run it with no args for docs) + - DO NOT COMMIT AND PUSH NEW OR CHANGED HAR files! + - we try to sanitize these (see `har-record.ts` for details) but there may be important PII in them + - if you need new HAR files for something, let us know and we will generate them ourselves using a dummy account + - IF YOUR PR CHANGES OR ADDS HAR FILES WE WILL CLOSE IT. Ask for HAR files and we'll be happy to generate clean ones you can test against. + +#### HTML Corpus (Manual) + +- For testing post-interaction states (e.g., expanded textareas, modal dialogs, dynamic content) +- HTML snapshots live in `tests/corpus/html/`, manually captured using SingleFile browser extension +- All assets are inlined in a single HTML file by SingleFile +- **Creating new HTML corpus files:** + 1. Navigate to the desired page state (click buttons, expand textareas, etc.) + 2. Use SingleFile browser extension to save the complete page + 3. Save the `.html` file to `tests/corpus/html/` with a descriptive name + 4. Add an entry to `tests/corpus/_corpus-index.ts` with `type: 'html'` and a description of the captured state + +#### Viewing Corpus Files + +- Run `pnpm run corpus:view` to start the test server at http://localhost:3001 +- Select any corpus file to view in two modes: + - **Clean**: Original page without extension + - **Gitcasso**: Page with extension injected for testing +- Both HAR and HTML corpus types are supported diff --git a/browser-extension/package.json b/browser-extension/package.json index 9097cba..eb4fef4 100644 --- a/browser-extension/package.json +++ b/browser-extension/package.json @@ -62,8 +62,8 @@ "test": "vitest run", "playground": "vite --config vite.playground.config.ts", "playground:build": "vite build --config vite.playground.config.ts", - "har:record": "tsx tests/har-record.ts", - "har:view": "tsx tests/har-view.ts" + "corpus:record:har": "tsx tests/har-record.ts", + "corpus:view": "tsx tests/corpus-view.ts" }, "type": "module", "version": "0.0.1" diff --git a/browser-extension/src/entrypoints/content.ts b/browser-extension/src/entrypoints/content.ts index c21c33a..3136329 100644 --- a/browser-extension/src/entrypoints/content.ts +++ b/browser-extension/src/entrypoints/content.ts @@ -99,9 +99,7 @@ function enhanceMaybe(textarea: HTMLTextAreaElement) { return } - logger.debug('activating textarea {}', textarea) injectStyles() - try { const location = detectLocation() logger.debug('[gitcasso] Calling tryToEnhance with location:', location) diff --git a/browser-extension/src/lib/config.ts b/browser-extension/src/lib/config.ts index 73f1820..74b71b2 100644 --- a/browser-extension/src/lib/config.ts +++ b/browser-extension/src/lib/config.ts @@ -9,6 +9,6 @@ export type LogLevel = (typeof LOG_LEVELS)[number] export const CONFIG = { ADDED_OVERTYPE_CLASS: 'gitcasso-overtype', EXTENSION_NAME: 'gitcasso', // decorates logs - LOG_LEVEL: 'INFO' satisfies LogLevel, + LOG_LEVEL: 'DEBUG' satisfies LogLevel, MODE: 'PROD' satisfies ModeType, } as const diff --git a/browser-extension/tests/har-fixture.ts b/browser-extension/tests/corpus-fixture.ts similarity index 75% rename from browser-extension/tests/har-fixture.ts rename to browser-extension/tests/corpus-fixture.ts index befcdf9..d7a28c7 100644 --- a/browser-extension/tests/har-fixture.ts +++ b/browser-extension/tests/corpus-fixture.ts @@ -30,21 +30,21 @@ vi.mock('overtype', () => { }) import { describe as baseDescribe, test as baseTest, expect } from 'vitest' -import type { PAGES } from './har/_har-index' -import { cleanupDOM, setupHarDOM } from './har-fixture-utils' +import type { CORPUS } from './corpus/_corpus-index' +import { cleanupDOM, setupDOM } from './corpus-utils' export const describe = baseDescribe // Re-export expect from vitest export { expect } -// Fluent interface for HAR-based tests -export function usingHar(harKey: keyof typeof PAGES) { +// Fluent interface for any corpus type (HAR or HTML) +export function forCorpus(corpusKey: keyof typeof CORPUS) { return { it: (name: string, fn: () => void | Promise) => { - return baseTest(`${harKey}:${name}`, async () => { - // Setup HAR DOM before test - await setupHarDOM(harKey) + return baseTest(`${String(corpusKey)}:${name}`, async () => { + // Setup DOM for any corpus type (delegates to HAR or HTML based on type) + await setupDOM(corpusKey) try { return await fn() diff --git a/browser-extension/tests/har-fixture-utils.ts b/browser-extension/tests/corpus-utils.ts similarity index 65% rename from browser-extension/tests/har-fixture-utils.ts rename to browser-extension/tests/corpus-utils.ts index 15faa4d..9ad40a1 100644 --- a/browser-extension/tests/har-fixture-utils.ts +++ b/browser-extension/tests/corpus-utils.ts @@ -3,7 +3,7 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' import type { Har as HarFile } from 'har-format' import { parseHTML } from 'linkedom' -import { PAGES } from './har/_har-index' +import { CORPUS } from './corpus/_corpus-index' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -32,21 +32,51 @@ export interface TestDOMContext { let currentDOMInstance: any = null let originalGlobals: Partial = {} -export async function loadHtmlFromHar(key: keyof typeof PAGES): Promise { - const url = PAGES[key] - const harPath = path.join(__dirname, 'har', `${key}.har`) +export async function setupDOM(key: keyof typeof CORPUS): Promise { + const entry = CORPUS[key] + if (!entry) { + throw new Error(`Invalid corpus key: ${String(key)}`) + } + + let html: string + if (entry.type === 'har') { + html = await loadRootHtmlStringFromHar(key) + } else if (entry.type === 'html') { + html = await loadHtmlStringFromHtml(key) + } else { + throw new Error(`Unsupported corpus type: ${entry.type}`) + } + const domGlobals = createDOMFromString(html, entry.url) + setupDOMFromHar(domGlobals) + return domGlobals +} + +async function loadRootHtmlStringFromHar(key: keyof typeof CORPUS): Promise { + const entry = CORPUS[key] + if (!entry || entry.type !== 'har') { + throw new Error(`Invalid HAR corpus key: ${String(key)}`) + } + const url = entry.url + const harPath = path.join(__dirname, 'corpus', `${String(key)}.har`) const harContent = await fs.readFile(harPath, 'utf-8') const harData: HarFile = JSON.parse(harContent) const mainEntry = harData.log.entries.find((entry) => entry.request.url === url) - if (!mainEntry) { throw new Error(`No entry found for URL: ${url} in HAR file: ${harPath}`) } + return mainEntry.response.content.text! +} - return mainEntry.response.content.text || '' +async function loadHtmlStringFromHtml(key: keyof typeof CORPUS): Promise { + const entry = CORPUS[key] + if (!entry || entry.type !== 'html') { + throw new Error(`Invalid HTML corpus key: ${String(key)}`) + } + const htmlPath = path.join(__dirname, 'corpus', `${String(key)}.html`) + return await fs.readFile(htmlPath, 'utf-8') } -export function createDOMFromHar(html: string, url: string): TestDOMGlobals { +function createDOMFromString(html: string, url: string): TestDOMGlobals { const dom = parseHTML(html) return { @@ -68,7 +98,7 @@ export function createDOMFromHar(html: string, url: string): TestDOMGlobals { } } -export function setupDOMFromHar(domGlobals: TestDOMGlobals): void { +function setupDOMFromHar(domGlobals: TestDOMGlobals): void { // Store original globals for cleanup originalGlobals = { Document: (globalThis as any).Document, @@ -100,11 +130,3 @@ export function cleanupDOM(): void { originalGlobals = {} } } - -export async function setupHarDOM(key: keyof typeof PAGES): Promise { - const html = await loadHtmlFromHar(key) - const url = PAGES[key] - const domGlobals = createDOMFromHar(html, url) - setupDOMFromHar(domGlobals) - return domGlobals -} diff --git a/browser-extension/tests/corpus-view.ts b/browser-extension/tests/corpus-view.ts new file mode 100644 index 0000000..b7fdd00 --- /dev/null +++ b/browser-extension/tests/corpus-view.ts @@ -0,0 +1,654 @@ +/** + * Corpus Viewer Test Server + * + * This Express server serves recorded corpus files (both HAR and HTML) as live web pages for testing. + * It provides two viewing modes: 'clean' (original page) and 'gitcasso' (with extension injected). + * + * Key components: + * - Loads HAR files from ./corpus/har/ and HTML files from ./corpus/html/ based on CORPUS index in `./_corpus-index.ts` + * - For HAR: Patches URLs in HTML to serve assets locally from HAR data + * - For HTML: Serves SingleFile-captured HTML directly (assets already inlined) + * - Handles asset serving by matching HAR entries to requested paths (HAR corpus only) + * + * Development notes: + * - Injects Gitcasso content script in 'gitcasso' mode with location patching + * - Location patching uses history.pushState to simulate original URLs + * - Chrome APIs are mocked for extension testing outside browser context + * - Extension assets served from `./output/chrome-mv3-dev` via `/chrome-mv3-dev` route + * - Floating rebuild button in gitcasso mode triggers `pnpm run build:dev` and then refresh + * - CommentSpot monitoring panel displays enhanced textareas with spot data and element info + * - Real-time updates every 2 seconds to track textarea enhancement detection and debugging + */ + +import { spawn } from 'node:child_process' +import fs from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import express from 'express' +import type { Har } from 'har-format' +import { CORPUS } from './corpus/_corpus-index' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const app = express() +const PORT = 3001 + +// Constants +const WEBEXTENSION_POLYFILL_PATCH = + 'throw new Error("This script should only be loaded in a browser extension.")' +const WEBEXTENSION_POLYFILL_REPLACEMENT = + 'console.warn("Webextension-polyfill check bypassed for corpus testing")' +const BROWSER_API_MOCKS = + 'window.chrome=window.chrome||{runtime:{getURL:path=>"chrome-extension://gitcasso-test/"+path,onMessage:{addListener:()=>{}},sendMessage:()=>Promise.resolve(),id:"gitcasso-test"}};window.browser=window.chrome;' +const PERMISSIVE_CSP = "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: http: https:;" + +// UI Styles +const REBUILD_BUTTON_STYLES = ` + position: fixed; + top: 20px; + right: 20px; + width: 50px; + height: 50px; + background: #007acc; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + cursor: pointer; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + z-index: 999999; + user-select: none; + transition: all 0.2s ease; + font-family: system-ui, -apple-system, sans-serif; +` + +const COMMENT_SPOT_STYLES = { + container: + 'position: fixed; top: 80px; right: 20px; width: 400px; max-height: 400px; background: rgba(255, 255, 255, 0.95); border: 1px solid #ddd; border-radius: 8px; padding: 15px; font-family: Monaco, Menlo, Ubuntu Mono, monospace; font-size: 11px; line-height: 1.4; overflow-y: auto; z-index: 999998; box-shadow: 0 4px 12px rgba(0,0,0,0.2); backdrop-filter: blur(10px);', + empty: 'color: #666; font-style: italic;', + header: 'font-weight: bold; margin-bottom: 8px; color: #333;', + jsonPre: 'margin: 4px 0; font-size: 10px;', + noInfo: 'color: #999; font-style: italic; margin-top: 4px;', + spotContainer: 'margin-bottom: 12px; padding: 8px; border: 1px solid #eee; border-radius: 4px;', + spotTitle: 'font-weight: bold; color: #555;', + textareaHeader: 'font-weight: bold; color: #007acc; margin-top: 8px;', + textareaPre: 'margin: 4px 0; font-size: 10px; color: #666;', +} + +// Middleware to parse JSON bodies +app.use(express.json()) + +// Store HAR json +const harCache = new Map() + +// Extract URL parts for location patching +function getUrlParts(key: string) { + const entry = CORPUS[key] + if (!entry) { + throw new Error(`Corpus entry not found: ${key}`) + } + const originalUrl = entry.url + const url = new URL(originalUrl) + return { + host: url.host, + hostname: url.hostname, + href: originalUrl, + pathname: url.pathname, + } +} + +// Load and cache HAR file +async function loadHar(key: string): Promise { + if (harCache.has(key)) { + return harCache.get(key)! + } + + const harPath = path.join(__dirname, 'corpus', `${key}.har`) + const harContent = await fs.readFile(harPath, 'utf-8') + const harData = JSON.parse(harContent) + harCache.set(key, harData) + return harData +} + +// Add redirect routes for each CORPUS URL to handle refreshes +Object.entries(CORPUS).forEach(([key, entry]) => { + const urlObj = new URL(entry.url) + app.get(urlObj.pathname, (_req, res) => { + res.redirect(`/corpus/${key}/gitcasso`) + }) +}) + +// List available corpus files +app.get('/', async (_req, res) => { + try { + const links = Object.entries(CORPUS) + .map(([key, entry]) => { + const description = entry.description + ? `
${entry.description}
` + : '' + return ` +
  • +
    +
    ${key}
    +
    ${entry.type.toUpperCase()}
    + ${description} +
    + +
  • + ` + }) + .join('') + + res.send(` + + + + Corpus Viewer + + + +

    πŸ“„ Corpus Viewer

    +

    Select a recorded page to view:

    +
      ${links}
    +
    +

    Corpus Types

    +

    HAR: Automated network captures of initial page loads

    +

    HTML: Manual SingleFile captures of post-interaction states

    +
    + + + `) + } catch (_error) { + res.status(500).send('Error listing corpus files') + } +}) + +// Serve the main page from corpus +app.get('/corpus/:key/:mode(clean|gitcasso)', async (req, res) => { + try { + // biome-ignore lint/complexity/useLiteralKeys: type comes from path string + const key = req.params['key'] + // biome-ignore lint/complexity/useLiteralKeys: type comes from path string + const mode = req.params['mode'] as 'clean' | 'gitcasso' + + if (!key || !(key in CORPUS)) { + return res.status(400).send('Invalid key - not found in CORPUS') + } + + const entry = CORPUS[key]! + + if (entry.type === 'har') { + // Handle HAR corpus + const harData = await loadHar(key) + const originalUrl = entry.url + const mainEntry = + harData.log.entries.find( + (entry) => + entry.request.url === originalUrl && + entry.response.content.mimeType?.includes('text/html') && + entry.response.content.text, + ) || + harData.log.entries.find( + (entry) => + entry.response.status === 200 && + entry.response.content.mimeType?.includes('text/html') && + entry.response.content.text, + ) + if (!mainEntry) { + return res.status(404).send('No HTML content found in HAR file') + } + + // Extract all domains from HAR entries for dynamic replacement + const domains = new Set() + harData.log.entries.forEach((entry) => { + try { + const url = new URL(entry.request.url) + domains.add(url.hostname) + } catch { + // Skip invalid URLs + } + }) + + // Replace external URLs with local asset URLs + let html = mainEntry.response.content.text! + domains.forEach((domain) => { + const escapedDomain = domain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const regex = new RegExp(`https?://${escapedDomain}`, 'g') + html = html.replace(regex, `/asset/${key}`) + }) + if (mode === 'gitcasso') { + html = injectGitcassoScriptForHAR(key, html) + } + return res.send(html) + } else if (entry.type === 'html') { + // Handle HTML corpus + const htmlPath = path.join(__dirname, 'corpus', `${key}.html`) + let html = await fs.readFile(htmlPath, 'utf-8') + + // Strip CSP headers that might block our injected scripts + html = stripCSPFromHTML(html) + + if (mode === 'gitcasso') { + html = await injectGitcassoScriptForHTML(key, html) + } + + // Set permissive headers for HTML corpus + res.set({ + 'Content-Security-Policy': PERMISSIVE_CSP, + 'X-Content-Type-Options': 'nosniff', + }) + + return res.send(html) + } else { + return res.status(400).send('Unknown corpus type') + } + } catch (error) { + console.error('Error serving page:', error) + return res.status(500).send('Error loading page') + } +}) + +// Serve assets from HAR file (only for HAR corpus) +app.get('/asset/:key/*', async (req, res) => { + try { + const key = req.params.key + if (!key || !(key in CORPUS)) { + return res.status(400).send('Invalid key - not found in CORPUS') + } + + const entry = CORPUS[key]! + if (entry.type !== 'har') { + return res.status(400).send('Asset serving only available for HAR corpus') + } + + const assetPath = (req.params as any)[0] as string + + const harData = await loadHar(key) + + // Find matching asset in HAR by full URL comparison + const assetEntry = harData.log.entries.find((entry) => { + try { + const url = new URL(entry.request.url) + return matchAssetPath(url, assetPath) + } catch { + return false + } + }) + + if (!assetEntry) { + return res.status(404).send('Asset not found') + } + + const content = assetEntry.response.content + const mimeType = content.mimeType || 'application/octet-stream' + res.set('Content-Type', mimeType) + if (content.encoding === 'base64') { + return res.send(Buffer.from(content.text!, 'base64')) + } else { + return res.send(content.text!) + } + } catch (error) { + console.error('Error serving asset:', error) + return res.status(404).send('Asset not found') + } +}) + +// Serve extension assets from filesystem +app.use('/chrome-mv3-dev', express.static(path.join(__dirname, '..', '.output', 'chrome-mv3-dev'))) + +// Rebuild endpoint +app.post('/rebuild', async (_req, res) => { + try { + console.log('Rebuild triggered via API') + + // Run pnpm run build:dev + const buildProcess = spawn('pnpm', ['run', 'build:dev'], { + cwd: path.join(__dirname, '..', '..'), + stdio: ['pipe', 'pipe', 'pipe'], + }) + + let stdout = '' + let stderr = '' + + buildProcess.stdout.on('data', (data) => { + stdout += data.toString() + console.log('[BUILD]', data.toString().trim()) + }) + + buildProcess.stderr.on('data', (data) => { + stderr += data.toString() + console.error('[BUILD ERROR]', data.toString().trim()) + }) + + buildProcess.on('close', (code) => { + if (code === 0) { + console.log('Build completed successfully') + res.json({ message: 'Build completed successfully', success: true }) + } else { + console.error('Build failed with code:', code) + res.status(500).json({ + error: stderr || stdout, + message: 'Build failed', + success: false, + }) + } + }) + + buildProcess.on('error', (error) => { + console.error('Failed to start build process:', error) + res.status(500).json({ + error: error.message, + message: 'Failed to start build process', + success: false, + }) + }) + } catch (error) { + console.error('Rebuild endpoint error:', error) + res.status(500).json({ + error: error instanceof Error ? error.message : 'Unknown error', + message: 'Internal server error', + success: false, + }) + } +}) + +app.listen(PORT, () => { + console.log(`Corpus Viewer running at http://localhost:${PORT}`) + console.log('Click the links to view recorded pages') +}) + +// Strip CSP meta tags and headers from HTML that might block our scripts +function stripCSPFromHTML(html: string): string { + // Remove CSP meta tags + html = html.replace(/]*http-equiv\s*=\s*["']content-security-policy["'][^>]*>/gi, '') + html = html.replace(/]*name\s*=\s*["']content-security-policy["'][^>]*>/gi, '') + + // Remove any other restrictive security meta tags + html = html.replace(/]*http-equiv\s*=\s*["']x-content-type-options["'][^>]*>/gi, '') + + return html +} + +// Shared UI Component Functions +function createRebuildButtonScript(): string { + return ` + // Create floating rebuild button + const rebuildButton = document.createElement('div'); + rebuildButton.id = 'gitcasso-rebuild-btn'; + rebuildButton.innerHTML = 'πŸ”„'; + rebuildButton.title = 'Rebuild Extension'; + rebuildButton.style.cssText = \`${REBUILD_BUTTON_STYLES}\`; + + rebuildButton.addEventListener('mouseenter', () => { + rebuildButton.style.transform = 'scale(1.1)'; + rebuildButton.style.backgroundColor = '#005a9e'; + }); + + rebuildButton.addEventListener('mouseleave', () => { + rebuildButton.style.transform = 'scale(1)'; + rebuildButton.style.backgroundColor = '#007acc'; + }); + + rebuildButton.addEventListener('click', async () => { + try { + rebuildButton.innerHTML = '⏳'; + rebuildButton.style.backgroundColor = '#ffa500'; + rebuildButton.title = 'Rebuilding...'; + + const response = await fetch('/rebuild', { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + + const result = await response.json(); + + if (result.success) { + rebuildButton.innerHTML = 'βœ…'; + rebuildButton.style.backgroundColor = '#28a745'; + rebuildButton.title = 'Build successful! Reloading...'; + + setTimeout(() => { + location.reload(true); + }, 1000); + } else { + rebuildButton.innerHTML = '❌'; + rebuildButton.style.backgroundColor = '#dc3545'; + rebuildButton.title = 'Build failed: ' + (result.error || result.message); + + setTimeout(() => { + rebuildButton.innerHTML = 'πŸ”„'; + rebuildButton.style.backgroundColor = '#007acc'; + rebuildButton.title = 'Rebuild Extension'; + }, 3000); + } + } catch (error) { + console.error('Rebuild failed:', error); + rebuildButton.innerHTML = '❌'; + rebuildButton.style.backgroundColor = '#dc3545'; + rebuildButton.title = 'Network error: ' + error.message; + + setTimeout(() => { + rebuildButton.innerHTML = 'πŸ”„'; + rebuildButton.style.backgroundColor = '#007acc'; + rebuildButton.title = 'Rebuild Extension'; + }, 3000); + } + }); + + document.body.appendChild(rebuildButton); + ` +} + +function createCommentSpotDisplayScript(urlParts: ReturnType): string { + return ` + // Create CommentSpot display + const commentSpotDisplay = document.createElement('div'); + commentSpotDisplay.id = 'gitcasso-comment-spots'; + commentSpotDisplay.style.cssText = '${COMMENT_SPOT_STYLES.container}'; + + // Simplified display formatting + const styles = ${JSON.stringify(COMMENT_SPOT_STYLES)}; + + function updateCommentSpotDisplay() { + const textareas = document.querySelectorAll('textarea'); + const spotsFound = []; + + for (const textarea of textareas) { + const forValue = 'id=' + textarea.id + ' name=' + textarea.name + ' className=' + textarea.className; + const enhancedItem = window.gitcassoTextareaRegistry ? window.gitcassoTextareaRegistry.get(textarea) : undefined; + if (enhancedItem) { + spotsFound.push({ + for: forValue, + spot: enhancedItem.spot, + title: enhancedItem.enhancer.tableTitle(enhancedItem.spot), + }); + } else { + spotsFound.push({ + for: forValue, + spot: 'NO_SPOT', + }); + } + } + + console.log('Enhanced textareas:', spotsFound.filter(s => s.spot !== 'NO_SPOT').length); + console.log('All textareas on page:', textareas.length); + commentSpotDisplay.innerHTML = '
    ${urlParts.href}\\n' + JSON.stringify(spotsFound, null, 2) + '
    '; + } + + // Initial update + updateCommentSpotDisplay() + setTimeout(updateCommentSpotDisplay, 100); + setTimeout(updateCommentSpotDisplay, 200); + setTimeout(updateCommentSpotDisplay, 400); + setTimeout(updateCommentSpotDisplay, 800); + + // Update display periodically + setInterval(updateCommentSpotDisplay, 2000); + + document.body.appendChild(commentSpotDisplay); + ` +} + +// Asset matching helper +function matchAssetPath(url: URL, assetPath: string): boolean { + // First try exact path match + if (url.pathname === `/${assetPath}`) { + return true + } + // Then try path ending match (for nested paths) + if (url.pathname.endsWith(`/${assetPath}`)) { + return true + } + // Handle query parameters - check if path without query matches + const pathWithoutQuery = url.pathname + url.search + if (pathWithoutQuery === `/${assetPath}` || pathWithoutQuery.endsWith(`/${assetPath}`)) { + return true + } + return false +} + +// Unified script injection with different content script loading strategies +function createGitcassoScript( + urlParts: ReturnType, + contentScriptCode?: string, +): string { + const contentScriptSetup = contentScriptCode + ? // Direct embedding (for HTML corpus) + ` + // Set up mocked location + window.gitcassoMockLocation = { + host: '${urlParts.host}', + pathname: '${urlParts.pathname}' + }; + + // Set up browser API mocks + window.chrome = window.chrome || { + runtime: { + getURL: path => "chrome-extension://gitcasso-test/" + path, + onMessage: { addListener: () => {} }, + sendMessage: () => Promise.resolve(), + id: "gitcasso-test" + } + }; + window.browser = window.chrome; + + // Execute the patched content script directly + try { + ${contentScriptCode} + console.log('Gitcasso content script loaded with location patching for:', '${urlParts.href}'); + } catch (error) { + console.error('Failed to execute content script:', error); + } + ` + : // Fetch-based loading (for HAR corpus) + ` + // Fetch and patch the content script to remove webextension-polyfill issues + fetch('/chrome-mv3-dev/content-scripts/content.js') + .then(response => response.text()) + .then(code => { + console.log('Fetched content script, patching webextension-polyfill and detectLocation...'); + + // Replace the problematic webextension-polyfill error check + let patchedCode = code.replace( + '${WEBEXTENSION_POLYFILL_PATCH}', + '${WEBEXTENSION_POLYFILL_REPLACEMENT}' + ); + window.gitcassoMockLocation = { + host: '${urlParts.host}', + pathname: '${urlParts.pathname}' + }; + + // Execute the patched script with browser API mocks prepended + const script = document.createElement('script'); + script.textContent = '${BROWSER_API_MOCKS}' + patchedCode; + document.head.appendChild(script); + console.log('Gitcasso content script loaded with location patching for:', '${urlParts.href}'); + }) + .catch(error => { + console.error('Failed to load and patch content script:', error); + }); + ` + + return ` + + ` +} + +// HAR version - uses fetch() to load content script (original approach) +function injectGitcassoScriptForHAR(key: string, html: string): string { + const urlParts = getUrlParts(key) + const contentScriptTag = createGitcassoScript(urlParts) + + if (html.includes('')) { + return html.replace('', `${contentScriptTag}`) + } else { + return html + contentScriptTag + } +} + +// HTML version - embeds content script directly to avoid CSP issues +async function injectGitcassoScriptForHTML(key: string, html: string): Promise { + const urlParts = getUrlParts(key) + + // Read and embed the content script directly to avoid CSP issues + let contentScriptCode = '' + try { + const contentScriptPath = path.join( + __dirname, + '..', + '.output', + 'chrome-mv3-dev', + 'content-scripts', + 'content.js', + ) + contentScriptCode = await fs.readFile(contentScriptPath, 'utf-8') + + // Patch the content script to remove webextension-polyfill issues + contentScriptCode = contentScriptCode.replace( + WEBEXTENSION_POLYFILL_PATCH, + WEBEXTENSION_POLYFILL_REPLACEMENT, + ) + } catch (error) { + console.warn('Could not read content script, using fallback:', error) + contentScriptCode = 'console.warn("Content script not found - extension may not be built");' + } + + const contentScriptTag = createGitcassoScript(urlParts, contentScriptCode) + + if (html.includes('')) { + return html.replace('', `${contentScriptTag}`) + } else { + return html + contentScriptTag + } +} diff --git a/browser-extension/tests/corpus/_corpus-index.ts b/browser-extension/tests/corpus/_corpus-index.ts new file mode 100644 index 0000000..b240898 --- /dev/null +++ b/browser-extension/tests/corpus/_corpus-index.ts @@ -0,0 +1,39 @@ +export type CorpusType = 'har' | 'html' + +export interface CorpusEntry { + url: string + type: CorpusType + description?: string // Helpful for HTML corpus to describe the captured state +} + +export const CORPUS: Record = { + // HAR corpus (initial page loads) + gh_issue: { + type: 'har', + url: 'https://github.com/diffplug/selfie/issues/523', + }, + gh_issue_populated_comment: { + description: 'comment text box has some text', + type: 'html', + url: 'https://github.com/diffplug/selfie/issues/523', + }, + gh_new_issue: { + type: 'har', + url: 'https://github.com/diffplug/selfie/issues/new', + }, + gh_new_pr: { + type: 'har', + url: 'https://github.com/diffplug/selfie/compare/main...cavia-porcellus:selfie:main?expand=1', + }, + gh_pr: { + type: 'har', + url: 'https://github.com/diffplug/selfie/pull/517', + }, + // HTML corpus (captured after user interactions via SingleFile) + // Add new entries here as needed, e.g.: + // gh_issue_with_comment_preview: { + // url: 'https://github.com/diffplug/selfie/issues/523', + // type: 'html', + // description: 'Issue page with comment textarea expanded and preview tab active' + // } +} as const diff --git a/browser-extension/tests/har/gh_issue.har b/browser-extension/tests/corpus/gh_issue.har similarity index 100% rename from browser-extension/tests/har/gh_issue.har rename to browser-extension/tests/corpus/gh_issue.har diff --git a/browser-extension/tests/corpus/gh_issue_populated_comment.html b/browser-extension/tests/corpus/gh_issue_populated_comment.html new file mode 100644 index 0000000..77811a3 --- /dev/null +++ b/browser-extension/tests/corpus/gh_issue_populated_comment.html @@ -0,0 +1,1578 @@ + + + + + + + + + + + + +[jvm] docs for VCR Β· Issue #523 Β· diffplug/selfie + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + +
    + Skip to content + + + + + + + + +
    +
    + + + + + + +
    +
    +
    + +
    + + + +
    +
    +
    + + + + + + +

    [jvm] docs for VCR #523

    Activity

    oss-test-user

    Add a comment

    new Comment
    Markdown input: edit mode selected.

    Metadata

    Metadata

    Assignees

    No one assigned

      Labels

      Type

      No type

      Projects

      No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Notifications

      You're not receiving notifications from this thread.

      Issue actions

        +
        +
        +
        +
        + + + + + + + + +
        +
        [jvm] docs for VCR Β· Issue #523 Β· diffplug/selfie
        +
        + + \ No newline at end of file diff --git a/browser-extension/tests/har/gh_new_issue.har b/browser-extension/tests/corpus/gh_new_issue.har similarity index 100% rename from browser-extension/tests/har/gh_new_issue.har rename to browser-extension/tests/corpus/gh_new_issue.har diff --git a/browser-extension/tests/har/gh_new_pr.har b/browser-extension/tests/corpus/gh_new_pr.har similarity index 100% rename from browser-extension/tests/har/gh_new_pr.har rename to browser-extension/tests/corpus/gh_new_pr.har diff --git a/browser-extension/tests/har/gh_pr.har b/browser-extension/tests/corpus/gh_pr.har similarity index 100% rename from browser-extension/tests/har/gh_pr.har rename to browser-extension/tests/corpus/gh_pr.har diff --git a/browser-extension/tests/har-record.ts b/browser-extension/tests/har-record.ts index 8125ed3..eb9af1b 100644 --- a/browser-extension/tests/har-record.ts +++ b/browser-extension/tests/har-record.ts @@ -1,7 +1,7 @@ import fs from 'node:fs/promises' import path from 'node:path' import { chromium } from '@playwright/test' -import { PAGES } from './har/_har-index' +import { CORPUS } from './corpus/_corpus-index' // Convert glob pattern to regex function globToRegex(pattern: string): RegExp { @@ -12,10 +12,12 @@ function globToRegex(pattern: string): RegExp { return new RegExp(`^${regexPattern}$`) } -// Filter pages based on pattern -function filterPages(pattern: string) { +// Filter HAR corpus entries based on pattern +function filterHarEntries(pattern: string) { const regex = globToRegex(pattern) - return Object.entries(PAGES).filter(([name]) => regex.test(name)) + return Object.entries(CORPUS) + .filter(([name, entry]) => regex.test(name) && entry.type === 'har') + .map(([name, entry]) => [name, entry.url] as const) } const FILTER = @@ -35,7 +37,7 @@ async function record(name: string, url: string) { const context = await browser.newContext({ recordHar: { mode: 'minimal', // smaller; omits cookies etc. - path: `tests/har/${name}.har`, + path: `tests/corpus/har/${name}.har`, urlFilter: FILTER, // restrict scope to GitHub + assets }, storageState: 'playwright/.auth/gh.json', // local-only; never commit @@ -63,7 +65,7 @@ function stripHeaders(headers?: any[]) { async function sanitize(filename: string) { console.log('Sanitizing:', filename) - const p = path.join('tests/har', filename) + const p = path.join('tests/corpus/har', filename) const har = JSON.parse(await fs.readFile(p, 'utf8')) for (const e of har.log?.entries ?? []) { @@ -87,47 +89,51 @@ async function sanitize(filename: string) { // If no argument provided, show available keys if (!pattern) { - console.log('Available recording targets:') - for (const [name] of Object.entries(PAGES)) { - console.log(` ${name}`) + console.log('Available HAR recording targets:') + for (const [name, entry] of Object.entries(CORPUS)) { + if (entry.type === 'har') { + console.log(` ${name}`) + } } - console.log('\nUsage: pnpm run har:record ') + console.log('\nUsage: pnpm run corpus:record:har ') console.log('Examples:') - console.log(' pnpm run har:record "*" # Record all') - console.log(' pnpm run har:record "github_*" # Record all github_*') - console.log(' pnpm run har:record "github_issue" # Record specific target') + console.log(' pnpm run corpus:record:har "*" # Record all HAR targets') + console.log(' pnpm run corpus:record:har "gh_*" # Record all gh_* targets') + console.log(' pnpm run corpus:record:har "gh_issue" # Record specific target') return } - // Filter pages based on pattern - const pagesToRecord = filterPages(pattern) + // Filter HAR entries based on pattern + const entriesToRecord = filterHarEntries(pattern) - if (pagesToRecord.length === 0) { - console.log(`No targets match pattern: ${pattern}`) - console.log('Available targets:') - for (const [name] of Object.entries(PAGES)) { - console.log(` ${name}`) + if (entriesToRecord.length === 0) { + console.log(`No HAR targets match pattern: ${pattern}`) + console.log('Available HAR targets:') + for (const [name, entry] of Object.entries(CORPUS)) { + if (entry.type === 'har') { + console.log(` ${name}`) + } } return } - console.log(`Recording ${pagesToRecord.length} target(s) matching "${pattern}":`) - for (const [name] of pagesToRecord) { + console.log(`Recording ${entriesToRecord.length} HAR target(s) matching "${pattern}":`) + for (const [name] of entriesToRecord) { console.log(` ${name}`) } console.log() - await fs.mkdir('tests/har', { recursive: true }) + await fs.mkdir('tests/corpus/har', { recursive: true }) // Record filtered HAR files - for (const [name, url] of pagesToRecord) { + for (const [name, url] of entriesToRecord) { await record(name, url) } console.log('Recording complete. Sanitizing...') // Sanitize recorded HAR files - for (const [name] of pagesToRecord) { + for (const [name] of entriesToRecord) { await sanitize(`${name}.har`) } diff --git a/browser-extension/tests/har-view.ts b/browser-extension/tests/har-view.ts deleted file mode 100644 index 6586eba..0000000 --- a/browser-extension/tests/har-view.ts +++ /dev/null @@ -1,505 +0,0 @@ -/** - * HAR Page Viewer Test Server - * - * This Express server serves recorded HAR files as live web pages for testing. - * It provides two viewing modes: 'clean' (original page) and 'gitcasso' (with extension injected). - * - * Key components: - * - Loads HAR files from ./har/ directory based on PAGES index in `./har/_har_index.ts` - * - Patches URLs in HTML to serve assets locally from HAR data - * - Handles asset serving by matching HAR entries to requested paths - * - * Development notes: - * - Injects Gitcasso content script in 'gitcasso' mode with location patching - * - Location patching uses history.pushState to simulate original URLs - * - Chrome APIs are mocked for extension testing outside browser context - * - Extension assets served from `./output/chrome-mv3-dev` via `/chrome-mv3-dev` route - * - Floating rebuild button in gitcasso mode triggers `pnpm run build:dev` and then refresh - * - CommentSpot monitoring panel displays enhanced textareas with spot data and element info - * - Real-time updates every 2 seconds to track textarea enhancement detection and debugging - */ - -import { spawn } from 'node:child_process' -import { error } from 'node:console' -import fs from 'node:fs/promises' -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import express from 'express' -import type { Har } from 'har-format' -import { PAGES } from './har/_har-index' - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const app = express() -const PORT = 3001 - -// Middleware to parse JSON bodies -app.use(express.json()) - -// Store HAR json -const harCache = new Map() - -// Extract URL parts for location patching -function getUrlParts(key: keyof typeof PAGES) { - const originalUrl = PAGES[key] - const url = new URL(originalUrl) - return { - host: url.host, - hostname: url.hostname, - href: originalUrl, - pathname: url.pathname, - } -} - -// Load and cache HAR file -async function loadHar(key: keyof typeof PAGES): Promise { - if (harCache.has(key)) { - return harCache.get(key)! - } - - const harPath = path.join(__dirname, 'har', `${key}.har`) - const harContent = await fs.readFile(harPath, 'utf-8') - const harData = JSON.parse(harContent) - harCache.set(key, harData) - return harData -} - -// Add redirect routes for each PAGES URL to handle refreshes -Object.entries(PAGES).forEach(([key, url]) => { - const urlObj = new URL(url) - app.get(urlObj.pathname, (_req, res) => { - res.redirect(`/har/${key}/gitcasso`) - }) -}) - -// List available HAR files -app.get('/', async (_req, res) => { - try { - const harDir = path.join(__dirname, 'har') - const files = await fs.readdir(harDir) - const harFiles = files.filter((file) => file.endsWith('.har')) - - const links = harFiles - .map((file) => { - const basename = path.basename(file, '.har') - return ` -
      • -
        ${basename}
        - -
      • - ` - }) - .join('') - - res.send(` - - - - HAR Page Viewer - - - -

        πŸ“„ HAR Page Viewer

        -

        Select a recorded page to view:

        -
          ${links}
        - - - `) - } catch (_error) { - res.status(500).send('Error listing HAR files') - } -}) - -// Serve the main HTML page from HAR -app.get('/har/:key/:mode(clean|gitcasso)', async (req, res) => { - try { - // biome-ignore lint/complexity/useLiteralKeys: type comes from path string - const key = req.params['key'] as keyof typeof PAGES - // biome-ignore lint/complexity/useLiteralKeys: type comes from path string - const mode = req.params['mode'] as 'clean' | 'gitcasso' - if (!(key in PAGES)) { - return res.status(400).send('Invalid key - not found in PAGES') - } - - // Find the main HTML response - const harData = await loadHar(key) - const originalUrl = PAGES[key] - const mainEntry = - harData.log.entries.find( - (entry) => - entry.request.url === originalUrl && - entry.response.content.mimeType?.includes('text/html') && - entry.response.content.text, - ) || - harData.log.entries.find( - (entry) => - entry.response.status === 200 && - entry.response.content.mimeType?.includes('text/html') && - entry.response.content.text, - ) - if (!mainEntry) { - return res.status(404).send('No HTML content found in HAR file') - } - - // Extract all domains from HAR entries for dynamic replacement - const domains = new Set() - harData.log.entries.forEach((entry) => { - try { - const url = new URL(entry.request.url) - domains.add(url.hostname) - } catch { - // Skip invalid URLs - } - }) - - // Replace external URLs with local asset URLs - let html = mainEntry.response.content.text! - domains.forEach((domain) => { - const escapedDomain = domain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - const regex = new RegExp(`https?://${escapedDomain}`, 'g') - html = html.replace(regex, `/asset/${key}`) - }) - if (mode === 'gitcasso') { - html = injectGitcassoScript(key, html) - } - return res.send(html) - } catch (error) { - console.error('Error serving page:', error) - return res.status(500).send('Error loading page') - } -}) - -// Serve assets from HAR file -app.get('/asset/:key/*', async (req, res) => { - try { - const key = req.params.key as keyof typeof PAGES - if (!(key in PAGES)) { - return res.status(400).send('Invalid key - not found in PAGES') - } - const assetPath = (req.params as any)[0] as string - - const harData = await loadHar(key) - - // Find matching asset in HAR by full URL comparison - const assetEntry = harData.log.entries.find((entry) => { - try { - const url = new URL(entry.request.url) - // First try exact path match - if (url.pathname === `/${assetPath}`) { - return true - } - // Then try path ending match (for nested paths) - if (url.pathname.endsWith(`/${assetPath}`)) { - return true - } - // Handle query parameters - check if path without query matches - const pathWithoutQuery = url.pathname + url.search - if (pathWithoutQuery === `/${assetPath}` || pathWithoutQuery.endsWith(`/${assetPath}`)) { - return true - } - return false - } catch { - return false - } - }) - - if (!assetEntry) { - return res.status(404).send('Asset not found') - } - - const content = assetEntry.response.content - const mimeType = content.mimeType || 'application/octet-stream' - res.set('Content-Type', mimeType) - if (content.encoding === 'base64') { - return res.send(Buffer.from(content.text!, 'base64')) - } else { - return res.send(content.text!) - } - } catch (error) { - console.error('Error serving asset:', error) - return res.status(404).send('Asset not found') - } -}) -// Serve extension assets from filesystem -app.use('/chrome-mv3-dev', express.static(path.join(__dirname, '..', '.output', 'chrome-mv3-dev'))) - -// Rebuild endpoint -app.post('/rebuild', async (_req, res) => { - try { - console.log('Rebuild triggered via API') - - // Run pnpm run rebuild:dev - const buildProcess = spawn('pnpm', ['run', 'build:dev'], { - cwd: path.join(__dirname, '..', '..'), - stdio: ['pipe', 'pipe', 'pipe'], - }) - - let stdout = '' - let stderr = '' - - buildProcess.stdout.on('data', (data) => { - stdout += data.toString() - console.log('[BUILD]', data.toString().trim()) - }) - - buildProcess.stderr.on('data', (data) => { - stderr += data.toString() - console.error('[BUILD ERROR]', data.toString().trim()) - }) - - buildProcess.on('close', (code) => { - if (code === 0) { - console.log('Build completed successfully') - res.json({ message: 'Build completed successfully', success: true }) - } else { - console.error('Build failed with code:', code) - res.status(500).json({ - error: stderr || stdout, - message: 'Build failed', - success: false, - }) - } - }) - - buildProcess.on('error', (error) => { - console.error('Failed to start build process:', error) - res.status(500).json({ - error: error.message, - message: 'Failed to start build process', - success: false, - }) - }) - } catch (error) { - console.error('Rebuild endpoint error:', error) - res.status(500).json({ - error: error instanceof Error ? error.message : 'Unknown error', - message: 'Internal server error', - success: false, - }) - } -}) - -app.listen(PORT, () => { - console.log(`HAR Page Viewer running at http://localhost:${PORT}`) - console.log('Click the links to view recorded pages') -}) - -function injectGitcassoScript(key: keyof typeof PAGES, html: string) { - const urlParts = getUrlParts(key) - - // Inject patched content script with location patching - const contentScriptTag = - ` - - ` - if (!html.includes('')) { - throw error('No closing body tag, nowhere to put the content script!') - } - return html.replace('', `${contentScriptTag}`) -} diff --git a/browser-extension/tests/har/_har-index.ts b/browser-extension/tests/har/_har-index.ts deleted file mode 100644 index dfd73e2..0000000 --- a/browser-extension/tests/har/_har-index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const PAGES = { - gh_issue: 'https://github.com/diffplug/selfie/issues/523', - gh_new_issue: 'https://github.com/diffplug/selfie/issues/new', - gh_new_pr: - 'https://github.com/diffplug/selfie/compare/main...cavia-porcellus:selfie:main?expand=1', - gh_pr: 'https://github.com/diffplug/selfie/pull/517', -} as const diff --git a/browser-extension/tests/lib/enhancers/github.test.ts b/browser-extension/tests/lib/enhancers/github.test.ts index 35692c5..7e47a14 100644 --- a/browser-extension/tests/lib/enhancers/github.test.ts +++ b/browser-extension/tests/lib/enhancers/github.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, usingHar } from '../../har-fixture' +import { describe, expect, forCorpus as withCorpus } from '../../corpus-fixture' // must import fixture **first** for mocks, the `expect` keeps biome from changing sort-order expect @@ -35,7 +35,7 @@ function enhancements(document: Document, window: Window) { } describe('github', () => { - usingHar('gh_pr').it('should create the correct spot object', async () => { + withCorpus('gh_pr').it('should create the correct spot object', async () => { expect(enhancements(document, window)).toMatchInlineSnapshot(` [ { @@ -70,7 +70,7 @@ describe('github', () => { ] `) }) - usingHar('gh_new_pr').it('should create the correct spot object', async () => { + withCorpus('gh_new_pr').it('should create the correct spot object', async () => { expect(enhancements(document, window)).toMatchInlineSnapshot(` [ { @@ -102,7 +102,7 @@ describe('github', () => { ] `) }) - usingHar('gh_issue').it('should create the correct spot object', async () => { + withCorpus('gh_issue').it('no enhancement on initial page load', async () => { expect(enhancements(document, window)).toMatchInlineSnapshot(` [ { @@ -112,7 +112,42 @@ describe('github', () => { ] `) }) - usingHar('gh_new_issue').it('should create the correct spot object', async () => { + withCorpus('gh_issue_populated_comment').it('should create the correct spot object', async () => { + expect(enhancements(document, window)).toMatchInlineSnapshot(` + [ + { + "for": "id=:rn: name=null className=prc-Textarea-TextArea-13q4j overtype-input", + "spot": { + "domain": "github.com", + "number": 523, + "slug": "diffplug/selfie", + "title": "TODO_TITLE", + "type": "GH_ISSUE_ADD_COMMENT", + "unique_key": "github.com:diffplug/selfie:523", + }, + "title": "TITLE_TODO", + "upperDecoration": + + + + # + 523 + + diffplug/selfie + + , + }, + ] + `) + }) + withCorpus('gh_new_issue').it('should create the correct spot object', async () => { expect(enhancements(document, window)).toMatchInlineSnapshot(` [ {