Skip to content

Commit 646de51

Browse files
Merge pull request #9 from richardkmichael/issues-page-scroll
2 parents 3bc0856 + 74d61f9 commit 646de51

2 files changed

Lines changed: 131 additions & 59 deletions

File tree

extension/assets/content.js

Lines changed: 71 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
7575
// Selector for page header actions (using prefix to avoid CSS module hash)
7676
const HEADER_ACTIONS_SELECTOR = '[data-component="PH_Actions"] [class*="HeaderMenu-module__menuActionsContainer"]';
7777

78+
// Selector for sticky header actions (appears when scrolling)
79+
const STICKY_HEADER_ACTIONS_SELECTOR = '[class*="HeaderMetadata-module__stickyContainer"] [class*="HeaderMenu-module__menuActionsContainer"]';
80+
7881
// Extension button marker
7982
const BOOKMARK_BUTTON_ATTR = 'data-extension-bookmark';
8083

@@ -122,8 +125,14 @@ if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
122125
button.setAttribute('aria-label', bookmarked ? 'Remove bookmark' : 'Bookmark issue');
123126
}
124127

128+
// Update all bookmark buttons on the page (main header and sticky header)
129+
function updateAllBookmarkButtons(bookmarked) {
130+
const buttons = document.querySelectorAll(`[${BOOKMARK_BUTTON_ATTR}]`);
131+
buttons.forEach(button => updateBookmarkButton(button, bookmarked));
132+
}
133+
125134
// Handle bookmark button click
126-
async function handleBookmarkClick(button) {
135+
async function handleBookmarkClick() {
127136
const issueData = getIssueData();
128137
if (!issueData) {
129138
return;
@@ -139,58 +148,36 @@ if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
139148
data: { id: issueData.id }
140149
});
141150
console.log('[GitHub Bookmarked Issues] Removed bookmark:', issueData.id);
142-
updateBookmarkButton(button, false);
151+
updateAllBookmarkButtons(false);
143152
} else {
144153
// Add bookmark
145154
await browser.runtime.sendMessage({
146155
type: 'BOOKMARK_ISSUE',
147156
data: issueData
148157
});
149158
console.log('[GitHub Bookmarked Issues] Added bookmark:', issueData.id);
150-
updateBookmarkButton(button, true);
159+
updateAllBookmarkButtons(true);
151160
}
152161
} catch (error) {
153162
console.error('[GitHub Bookmarked Issues] Failed to toggle bookmark:', error);
154163
showErrorNotification('Failed to update bookmark. Please try again.');
155164
}
156165
}
157166

