Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 71 additions & 55 deletions extension/assets/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
// Selector for page header actions (using prefix to avoid CSS module hash)
const HEADER_ACTIONS_SELECTOR = '[data-component="PH_Actions"] [class*="HeaderMenu-module__menuActionsContainer"]';

// Selector for sticky header actions (appears when scrolling)
const STICKY_HEADER_ACTIONS_SELECTOR = '[class*="HeaderMetadata-module__stickyContainer"] [class*="HeaderMenu-module__menuActionsContainer"]';

// Extension button marker
const BOOKMARK_BUTTON_ATTR = 'data-extension-bookmark';

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

// Update all bookmark buttons on the page (main header and sticky header)
function updateAllBookmarkButtons(bookmarked) {
const buttons = document.querySelectorAll(`[${BOOKMARK_BUTTON_ATTR}]`);
buttons.forEach(button => updateBookmarkButton(button, bookmarked));
}

// Handle bookmark button click
async function handleBookmarkClick(button) {
async function handleBookmarkClick() {
const issueData = getIssueData();
if (!issueData) {
return;
Expand All @@ -139,58 +148,36 @@ if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
data: { id: issueData.id }
});
console.log('[GitHub Bookmarked Issues] Removed bookmark:', issueData.id);
updateBookmarkButton(button, false);
updateAllBookmarkButtons(false);
} else {
// Add bookmark
await browser.runtime.sendMessage({
type: 'BOOKMARK_ISSUE',
data: issueData
});
console.log('[GitHub Bookmarked Issues] Added bookmark:', issueData.id);
updateBookmarkButton(button, true);
updateAllBookmarkButtons(true);
}
} catch (error) {
console.error('[GitHub Bookmarked Issues] Failed to toggle bookmark:', error);
showErrorNotification('Failed to update bookmark. Please try again.');
}
}

