diff --git a/.github/workflows/browser-extension.yml b/.github/workflows/browser-extension.yml new file mode 100644 index 0000000..a547efa --- /dev/null +++ b/.github/workflows/browser-extension.yml @@ -0,0 +1,26 @@ +on: + pull_request: + push: + branches: [main, release] + workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + lint-test-and-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version-file: '.nvmrc' + cache: 'npm' + cache-dependency-path: browser-extension/package-lock.json + - run: npm ci + working-directory: browser-extension + - run: npm run biome + working-directory: browser-extension + - run: npm test + working-directory: browser-extension + - run: npm run compile + working-directory: browser-extension \ No newline at end of file diff --git a/README.md b/README.md index f46d8ba..3204ae5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Gitcasso -*Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly places).* +*Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly websites).* - "Syntax highlighting is the lie that enables us to see the truth." - "The meaning of life is to find your lost comment drafts. The purpose of life is to post them." @@ -12,6 +12,6 @@ TODO: screenshot of comment draft storage and restoration If there's something you'd like to add or fix, see [CONTRIBUTING.md](CONTRIBUTING.md). Special thanks to: -- [overtype](https://github.com/panphora/overtype) for the trick which makes syntax highlighting possible -- [shiki](https://github.com/shikijs/shiki) for the broad library of syntax highlighters +- [overtype](https://overtype.dev/) for doing `textarea` syntax highlighting of `md` +- [highlight.js](https://highlightjs.org/) for the broad library of syntax highlighters - [Yukai Huang](https://github.com/Yukaii) for [the PRs](https://github.com/panphora/overtype/issues?q=is%3Apr+author%3AYukaii) which made the two work together \ No newline at end of file diff --git a/browser-extension/README.md b/browser-extension/README.md index 8c1dab6..24c46df 100644 --- a/browser-extension/README.md +++ b/browser-extension/README.md @@ -17,7 +17,7 @@ - `npm run biome` - runs `biome check` (lint & formatting) - `npm run biome:fix` - fixes most of what `biome check` finds - `npm run compile` - typechecking -- `npm run test` - vitest +- `npm test` - vitest ### Deployment - `npm run build` - build for mv3 for most browsers @@ -27,10 +27,18 @@ This is a [WXT](https://wxt.dev/)-based browser extension that -- finds `textarea` components and decorates them with [overtype](https://overtype.dev/) and [shiki](https://github.com/shikijs/shiki). +- finds `textarea` components and decorates them with [overtype](https://overtype.dev/) and [highlight.js](https://highlightjs.org/) - stores unposted comment drafts, and makes them easy to find via the extension popup ### Entry points -- src/entrypoints/content.ts - injected into every webpage -- src/entrypoints/popup - html/css/ts which opens when the extension's button gets clicked +- `src/entrypoints/content.ts` - injected into every webpage +- `src/entrypoints/popup` - html/css/ts which opens when the extension's button gets clicked + +### Architecture + +Every time a `textarea` shows up on a page, on initial load or later on, it gets passed to a list of `CommentEnhancer`s. Each one gets a turn to say "I can enhance this box!". They show that they can enhance it by returning a [`CommentSpot`, `Overtype`]. + +Those values get bundled up with the `HTMLTextAreaElement` itself into an `EnhancedTextarea`, which gets added to the `TextareaRegistry`. At some interval, draft edits will get saved by the browser extension (TODO). + +When the `textarea` gets removed from the page, the `TextareaRegistry` is notified so that the `CommentSpot` can be marked as abandoned or submitted as appropriate (TODO). diff --git a/browser-extension/biome.json b/browser-extension/biome.json index 9f5a110..5dd9019 100644 --- a/browser-extension/biome.json +++ b/browser-extension/biome.json @@ -10,7 +10,7 @@ }, "files": { "ignoreUnknown": false, - "includes": [".*", "src/**", "tests/**"] + "includes": [".*", "src/**", "tests/**", "!src/overtype", "!src/playgrounds"] }, "formatter": { "enabled": true, @@ -41,7 +41,7 @@ "linter": { "rules": { "complexity": { - "noExcessiveCognitiveComplexity": "warn" + "noExcessiveCognitiveComplexity": "off" }, "correctness": { "noUnusedVariables": "error", @@ -65,7 +65,7 @@ "allow": ["assert", "error", "info", "warn"] } }, - "noExplicitAny": "error", + "noExplicitAny": "off", "noVar": "error" } } diff --git a/browser-extension/package-lock.json b/browser-extension/package-lock.json index b58c2d4..5bda396 100644 --- a/browser-extension/package-lock.json +++ b/browser-extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "gitcasso", - "version": "1.0.0", + "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gitcasso", - "version": "1.0.0", + "version": "0.0.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/browser-extension/package.json b/browser-extension/package.json index b802782..389c572 100644 --- a/browser-extension/package.json +++ b/browser-extension/package.json @@ -6,7 +6,7 @@ "overtype": "^1.2.3", "webextension-polyfill": "^0.12.0" }, - "description": "Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly places).", + "description": "Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly websites).", "devDependencies": { "@biomejs/biome": "^2.1.2", "@testing-library/jest-dom": "^6.6.4", @@ -43,5 +43,5 @@ "zip:firefox": "wxt zip -b firefox" }, "type": "module", - "version": "1.0.0" + "version": "0.0.1" } diff --git a/browser-extension/src/entrypoints/content.ts b/browser-extension/src/entrypoints/content.ts index 95d2321..1610d92 100644 --- a/browser-extension/src/entrypoints/content.ts +++ b/browser-extension/src/entrypoints/content.ts @@ -1,53 +1,103 @@ -import hljs from "highlight.js"; -import OverType from "../overtype/overtype"; +import { CONFIG } from '../lib/config' +import { logger } from '../lib/logger' +import { EnhancerRegistry, TextareaRegistry } from '../lib/registries' +import { githubPrNewCommentContentScript } from '../playgrounds/github-playground' + +const enhancers = new EnhancerRegistry() +const enhancedTextareas = new TextareaRegistry() export default defineContentScript({ main() { - if (window.location.hostname !== "github.com") { - return; + if (CONFIG.MODE === 'PLAYGROUNDS_PR') { + githubPrNewCommentContentScript() + return } - OverType.setCodeHighlighter(hljsHighlighter); - const ghCommentBox = document.getElementById( - "new_comment_field" - ) as HTMLTextAreaElement | null; - if (ghCommentBox) { - const overtypeContainer = modifyDOM(ghCommentBox); - new OverType(overtypeContainer, { - placeholder: "Add your comment here...", - autoResize: true, - minHeight: "102px", - padding: "var(--base-size-8)", - }); + const textAreasOnPageLoad = document.querySelectorAll(`textarea`) + for (const textarea of textAreasOnPageLoad) { + enhanceMaybe(textarea) } + const observer = new MutationObserver(handleMutations) + observer.observe(document.body, { + childList: true, + subtree: true, + }) + logger.debug('Extension loaded with', enhancers.getEnhancerCount, 'handlers') }, - matches: [""], - runAt: "document_end", -}); - -function modifyDOM(overtypeInput: HTMLTextAreaElement): HTMLElement { - overtypeInput.classList.add("overtype-input"); - const overtypePreview = document.createElement("div"); - overtypePreview.classList.add("overtype-preview"); - overtypeInput.insertAdjacentElement("afterend", overtypePreview); - const overtypeWrapper = overtypeInput.parentElement!.closest("div")!; - overtypeWrapper.classList.add("overtype-wrapper"); - overtypeInput.placeholder = "Add your comment here..."; - const overtypeContainer = overtypeWrapper.parentElement!.closest("div")!; - overtypeContainer.classList.add("overtype-container"); - return overtypeContainer.parentElement!.closest("div")!; -} + matches: [''], + runAt: 'document_end', +}) + +function handleMutations(mutations: MutationRecord[]): void { + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + if (node.nodeType === Node.ELEMENT_NODE) { + const element = node as Element + if (element.tagName === 'TEXTAREA') { + enhanceMaybe(element as HTMLTextAreaElement) + } else { + // Also check for textareas within added subtrees + const textareas = element.querySelectorAll?.('textarea') + if (textareas) { + for (const textarea of textareas) { + enhanceMaybe(textarea) + } + } + } + } + } -function hljsHighlighter(code: string, language: string) { - try { - if (language && hljs.getLanguage(language)) { - const result = hljs.highlight(code, { language }); - return result.value; - } else { - const result = hljs.highlightAuto(code); - return result.value; + for (const node of mutation.removedNodes) { + if (node.nodeType === Node.ELEMENT_NODE) { + const element = node as Element + if (element.tagName === 'TEXTAREA') { + enhancedTextareas.unregisterDueToModification(element as HTMLTextAreaElement) + } else { + // Also check for textareas within removed subtrees + const textareas = element.querySelectorAll?.('textarea') + if (textareas) { + for (const textarea of textareas) { + enhancedTextareas.unregisterDueToModification(textarea) + } + } + } + } } - } catch (error) { - console.warn("highlight.js highlighting failed:", error); - return code; + } +} + +function enhanceMaybe(textarea: HTMLTextAreaElement) { + if (enhancedTextareas.get(textarea)) { + logger.debug('textarea already registered {}', textarea) + return + } + + logger.debug('activating textarea {}', textarea) + injectStyles() + + const enhancedTextarea = enhancers.tryToEnhance(textarea) + if (enhancedTextarea) { + logger.debug( + 'Identified textarea:', + enhancedTextarea.spot.type, + enhancedTextarea.spot.unique_key, + ) + enhancedTextareas.register(enhancedTextarea) + } else { + logger.debug('No handler found for textarea') + } +} + +const STYLES = ` +.${CONFIG.ADDED_OVERTYPE_CLASS} { + background: cyan !important; +} +` + +function injectStyles(): void { + if (!document.getElementById('gitcasso-styles')) { + const style = document.createElement('style') + style.textContent = STYLES + style.id = 'gitcasso-styles' + document.head.appendChild(style) } } diff --git a/browser-extension/src/entrypoints/content/config.ts b/browser-extension/src/entrypoints/content/config.ts deleted file mode 100644 index c4f0a86..0000000 --- a/browser-extension/src/entrypoints/content/config.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Configuration constants for the extension -export const CONFIG = { - ADDED_OVERTYPE_CLASS: 'gitcasso-overtype', - // Debug settings - DEBUG: true, // Set to true to enable debug logging - EXTENSION_NAME: 'gitcasso', - INITIAL_SCAN_DELAY_MS: 100, - MUTATION_OBSERVER_DELAY_MS: 100, -} as const diff --git a/browser-extension/src/entrypoints/content/styles.ts b/browser-extension/src/entrypoints/content/styles.ts deleted file mode 100644 index 6d27449..0000000 --- a/browser-extension/src/entrypoints/content/styles.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { CONFIG } from './config' - -const STYLES = ` -.${CONFIG.ADDED_OVERTYPE_CLASS} { - background: cyan !important; -} -` - -export function injectStyles(): void { - if (!document.getElementById('gitcasso-styles')) { - const style = document.createElement('style') - style.textContent = STYLES - style.id = 'gitcasso-styles' - document.head.appendChild(style) - } -} diff --git a/browser-extension/src/entrypoints/popup/index.html b/browser-extension/src/entrypoints/popup/index.html index 650ae14..a9c17b6 100644 --- a/browser-extension/src/entrypoints/popup/index.html +++ b/browser-extension/src/entrypoints/popup/index.html @@ -9,7 +9,7 @@
-
Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly places).
+
Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly websites).