158-
// Create and insert bookmark button
159-
async function insertBookmarkButton() {
160-
const timestamp = performance.now().toFixed(1);
161-
const issueData = getIssueData();
162-
if (!issueData) {
163-
console.log(`[GitHub Bookmarked Issues] [${timestamp}ms] Not on an issue page`);
164-
return;
165-
}
166-
167-
const actionsContainer = document.querySelector(HEADER_ACTIONS_SELECTOR);
168-
if (!actionsContainer) {
169-
console.log(`[GitHub Bookmarked Issues] [${timestamp}ms] Header actions not found, selector: ${HEADER_ACTIONS_SELECTOR}`);
170-
return;
171-
}
172-
173-
// Check if button already exists
174-
if (document.querySelector(`[${BOOKMARK_BUTTON_ATTR}]`)) {
175-
console.log(`[GitHub Bookmarked Issues] [${timestamp}ms] Bookmark button already exists`);
176-
return;
177-
}
178-
179-
console.log(`[GitHub Bookmarked Issues] [${timestamp}ms] Creating button, container children: ${actionsContainer.children.length}`);
180-
181-
// Create bookmark button
182-
const bookmarkButton = document.createElement('button');
183-
bookmarkButton.setAttribute('data-component', 'IconButton');
184-
bookmarkButton.setAttribute('type', 'button');
185-
bookmarkButton.className = 'prc-Button-ButtonBase-c50BI prc-Button-IconButton-szpyj';
186-
bookmarkButton.setAttribute('data-loading', 'false');
187-
bookmarkButton.setAttribute('data-no-visuals', 'true');
188-
bookmarkButton.setAttribute('data-size', 'medium');
189-
bookmarkButton.setAttribute('data-variant', 'invisible');
190-
bookmarkButton.setAttribute(BOOKMARK_BUTTON_ATTR, 'true');
167+
// Create a bookmark button element
168+
function createBookmarkButton(bookmarked) {
169+
const button = document.createElement('button');
170+
button.setAttribute('data-component', 'IconButton');
171+
button.setAttribute('type', 'button');
172+
button.className = 'prc-Button-ButtonBase-c50BI prc-Button-IconButton-szpyj';
173+
button.setAttribute('data-loading', 'false');
174+
button.setAttribute('data-no-visuals', 'true');
175+
button.setAttribute('data-size', 'medium');
176+
button.setAttribute('data-variant', 'invisible');
177+
button.setAttribute(BOOKMARK_BUTTON_ATTR, 'true');
191178

192179
// Add inline styles to match GitHub's native icon buttons
193-
bookmarkButton.style.cssText = `
180+
button.style.cssText = `
194181
border: none;
195182
background: transparent;
196183
padding: 0;
@@ -202,28 +189,57 @@ if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
202189
color: inherit;
203190
`;
204191

205-
// Check if already bookmarked and set initial state
206-
const bookmarked = await isBookmarked(issueData.id);
207-
updateBookmarkButton(bookmarkButton, bookmarked);
192+
updateBookmarkButton(button, bookmarked);
193+
button.addEventListener('click', () => handleBookmarkClick());
194+
195+
return button;
196+
}
197+
198+
// Insert bookmark button into a container if not already present
199+
function insertButtonIntoContainer(container, bookmarked, containerName) {
200+
// Check if this container already has a bookmark button
201+
if (container.querySelector(`[${BOOKMARK_BUTTON_ATTR}]`)) {
202+
return false;
203+
}
204+
205+
const button = createBookmarkButton(bookmarked);
206+
container.appendChild(button);
207+
208+
const timestamp = performance.now().toFixed(1);
209+
console.log(`[GitHub Bookmarked Issues] [${timestamp}ms] Bookmark button added to ${containerName}`);
210+
return true;
211+
}
212+
213+
// Create and insert bookmark buttons into all available headers
214+
async function insertBookmarkButton() {
215+
const timestamp = performance.now().toFixed(1);
216+
const issueData = getIssueData();
217+
if (!issueData) {
218+
console.log(`[GitHub Bookmarked Issues] [${timestamp}ms] Not on an issue page`);
219+
return;
220+
}
208221

209-
// Add click handler
210-
bookmarkButton.addEventListener('click', () => handleBookmarkClick(bookmarkButton));
222+
// Find both header containers
223+
const mainHeader = document.querySelector(HEADER_ACTIONS_SELECTOR);
224+
const stickyHeader = document.querySelector(STICKY_HEADER_ACTIONS_SELECTOR);
211225

212-
// Insert as last child in header actions
213-
actionsContainer.appendChild(bookmarkButton);
226+
if (!mainHeader && !stickyHeader) {
227+
console.log(`[GitHub Bookmarked Issues] [${timestamp}ms] No header actions found`);
228+
return;
229+
}
214230

215-
const insertTimestamp = performance.now().toFixed(1);
216-
console.log(`[GitHub Bookmarked Issues] [${insertTimestamp}ms] Bookmark button added to DOM, parent: ${actionsContainer.className}`);
231+
// Check if already bookmarked (only once for both buttons)
232+
const bookmarked = await isBookmarked(issueData.id);
217233

218-
// Debug: watch for button removal (helps diagnose timing issues)
219-
const removalObserver = new MutationObserver(() => {
220-
if (!document.querySelector(`[${BOOKMARK_BUTTON_ATTR}]`)) {
221-
const removeTimestamp = performance.now().toFixed(1);
222-
console.log(`[GitHub Bookmarked Issues] [${removeTimestamp}ms] Button was REMOVED from DOM`);
223-
removalObserver.disconnect();
224-
}
225-
});
226-
removalObserver.observe(actionsContainer.parentElement || document.body, { childList: true, subtree: true });
234+
// Insert into main header if available
235+
if (mainHeader) {
236+
insertButtonIntoContainer(mainHeader, bookmarked, 'main header');
237+
}
238+
239+
// Insert into sticky header if available
240+
if (stickyHeader) {
241+
insertButtonIntoContainer(stickyHeader, bookmarked, 'sticky header');
242+
}
227243
}
228244

229245
// Track current URL to detect navigation

tests/extension.spec.js

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,8 @@ test.describe('', () => {
167167

168168
await expect(page.locator(headerActionsSelector)).toBeVisible({ timeout: 15000 });
169169

170-
const bookmarkButton = page.locator(bookmarkSelector);
170+
// Use main header selector to avoid matching sticky header button
171+
const bookmarkButton = page.locator(`${headerActionsSelector} ${bookmarkSelector}`);
171172
await expect(bookmarkButton).toBeVisible({ timeout: 10000 });
172173

173174
// Click to add bookmark
@@ -189,6 +190,58 @@ test.describe('', () => {
189190
expect(Object.keys(bookmarks)).not.toContain(expectedKey);
190191
}).toPass({ timeout: 5000 });
191192
});
193+
194+
test('bookmark button in sticky header', async () => {
195+
const bookmarkSelector = '[data-extension-bookmark]';
196+
const headerActionsSelector = '[data-component="PH_Actions"]';
197+
const stickyHeaderSelector = '[class*="HeaderMetadata-module__stickyContainer"]';
198+
199+
await clearBookmarks(context);
200+
201+
// Use an issue with enough content to scroll
202+
await page.goto('https://github.com/microsoft/playwright/issues/11975');
203+
await page.waitForLoadState('domcontentloaded');
204+
205+
await expect(page.locator(headerActionsSelector)).toBeVisible({ timeout: 15000 });
206+
207+
// Verify bookmark button exists in main header
208+
const mainBookmarkButton = page.locator(`${headerActionsSelector} ${bookmarkSelector}`);
209+
await expect(mainBookmarkButton).toBeVisible({ timeout: 10000 });
210+
211+
// Scroll down to trigger sticky header
212+
await page.evaluate(() => window.scrollTo(0, 1500));
213+
await page.waitForTimeout(500);
214+
215+
// Verify sticky header appeared
216+
await expect(page.locator(stickyHeaderSelector)).toBeVisible({ timeout: 5000 });
217+
218+
// Verify bookmark button exists in sticky header
219+
const stickyBookmarkButton = page.locator(`${stickyHeaderSelector} ${bookmarkSelector}`);
220+
await expect(stickyBookmarkButton).toBeVisible({ timeout: 5000 });
221+
222+
// Click sticky header bookmark button to add bookmark
223+
await stickyBookmarkButton.click();
224+
225+
// Verify bookmark was added
226+
const expectedKey = 'microsoft/playwright/issues/11975';
227+
await expect(async () => {
228+
const bookmarks = await getBookmarks(context);
229+
expect(Object.keys(bookmarks)).toContain(expectedKey);
230+
}).toPass({ timeout: 5000 });
231+
232+
// Scroll back up and verify main header button is also in bookmarked state
233+
await page.evaluate(() => window.scrollTo(0, 0));
234+
await page.waitForTimeout(500);
235+
236+
// Click main header button to remove bookmark (verifies sync)
237+
await mainBookmarkButton.click();
238+
239+
// Verify bookmark was removed
240+
await expect(async () => {
241+
const bookmarks = await getBookmarks(context);
242+
expect(Object.keys(bookmarks)).not.toContain(expectedKey);
243+
}).toPass({ timeout: 5000 });
244+
});
192245
});
193246

194247
// Bookmark button navigation tests - ensure button appears regardless of navigation path
@@ -208,7 +261,8 @@ test.describe('', () => {
208261
// Wait for header actions container (needed for button insertion)
209262
await expect(page.locator(headerActionsSelector)).toBeVisible({ timeout: 15000 });
210263

211-
const bookmarkButton = page.locator(bookmarkSelector);
264+
// Use main header selector to avoid matching sticky header button
265+
const bookmarkButton = page.locator(`${headerActionsSelector} ${bookmarkSelector}`);
212266
await expect(bookmarkButton).toBeVisible({ timeout: 10000 });
213267
});
214268

@@ -225,7 +279,8 @@ test.describe('', () => {
225279
// Wait for issue page to load
226280
await expect(page.locator(headerActionsSelector)).toBeVisible({ timeout: 15000 });
227281

228-
const bookmarkButton = page.locator(bookmarkSelector);
282+
// Use main header selector to avoid matching sticky header button
283+
const bookmarkButton = page.locator(`${headerActionsSelector} ${bookmarkSelector}`);
229284
await expect(bookmarkButton).toBeVisible({ timeout: 10000 });
230285
});
231286

@@ -247,7 +302,8 @@ test.describe('', () => {
247302
// Wait for issue page to load
248303
await expect(page.locator(headerActionsSelector)).toBeVisible({ timeout: 15000 });
249304

250-
const bookmarkButton = page.locator(bookmarkSelector);
305+
// Use main header selector to avoid matching sticky header button
306+
const bookmarkButton = page.locator(`${headerActionsSelector} ${bookmarkSelector}`);
251307
await expect(bookmarkButton).toBeVisible({ timeout: 10000 });
252308
});
253309

0 commit comments

Comments
 (0)