// Create and insert bookmark button
async function insertBookmarkButton() {
const timestamp = performance.now().toFixed(1);
const issueData = getIssueData();
if (!issueData) {
console.log(`[GitHub Bookmarked Issues] [${timestamp}ms] Not on an issue page`);
return;
}

const actionsContainer = document.querySelector(HEADER_ACTIONS_SELECTOR);
if (!actionsContainer) {
console.log(`[GitHub Bookmarked Issues] [${timestamp}ms] Header actions not found, selector: ${HEADER_ACTIONS_SELECTOR}`);
return;
}

// Check if button already exists
if (document.querySelector(`[${BOOKMARK_BUTTON_ATTR}]`)) {
console.log(`[GitHub Bookmarked Issues] [${timestamp}ms] Bookmark button already exists`);
return;
}

console.log(`[GitHub Bookmarked Issues] [${timestamp}ms] Creating button, container children: ${actionsContainer.children.length}`);

// Create bookmark button
const bookmarkButton = document.createElement('button');
bookmarkButton.setAttribute('data-component', 'IconButton');
bookmarkButton.setAttribute('type', 'button');
bookmarkButton.className = 'prc-Button-ButtonBase-c50BI prc-Button-IconButton-szpyj';
bookmarkButton.setAttribute('data-loading', 'false');
bookmarkButton.setAttribute('data-no-visuals', 'true');
bookmarkButton.setAttribute('data-size', 'medium');
bookmarkButton.setAttribute('data-variant', 'invisible');
bookmarkButton.setAttribute(BOOKMARK_BUTTON_ATTR, 'true');
// Create a bookmark button element
function createBookmarkButton(bookmarked) {
const button = document.createElement('button');
button.setAttribute('data-component', 'IconButton');
button.setAttribute('type', 'button');
button.className = 'prc-Button-ButtonBase-c50BI prc-Button-IconButton-szpyj';
button.setAttribute('data-loading', 'false');
button.setAttribute('data-no-visuals', 'true');
button.setAttribute('data-size', 'medium');
button.setAttribute('data-variant', 'invisible');
button.setAttribute(BOOKMARK_BUTTON_ATTR, 'true');

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

// Check if already bookmarked and set initial state
const bookmarked = await isBookmarked(issueData.id);
updateBookmarkButton(bookmarkButton, bookmarked);
updateBookmarkButton(button, bookmarked);
button.addEventListener('click', () => handleBookmarkClick());

return button;
}

// Insert bookmark button into a container if not already present
function insertButtonIntoContainer(container, bookmarked, containerName) {
// Check if this container already has a bookmark button
if (container.querySelector(`[${BOOKMARK_BUTTON_ATTR}]`)) {
return false;
}

const button = createBookmarkButton(bookmarked);
container.appendChild(button);

const timestamp = performance.now().toFixed(1);
console.log(`[GitHub Bookmarked Issues] [${timestamp}ms] Bookmark button added to ${containerName}`);
return true;
}

// Create and insert bookmark buttons into all available headers
async function insertBookmarkButton() {
const timestamp = performance.now().toFixed(1);
const issueData = getIssueData();
if (!issueData) {
console.log(`[GitHub Bookmarked Issues] [${timestamp}ms] Not on an issue page`);
return;
}

// Add click handler
bookmarkButton.addEventListener('click', () => handleBookmarkClick(bookmarkButton));
// Find both header containers
const mainHeader = document.querySelector(HEADER_ACTIONS_SELECTOR);
const stickyHeader = document.querySelector(STICKY_HEADER_ACTIONS_SELECTOR);

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

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

// Debug: watch for button removal (helps diagnose timing issues)
const removalObserver = new MutationObserver(() => {
if (!document.querySelector(`[${BOOKMARK_BUTTON_ATTR}]`)) {
const removeTimestamp = performance.now().toFixed(1);
console.log(`[GitHub Bookmarked Issues] [${removeTimestamp}ms] Button was REMOVED from DOM`);
removalObserver.disconnect();
}
});
removalObserver.observe(actionsContainer.parentElement || document.body, { childList: true, subtree: true });
// Insert into main header if available
if (mainHeader) {
insertButtonIntoContainer(mainHeader, bookmarked, 'main header');
}

// Insert into sticky header if available
if (stickyHeader) {
insertButtonIntoContainer(stickyHeader, bookmarked, 'sticky header');
}
}

// Track current URL to detect navigation
Expand Down
64 changes: 60 additions & 4 deletions tests/extension.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ test.describe('', () => {

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

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

// Click to add bookmark
Expand All @@ -189,6 +190,58 @@ test.describe('', () => {
expect(Object.keys(bookmarks)).not.toContain(expectedKey);
}).toPass({ timeout: 5000 });
});

test('bookmark button in sticky header', async () => {
const bookmarkSelector = '[data-extension-bookmark]';
const headerActionsSelector = '[data-component="PH_Actions"]';
const stickyHeaderSelector = '[class*="HeaderMetadata-module__stickyContainer"]';

await clearBookmarks(context);

// Use an issue with enough content to scroll
await page.goto('https://github.com/microsoft/playwright/issues/11975');
await page.waitForLoadState('domcontentloaded');

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

// Verify bookmark button exists in main header
const mainBookmarkButton = page.locator(`${headerActionsSelector} ${bookmarkSelector}`);
await expect(mainBookmarkButton).toBeVisible({ timeout: 10000 });

// Scroll down to trigger sticky header
await page.evaluate(() => window.scrollTo(0, 1500));
await page.waitForTimeout(500);

// Verify sticky header appeared
await expect(page.locator(stickyHeaderSelector)).toBeVisible({ timeout: 5000 });

// Verify bookmark button exists in sticky header
const stickyBookmarkButton = page.locator(`${stickyHeaderSelector} ${bookmarkSelector}`);
await expect(stickyBookmarkButton).toBeVisible({ timeout: 5000 });

// Click sticky header bookmark button to add bookmark
await stickyBookmarkButton.click();

// Verify bookmark was added
const expectedKey = 'microsoft/playwright/issues/11975';
await expect(async () => {
const bookmarks = await getBookmarks(context);
expect(Object.keys(bookmarks)).toContain(expectedKey);
}).toPass({ timeout: 5000 });

// Scroll back up and verify main header button is also in bookmarked state
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(500);

// Click main header button to remove bookmark (verifies sync)
await mainBookmarkButton.click();

// Verify bookmark was removed
await expect(async () => {
const bookmarks = await getBookmarks(context);
expect(Object.keys(bookmarks)).not.toContain(expectedKey);
}).toPass({ timeout: 5000 });
});
});

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

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

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

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

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

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

Expand Down
Loading