Skip to content

Commit 45ea112

Browse files
authored
Make sure popup receives up-to-date draft information (#71 fixes #68, some work remaining in #72)
2 parents 3ec2141 + 6113106 commit 45ea112

File tree

7 files changed

+139
-86
lines changed

7 files changed

+139
-86
lines changed

.claude/commands/precommit.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
1. run `pnpm precommit`
2+
2. if tests fail, run `pnpm test -u` might fix them
3+
3. fix other problems manually
4+
4. go back to step 1: `pnpm precommit`

CONTRIBUTING.md

Lines changed: 50 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,22 @@ This extension ***must never transmit any data outside the browser***.
1616

1717
## Developer quickstart
1818

19-
### Hotreload development
20-
2119
- `pnpm install`
22-
- `pnpm dev`
23-
- open [`chrome://extensions`](chrome://extensions)
24-
- toggle **Developer mode** (top-right)
25-
- click "Load unpacked" (far left)
26-
- `.output/chrome-mv3-dev`
27-
- if you can't find `.output`, it's probably hidden, `command+shift+period` will show it
28-
- click the puzzle icon next to the url bar, then pin the Gitcasso icon
29-
30-
### Testing and quality
31-
- `pnpm biome` - runs `biome check` (lint & formatting)
32-
- `pnpm biome:fix` - fixes most of what `biome check` finds
33-
- `pnpm typecheck` - typechecking
34-
- `pnpm test` - vitest
35-
- `pnpm test -u` - updates all snapshots
20+
- to update the popup:
21+
- `pnpm playground` gives react hotreload environment
22+
- to improve comment detection and metadata extraction:
23+
- `pnpm corpus` gives dev environment for gitcasso's behavior on specific pages and page states
24+
- to open a PR:
25+
- `pnpm precommit` fixes formatting and lints (biome), runs typechecking and tests
26+
- no worries if some intermediate commits don't pass
27+
- to run the entire end-to-end browser extension with [WXT](https://wxt.dev/) hotreload:
28+
- `pnpm dev`
29+
- open [`chrome://extensions`](chrome://extensions)
30+
- toggle **Developer mode** (top-right)
31+
- click "Load unpacked" (far left)
32+
- `.output/chrome-mv3-dev`
33+
- if you can't find `.output`, it's probably hidden, `command+shift+period` will show it
34+
- click the puzzle icon next to the url bar, then pin the Gitcasso icon
3635

3736
## How it works
3837

@@ -76,46 +75,52 @@ Those `Spot` values get bundled up with the `HTMLTextAreaElement` itself into an
7675

7776
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.
7877

79-
## Testing
78+
## Test corpus
8079

81-
- `pnpm playground` gives you a test environment where you can tinker with the popup with various test data, supports hot reload
82-
- `pnpm corpus` gives you recordings of various web pages which you can see with and without enhancement by the browser extension
80+
We maintain a corpus of test pages in two formats for testing the browser extension:
81+
- `html` created by the [SingleFile](https://chromewebstore.google.com/detail/singlefile/mpiodijhokgodhhofbcjdecpffjipkle) browser extension
82+
- allows snapshotting the DOM at an intermediate step in the user's experience
83+
- most interactivity will be broken
84+
- `har` recording of all network traffic of an initial pageload
85+
- limited to initial page load, but most interactivity will work
8386

84-
### Test Corpus
87+
This corpus of pages is used to power snapshot tests and interactive dev environments.
8588

86-
We maintain a corpus of test pages in two formats for testing the browser extension:
89+
### Viewing corpus
8790

88-
#### HAR Corpus (Automated)
91+
- **DISABLE GITCASSO IN YOUR BROWSER!!**
92+
- Run `pnpm corpus` to start the test server at http://localhost:3001
93+
- Select any corpus file to view in two modes:
94+
- **Clean**: Original unaltered page
95+
- **Gitcasso**: Page with extension injected for testing
96+
- it shows all data Gitcasso is extracting (if any)
97+
- click the rebuild button to test changes to the Gitcasso source
8998

90-
- For testing initial page loads and network requests
91-
- HAR recordings live in `tests/corpus/*.har`, complete recordings of the network requests of a single page load
92-
- You can add or change URLs in `tests/corpus/_corpus-index.ts`
93-
- **Recording new HAR files:**
94-
- `npx playwright codegen https://github.com/login --save-storage=playwright/.auth/gh.json` will store new auth tokens
95-
- login manually, then close the browser
96-
- ***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!***
97-
- `pnpm corpus:har:record` records new HAR files using those auth tokens (it needs args, run it with no args for docs)
98-
- DO NOT COMMIT AND PUSH NEW OR CHANGED HAR files!
99-
- we try to sanitize these (see `corpus-har-record.ts` for details) but there may be important PII in them
100-
- if you need new HAR files for something, let us know and we will generate them ourselves using a dummy account
101-
- 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.
99+
### Unit testing against corpus
100+
101+
- `gh-detection.test.ts` does snapshot testing on the data which gitcasso extracts
102+
- `gh-ui.test.ts` does snapshot testing on the popup table decoration for all data extracted from the snapshots
102103

103-
#### HTML Corpus (Manual)
104+
### Adding HTML to the corpus (Manual)
104105

105106
- For testing post-interaction states (e.g., expanded textareas, modal dialogs, dynamic content)
106-
- HTML snapshots live in `tests/corpus/*.html`, manually captured using SingleFile browser extension
107-
- All assets are inlined in a single HTML file by SingleFile
108-
- **Creating new HTML corpus files:**
107+
- HTML snapshots live in `tests/corpus/*.html`, manually captured using SingleFile
108+
- how-to
109109
1. Navigate to the desired page state (click buttons, expand textareas, etc.)
110110
2. Use SingleFile browser extension to save the complete page
111-
3. Save the `.html` file to `tests/corpus/html/` with a descriptive name
111+
3. Save the `.html` file to `tests/corpus/` with a descriptive slug
112112
4. Add an entry to `tests/corpus/_corpus-index.ts` with `type: 'html'` and a description of the captured state
113113
5. Feel free to contribute these if you want, but be mindful that they will be part of our immutable git history
114114

115-
#### Viewing Corpus Files
115+
### Adding HAR to the corpus (Automated)
116116

117-
- Run `pnpm corpus` to start the test server at http://localhost:3001
118-
- Select any corpus file to view in two modes:
119-
- **Clean**: Original page without extension
120-
- **Gitcasso**: Page with extension injected for testing
121-
- Both HAR and HTML corpus types are supported
117+
- For testing initial page loads and network requests
118+
- HAR recordings live in `tests/corpus/*.har`, complete recordings of the network requests of a single page load
119+
- how-to
120+
- update `tests/corpus/_corpus-index.ts` with a descriptive slug and URL which you want to add
121+
- `npx playwright codegen https://github.com/login --save-storage=playwright/.auth/gh.json` will store new auth tokens
122+
- login manually, then close the browser
123+
- ***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!***
124+
- `pnpm corpus:har:record {slug}` records new HAR files using those auth tokens (it needs args, run it with no args for docs)
125+
- **CONTRIBUTING THESE IS RISKY!**
126+
- we try to sanitize these (see `corpus-har-record.ts` for details) but there may be important PII in them, which is why we only use a test account

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"build": "pnpm run build:overtype && wxt build",
5656
"build:dev": "pnpm run build:overtype && wxt build --mode development",
5757
"build:firefox": "wxt build -b firefox",
58-
"precommit": "npm run biome:fix && npm run typecheck && npm run test",
58+
"precommit": "pnpm biome:fix && pnpm typecheck && pnpm test",
5959
"typecheck": "tsc --noEmit",
6060
"dev": "wxt",
6161
"dev:firefox": "wxt -b firefox",

src/entrypoints/background.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CommentEvent, CommentSpot } from '@/lib/enhancer'
1+
import type { CommentEvent, CommentEventType, CommentSpot } from '@/lib/enhancer'
22
import { type DraftStats, statsFor } from '@/lib/enhancers/draft-stats'
33
import { logger } from '@/lib/logger'
44
import type { GetTableRowsResponse, ToBackgroundMessage } from '@/lib/messages'
@@ -38,12 +38,14 @@ export const openSpots = new Map<string, CommentStorage>()
3838

3939
export function handleCommentEvent(message: CommentEvent, sender: any): boolean {
4040
logger.debug('received comment event', message)
41-
if (
42-
(message.type === 'ENHANCED' || message.type === 'DESTROYED') &&
43-
sender.tab?.id &&
44-
sender.tab?.windowId
45-
) {
46-
if (message.type === 'ENHANCED') {
41+
42+
// Only process events with valid tab information
43+
if (!sender.tab?.id || !sender.tab?.windowId) {
44+
return CLOSE_MESSAGE_PORT
45+
}
46+
47+
switch (message.type) {
48+
case 'ENHANCED': {
4749
const commentState: CommentStorage = {
4850
drafts: [[Date.now(), message.draft || '']],
4951
sentOn: null,
@@ -55,12 +57,27 @@ export function handleCommentEvent(message: CommentEvent, sender: any): boolean
5557
trashedOn: null,
5658
}
5759
openSpots.set(message.spot.unique_key, commentState)
58-
} else if (message.type === 'DESTROYED') {
60+
break
61+
}
62+
case 'DESTROYED': {
5963
openSpots.delete(message.spot.unique_key)
60-
} else {
61-
throw new Error(`Unhandled comment event type: ${message.type}`)
64+
break
65+
}
66+
case 'LOST_FOCUS': {
67+
// Update the draft content for existing comment state
68+
const existingState = openSpots.get(message.spot.unique_key)
69+
if (existingState) {
70+
existingState.drafts.push([Date.now(), message.draft || ''])
71+
}
72+
break
73+
}
74+
default: {
75+
// TypeScript exhaustiveness check - will error if we miss any CommentEventType
76+
const exhaustiveCheck: never = message.type satisfies CommentEventType
77+
throw new Error(`Unhandled comment event type: ${exhaustiveCheck}`)
6278
}
6379
}
80+
6481
return CLOSE_MESSAGE_PORT
6582
}
6683

src/entrypoints/content.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CommentEvent, CommentSpot, StrippedLocation } from '../lib/enhancer'
1+
import type { CommentEvent, StrippedLocation } from '../lib/enhancer'
22
import { logger } from '../lib/logger'
33
import { EnhancerRegistry, TextareaRegistry } from '../lib/registries'
44

@@ -20,20 +20,13 @@ function detectLocation(): StrippedLocation {
2020
return result
2121
}
2222

23-
function sendEventToBackground(type: 'ENHANCED' | 'DESTROYED', spot: CommentSpot): void {
24-
const message: CommentEvent = {
25-
spot,
26-
type,
27-
}
23+
function sendEventToBackground(message: CommentEvent): void {
2824
browser.runtime.sendMessage(message).catch((error) => {
29-
logger.debug('Failed to send event to background:', error)
25+
logger.error('Failed to send event to background:', error)
3026
})
3127
}
3228

33-
enhancedTextareas.setEventHandlers(
34-
(spot) => sendEventToBackground('ENHANCED', spot),
35-
(spot) => sendEventToBackground('DESTROYED', spot),
36-
)
29+
enhancedTextareas.setCommentEventSender(sendEventToBackground)
3730

3831
export default defineContentScript({
3932
main() {
@@ -47,6 +40,14 @@ export default defineContentScript({
4740
childList: true,
4841
subtree: true,
4942
})
43+
44+
// Listen for tab visibility changes to capture draft content when switching tabs
45+
document.addEventListener('visibilitychange', () => {
46+
if (document.hidden) {
47+
enhancedTextareas.tabLostFocus()
48+
}
49+
})
50+
5051
logger.debug('Extension loaded with', enhancers.getEnhancerCount(), 'handlers')
5152
},
5253
matches: ['<all_urls>'],

src/lib/messages.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { CommentTableRow } from '@/entrypoints/background'
2-
import type { CommentEvent } from './enhancer'
2+
import type { CommentEvent, CommentEventType } from './enhancer'
33

44
// Message handler response types
55
export const CLOSE_MESSAGE_PORT = false as const // No response will be sent
@@ -29,12 +29,24 @@ export interface GetTableRowsResponse {
2929
rows: CommentTableRow[]
3030
}
3131

32+
// Exhaustive list of valid comment event types - TypeScript will error if CommentEventType changes
33+
const COMMENT_EVENT_TYPES = {
34+
DESTROYED: true,
35+
ENHANCED: true,
36+
LOST_FOCUS: true,
37+
} as const satisfies Record<CommentEventType, true>
38+
39+
// Helper function to check if a string is a valid CommentEventType
40+
function isValidCommentEventType(type: string): type is CommentEventType {
41+
return type in COMMENT_EVENT_TYPES
42+
}
43+
3244
// Type guard functions
3345
export function isContentToBackgroundMessage(message: any): message is ContentToBackgroundMessage {
3446
return (
3547
message &&
3648
typeof message.type === 'string' &&
37-
(message.type === 'ENHANCED' || message.type === 'DESTROYED') &&
49+
isValidCommentEventType(message.type) &&
3850
message.spot
3951
)
4052
}

src/lib/registries.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import type { OverTypeInstance } from 'overtype'
22
import OverType from 'overtype'
3-
import type { CommentEnhancer, CommentSpot, StrippedLocation } from './enhancer'
3+
import type {
4+
CommentEnhancer,
5+
CommentEvent,
6+
CommentEventType,
7+
CommentSpot,
8+
StrippedLocation,
9+
} from './enhancer'
410
import { CommentEnhancerMissing } from './enhancers/CommentEnhancerMissing'
511
import { GitHubEditEnhancer } from './enhancers/github/GitHubEditEnhancer'
612
import { GitHubIssueAppendEnhancer } from './enhancers/github/GitHubIssueAppendEnhancer'
@@ -101,26 +107,32 @@ export class EnhancerRegistry {
101107

102108
export class TextareaRegistry {
103109
private textareas = new Map<HTMLTextAreaElement, EnhancedTextarea>()
104-
private onEnhanced?: (spot: CommentSpot) => void
105-
private onDestroyed?: (spot: CommentSpot) => void
110+
private eventSender: (event: CommentEvent) => void = () => {}
106111

107-
setEventHandlers(
108-
onEnhanced: (spot: CommentSpot) => void,
109-
onDestroyed: (spot: CommentSpot) => void,
110-
): void {
111-
this.onEnhanced = onEnhanced
112-
this.onDestroyed = onDestroyed
112+
setCommentEventSender(sendEvent: (event: CommentEvent) => void): void {
113+
this.eventSender = sendEvent
113114
}
114115

115-
register<T extends CommentSpot>(textareaInfo: EnhancedTextarea<T>): void {
116-
this.textareas.set(textareaInfo.textarea, textareaInfo)
117-
this.onEnhanced?.(textareaInfo.spot)
116+
private sendEvent(eventType: CommentEventType, enhanced: EnhancedTextarea): void {
117+
this.eventSender({
118+
draft: enhanced.textarea.value,
119+
spot: enhanced.spot,
120+
type: eventType,
121+
})
122+
}
123+
124+
register<T extends CommentSpot>(enhanced: EnhancedTextarea<T>): void {
125+
this.textareas.set(enhanced.textarea, enhanced)
126+
enhanced.textarea.addEventListener('blur', () => {
127+
this.sendEvent('LOST_FOCUS', enhanced)
128+
})
129+
this.sendEvent('ENHANCED', enhanced)
118130
}
119131

120132
unregisterDueToModification(textarea: HTMLTextAreaElement): void {
121-
const textareaInfo = this.textareas.get(textarea)
122-
if (textareaInfo) {
123-
this.onDestroyed?.(textareaInfo.spot)
133+
const enhanced = this.textareas.get(textarea)
134+
if (enhanced) {
135+
this.sendEvent('DESTROYED', enhanced)
124136
this.textareas.delete(textarea)
125137
}
126138
}
@@ -129,7 +141,9 @@ export class TextareaRegistry {
129141
return this.textareas.get(textarea)
130142
}
131143

132-
getAllEnhanced(): EnhancedTextarea[] {
133-
return Array.from(this.textareas.values())
144+
tabLostFocus(): void {
145+
for (const enhanced of this.textareas.values()) {
146+
this.sendEvent('LOST_FOCUS', enhanced)
147+
}
134148
}
135149
}

0 commit comments

Comments
 (0)