Loading drafts from local storage...

diff --git a/browser-extension/src/lib/config.ts b/browser-extension/src/lib/config.ts new file mode 100644 index 0000000..1f5e748 --- /dev/null +++ b/browser-extension/src/lib/config.ts @@ -0,0 +1,10 @@ +const MODES = ['PROD', 'PLAYGROUNDS_PR'] as const + +export type ModeType = (typeof MODES)[number] + +export const CONFIG = { + ADDED_OVERTYPE_CLASS: 'gitcasso-overtype', + DEBUG: true, // enabled debug logging + EXTENSION_NAME: 'gitcasso', // decorates logs + MODE: 'PLAYGROUNDS_PR' satisfies ModeType, +} as const diff --git a/browser-extension/src/lib/enhancer.ts b/browser-extension/src/lib/enhancer.ts new file mode 100644 index 0000000..82ac2eb --- /dev/null +++ b/browser-extension/src/lib/enhancer.ts @@ -0,0 +1,26 @@ +import type { OverType } from '../overtype/mock-overtype' + +/** + * stores enough info about the location of a draft to: + * - display it in a table + * - reopen the draft in-context + */ +export interface CommentSpot { + unique_key: string + type: string +} + +/** wraps the textareas of a given platform with Gitcasso's enhancements */ +export interface CommentEnhancer { + /** guarantees to only return a type within this list */ + forSpotTypes(): string[] + /** + * whenever a new `textarea` is added to any webpage, this method is called. + * if we return non-null, then we become the handler for that text area. + */ + tryToEnhance(textarea: HTMLTextAreaElement): [OverType, Spot] | null + + tableIcon(spot: Spot): string + tableTitle(spot: Spot): string + buildUrl(spot: Spot): string +} diff --git a/browser-extension/src/lib/enhancers/github.ts b/browser-extension/src/lib/enhancers/github.ts new file mode 100644 index 0000000..a055263 --- /dev/null +++ b/browser-extension/src/lib/enhancers/github.ts @@ -0,0 +1,109 @@ +import { OverType } from '../../overtype/mock-overtype' +import type { CommentEnhancer, CommentSpot } from '../enhancer' + +const GITHUB_SPOT_TYPES = [ + 'GH_ISSUE_NEW', + 'GH_PR_NEW', + 'GH_ISSUE_ADD_COMMENT', + 'GH_PR_ADD_COMMENT', + /* TODO + 'GH_ISSUE_EDIT_COMMENT', + 'GH_PR_EDIT_COMMENT', + 'GH_PR_CODE_COMMENT', + */ +] as const + +export type GitHubSpotType = (typeof GITHUB_SPOT_TYPES)[number] + +export interface GitHubSpot extends CommentSpot { + type: GitHubSpotType // Override to narrow from string to specific union + domain: string + slug: string // owner/repo + number?: number | undefined // issue/PR number, undefined for new issues and PRs +} + +export class GitHubEnhancer implements CommentEnhancer { + forSpotTypes(): string[] { + return [...GITHUB_SPOT_TYPES] + } + + tryToEnhance(textarea: HTMLTextAreaElement): [OverType, GitHubSpot] | null { + // Only handle GitHub domains + if (!window.location.hostname.includes('github')) { + return null + } + + const pathname = window.location.pathname + + // Parse GitHub URL structure: /owner/repo/issues/123 or /owner/repo/pull/456 + const match = pathname.match(/^\/([^/]+)\/([^/]+)(?:\/(issues|pull)\/(\d+))?/) + if (!match) return null + + const [, owner, repo, urlType, numberStr] = match + const slug = `${owner}/${repo}` + const number = numberStr ? parseInt(numberStr, 10) : undefined + + // Determine comment type + let type: GitHubSpotType + + if (pathname.includes('/issues/new')) { + type = 'GH_ISSUE_NEW' + } else if (pathname.includes('/compare/') || pathname.endsWith('/compare')) { + type = 'GH_PR_NEW' + } else if (urlType && number) { + if (urlType === 'issues') { + type = 'GH_ISSUE_ADD_COMMENT' + } else { + type = 'GH_PR_ADD_COMMENT' + } + } else { + return null + } + + // Generate unique key based on context + let unique_key = `github:${slug}` + if (number) { + unique_key += `:${urlType}:${number}` + } else { + unique_key += ':new' + } + + const spot: GitHubSpot = { + domain: window.location.hostname, + number, + slug, + type, + unique_key, + } + const overtype = new OverType(textarea) + return [overtype, spot] + } + + tableTitle(spot: GitHubSpot): string { + const { slug, number } = spot + if (number) { + return `Comment on ${slug} #${number}` + } + return `New ${window.location.pathname.includes('/issues/') ? 'issue' : 'PR'} in ${slug}` + } + + tableIcon(spot: GitHubSpot): string { + switch (spot.type) { + case 'GH_ISSUE_NEW': + case 'GH_ISSUE_ADD_COMMENT': + return '🐛' // Issue icon + case 'GH_PR_NEW': + case 'GH_PR_ADD_COMMENT': + return '🔄' // PR icon + } + } + + buildUrl(spot: GitHubSpot): string { + const baseUrl = `https://${spot.domain}/${spot.slug}` + if (spot.number) { + const type = spot.type.indexOf('ISSUE') ? 'issues' : 'pull' + return `${baseUrl}/${type}/${spot.number}` + } + return baseUrl + } +} diff --git a/browser-extension/src/entrypoints/content/logger.ts b/browser-extension/src/lib/logger.ts similarity index 100% rename from browser-extension/src/entrypoints/content/logger.ts rename to browser-extension/src/lib/logger.ts diff --git a/browser-extension/src/lib/registries.ts b/browser-extension/src/lib/registries.ts new file mode 100644 index 0000000..467043a --- /dev/null +++ b/browser-extension/src/lib/registries.ts @@ -0,0 +1,62 @@ +import type { OverType } from '../overtype/mock-overtype' +import type { CommentEnhancer, CommentSpot } from './enhancer' +import { GitHubEnhancer } from './enhancers/github' + +export interface EnhancedTextarea { + textarea: HTMLTextAreaElement + spot: T + handler: CommentEnhancer + overtype: OverType +} + +export class EnhancerRegistry { + private enhancers = new Set>() + + constructor() { + // Register all available handlers + this.register(new GitHubEnhancer()) + } + + private register(handler: CommentEnhancer): void { + this.enhancers.add(handler) + } + + tryToEnhance(textarea: HTMLTextAreaElement): EnhancedTextarea | null { + for (const handler of this.enhancers) { + try { + const result = handler.tryToEnhance(textarea) + if (result) { + const [overtype, spot] = result + return { handler, overtype, spot, textarea } + } + } catch (error) { + console.warn('Handler failed to identify textarea:', error) + } + } + return null + } + + getEnhancerCount(): number { + return this.enhancers.size + } +} + +export class TextareaRegistry { + private textareas = new Map>() + + register(textareaInfo: EnhancedTextarea): void { + this.textareas.set(textareaInfo.textarea, textareaInfo) + // TODO: register as a draft in progress with the global list + } + + unregisterDueToModification(textarea: HTMLTextAreaElement): void { + if (this.textareas.has(textarea)) { + // TODO: register as abandoned or maybe submitted with the global list + this.textareas.delete(textarea) + } + } + + get(textarea: HTMLTextAreaElement): EnhancedTextarea | undefined { + return this.textareas.get(textarea) + } +} diff --git a/browser-extension/src/overtype/mock-overtype.ts b/browser-extension/src/overtype/mock-overtype.ts new file mode 100644 index 0000000..fec648c --- /dev/null +++ b/browser-extension/src/overtype/mock-overtype.ts @@ -0,0 +1,44 @@ +/** + * Mock implementation of Overtype for development + * This wraps a textarea and provides a minimal interface + */ +export class OverType { + public element: HTMLTextAreaElement + public instanceId: number + public initialized: boolean = true + + private static instanceCount = 0 + + constructor(target: HTMLTextAreaElement) { + this.element = target + this.instanceId = ++OverType.instanceCount + + // Store reference on the element + ;(target as any).overTypeInstance = this + + // Apply basic styling or enhancement + this.enhance() + } + + private enhance(): void { + // Mock enhancement - could add basic styling, event handlers, etc. + this.element.style.fontFamily = + 'Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace' + this.element.style.fontSize = '14px' + this.element.style.lineHeight = '1.5' + } + + getValue(): string { + return this.element.value + } + + setValue(value: string): void { + this.element.value = value + } + + destroy(): void { + // Clean up any enhancements + delete (this.element as any).overTypeInstance + this.initialized = false + } +} diff --git a/browser-extension/src/playgrounds/github-playground.ts b/browser-extension/src/playgrounds/github-playground.ts new file mode 100644 index 0000000..dae988f --- /dev/null +++ b/browser-extension/src/playgrounds/github-playground.ts @@ -0,0 +1,49 @@ +import hljs from "highlight.js"; +import OverType from "../overtype/overtype"; + +export function githubPrNewCommentContentScript() { + if (window.location.hostname !== "github.com") { + return; + } + OverType.setCodeHighlighter(hljsHighlighter); + const ghCommentBox = document.getElementById( + "new_comment_field" + ) as HTMLTextAreaElement | null; + if (ghCommentBox) { + const overtypeContainer = modifyDOM(ghCommentBox); + new OverType(overtypeContainer, { + placeholder: "Add your comment here...", + autoResize: true, + minHeight: "102px", + padding: "var(--base-size-8)", + }); + } +} + +function modifyDOM(overtypeInput: HTMLTextAreaElement): HTMLElement { + overtypeInput.classList.add("overtype-input"); + const overtypePreview = document.createElement("div"); + overtypePreview.classList.add("overtype-preview"); + overtypeInput.insertAdjacentElement("afterend", overtypePreview); + const overtypeWrapper = overtypeInput.parentElement!.closest("div")!; + overtypeWrapper.classList.add("overtype-wrapper"); + overtypeInput.placeholder = "Add your comment here..."; + const overtypeContainer = overtypeWrapper.parentElement!.closest("div")!; + overtypeContainer.classList.add("overtype-container"); + return overtypeContainer.parentElement!.closest("div")!; +} + +function hljsHighlighter(code: string, language: string) { + try { + if (language && hljs.getLanguage(language)) { + const result = hljs.highlight(code, { language }); + return result.value; + } else { + const result = hljs.highlightAuto(code); + return result.value; + } + } catch (error) { + console.warn("highlight.js highlighting failed:", error); + return code; + } +} diff --git a/browser-extension/tests/lib/enhancers/__snapshots__/github.test.ts.snap b/browser-extension/tests/lib/enhancers/__snapshots__/github.test.ts.snap new file mode 100644 index 0000000..fa2f132 --- /dev/null +++ b/browser-extension/tests/lib/enhancers/__snapshots__/github.test.ts.snap @@ -0,0 +1,11 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`GitHubHandler > should create correct GitHubContext spot for PR comment > github-pr-517-spot 1`] = ` +{ + "domain": "github.com", + "number": 517, + "slug": "diffplug/selfie", + "type": "GH_PR_ADD_COMMENT", + "unique_key": "github:diffplug/selfie:pull:517", +} +`; diff --git a/browser-extension/tests/lib/enhancers/github.test.ts b/browser-extension/tests/lib/enhancers/github.test.ts new file mode 100644 index 0000000..2ec59ef --- /dev/null +++ b/browser-extension/tests/lib/enhancers/github.test.ts @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { EnhancerRegistry, TextareaRegistry } from '../../../src/lib/registries' + +// Mock WXT's defineContentScript global +vi.stubGlobal('defineContentScript', vi.fn()) + +describe('GitHubHandler', () => { + let enhancers: EnhancerRegistry + let enhancedTextareas: TextareaRegistry + let mockTextarea: HTMLTextAreaElement + + beforeEach(() => { + // Reset DOM and registries for each test + document.body.innerHTML = '' + enhancers = new EnhancerRegistry() + enhancedTextareas = new TextareaRegistry() + + // Mock window.location for GitHub PR page + Object.defineProperty(window, 'location', { + value: { + hostname: 'github.com', + href: 'https://github.com/diffplug/selfie/pull/517', + pathname: '/diffplug/selfie/pull/517', + }, + writable: true, + }) + + // Create a mock textarea element that mimics GitHub's PR comment box + mockTextarea = document.createElement('textarea') + mockTextarea.name = 'comment[body]' + mockTextarea.placeholder = 'Leave a comment' + mockTextarea.className = 'form-control markdown-body' + + // Add it to a typical GitHub comment form structure + const commentForm = document.createElement('div') + commentForm.className = 'js-new-comment-form' + commentForm.appendChild(mockTextarea) + document.body.appendChild(commentForm) + }) + + it('should identify GitHub PR textarea and register it in TextareaRegistry', () => { + // Simulate the content script's enhanceMaybe function + const enhancedTextarea = enhancers.tryToEnhance(mockTextarea) + + expect(enhancedTextarea).toBeTruthy() + expect(enhancedTextarea?.textarea).toBe(mockTextarea) + expect(enhancedTextarea?.spot.type).toBe('GH_PR_ADD_COMMENT') + + // Register the enhanced textarea + if (enhancedTextarea) { + enhancedTextareas.register(enhancedTextarea) + } + + // Verify it's in the registry + const registeredTextarea = enhancedTextareas.get(mockTextarea) + expect(registeredTextarea).toBeTruthy() + expect(registeredTextarea?.textarea).toBe(mockTextarea) + }) + + it('should create correct GitHubContext spot for PR comment', () => { + const enhancedTextarea = enhancers.tryToEnhance(mockTextarea) + + expect(enhancedTextarea).toBeTruthy() + + // Snapshot test on the spot value + expect(enhancedTextarea?.spot).toMatchSnapshot('github-pr-517-spot') + + // Also verify specific expected values + expect(enhancedTextarea?.spot).toMatchObject({ + domain: 'github.com', + number: 517, + slug: 'diffplug/selfie', + type: 'GH_PR_ADD_COMMENT', + unique_key: 'github:diffplug/selfie:pull:517', + }) + }) + + it('should not enhance textarea on non-GitHub pages', () => { + // Change location to non-GitHub site + Object.defineProperty(window, 'location', { + value: { + hostname: 'example.com', + href: 'https://example.com/some/page', + pathname: '/some/page', + }, + writable: true, + }) + + const enhancedTextarea = enhancers.tryToEnhance(mockTextarea) + expect(enhancedTextarea).toBeNull() + }) +}) diff --git a/browser-extension/tests/setup.ts b/browser-extension/tests/setup.ts index 1171022..a9d0dd3 100644 --- a/browser-extension/tests/setup.ts +++ b/browser-extension/tests/setup.ts @@ -1,38 +1 @@ import '@testing-library/jest-dom/vitest' -import { vi } from 'vitest' - -// Mock browser APIs -interface GlobalWithBrowser { - browser: { - runtime: { - sendMessage: ReturnType - onMessage: { - addListener: ReturnType - } - } - } - chrome: GlobalWithBrowser['browser'] -} - -const globalWithBrowser = global as unknown as GlobalWithBrowser - -globalWithBrowser.browser = { - runtime: { - onMessage: { - addListener: vi.fn(), - }, - sendMessage: vi.fn(), - }, -} - -globalWithBrowser.chrome = globalWithBrowser.browser - -// Mock console methods to reduce noise in tests -global.console = { - ...console, - debug: vi.fn(), - error: vi.fn(), - info: vi.fn(), - log: vi.fn(), - warn: vi.fn(), -} diff --git a/browser-extension/vitest.config.ts b/browser-extension/vitest.config.ts index d247fc3..af7d365 100644 --- a/browser-extension/vitest.config.ts +++ b/browser-extension/vitest.config.ts @@ -1,14 +1,8 @@ -import path from 'node:path' import { defineConfig } from 'vitest/config' +import { WxtVitest } from 'wxt/testing' export default defineConfig({ - resolve: { - alias: { - '@': path.resolve(__dirname, './entrypoints'), - '@content': path.resolve(__dirname, './entrypoints/content'), - '@utils': path.resolve(__dirname, './entrypoints/content/utils'), - }, - }, + plugins: [WxtVitest()], test: { coverage: { exclude: [ diff --git a/browser-extension/wxt.config.ts b/browser-extension/wxt.config.ts index 0514d1a..edc2589 100644 --- a/browser-extension/wxt.config.ts +++ b/browser-extension/wxt.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from 'wxt' export default defineConfig({ manifest: { description: - 'Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly places).', + 'Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly websites).', host_permissions: ['https://*/*', 'http://*/*'], icons: { 16: '/icons/icon-16.png',