Skip to content

Conversation

@leochiu-a
Copy link
Owner

@leochiu-a leochiu-a commented Nov 25, 2025

Summary

This PR adds a dramatic "You Died" banner feature that displays whenever a pull request is closed on GitHub, completing the full lifecycle celebration suite alongside merge, creation, and approval banners. When users close a PR, they'll see an Elden Ring-themed "You Died" screen with optional sound effects, providing a thematic and humorous touch to the PR workflow.

Key Changes

  • New Close Banner Feature: Added close-pull-request.png asset and close detection mechanism that watches for PR close button clicks and state changes
  • Modular Banner System: Refactored banner rendering logic into reusable banner.ts module supporting all banner types (merged, created, approved, closed)
  • Close Detection System: Implemented closeWatcher.ts with MutationObserver to reliably detect when PRs transition to closed state
  • Settings UI Enhancement: Added "Show on PR close" toggle in popup settings with persistent storage support
  • Comprehensive Test Coverage: Added unit tests for banner rendering (banner.test.ts) and close detection (closeWatcher.test.ts) with 100% coverage of new functionality
  • Documentation Update: Updated README to showcase the new PR close banner feature

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • ♻️ Refactoring (no functional changes)
  • 🎨 Style/formatting changes
  • 🧪 Test improvements
  • 🔧 Configuration changes

Test Plan

Manual Testing

  1. Load the extension in Chrome and navigate to any GitHub pull request
  2. Click the "Close pull request" button in the comment area
  3. Verify the "You Died" banner appears with the close-pull-request.png image
  4. Check audio playback - the "You Die" sound should play (if sound is enabled in settings)
  5. Test settings toggle:
    • Open extension popup
    • Uncheck "Show on PR close"
    • Close another PR and verify no banner appears
    • Re-enable the setting and verify banner shows again
  6. Test existing features - verify merge, create, and approval banners still work as expected

Automated Testing

  • Run npm test to execute the full test suite
  • New tests cover:
    • banner.test.ts: Banner rendering for all types (merged, created, approved, closed)
    • closeWatcher.test.ts: Close state detection and MutationObserver behavior
    • Updated existing tests to include closed banner type

Breaking Changes

None

Checklist

  • 📝 Code follows the style guidelines
  • 👀 Self-review has been performed
  • 🧪 Tests have been added/updated
  • 📖 Documentation has been updated

Copy link
Owner Author

@leochiu-a leochiu-a left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Great work on adding the PR close banner feature! The implementation is solid overall, but I've identified several issues that should be addressed:

Key Issues:

  • 🔴 Security: Potential XSS vulnerability in banner HTML rendering
  • 🟡 Performance: MutationObserver memory leak in edge cases
  • 🟡 Bug: Race condition in close detection timeout logic
  • 🟢 Maintainability: Inconsistent error handling patterns

Please review the inline comments below for specific fixes.

Comment on lines 38 to 44
try {
const banner = document.createElement('div');
banner.id = 'elden-ring-banner';

const asset = bannerAssetMap[type];
const imgPath = chrome.runtime.getURL(`assets/${asset.image}`);
banner.innerHTML = `<img src="${imgPath}" alt="${asset.alt}">`;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security Issue: Potential XSS Vulnerability

While chrome.runtime.getURL() is safe, using innerHTML to inject content could be risky if the asset mapping logic is ever modified. It's better to use DOM APIs for security best practices.

Current:

const banner = document.createElement('div');
banner.id = 'elden-ring-banner';

const asset = bannerAssetMap[type];
const imgPath = chrome.runtime.getURL(`assets/${asset.image}`);
banner.innerHTML = `<img src="${imgPath}" alt="${asset.alt}">`;
document.body.appendChild(banner);

Fix:

const banner = document.createElement('div');
banner.id = 'elden-ring-banner';

const asset = bannerAssetMap[type];
const imgPath = chrome.runtime.getURL(`assets/${asset.image}`);

const img = document.createElement('img');
img.src = imgPath;
img.alt = asset.alt;
banner.appendChild(img);
document.body.appendChild(banner);

This eliminates any potential XSS attack surface.

Comment on lines 29 to 72
export const waitForCloseComplete = (onClose: () => void, timeoutMs: number = 10000): void => {
closeHandled = false;

const observer = new MutationObserver((mutations) => {
if (closeHandled) return;

mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE || closeHandled) {
return;
}

const element = node as Element;
if (isClosedState(element)) {
handleClose(observer, onClose);
return;
}

const closedElement = element.querySelector('.State.State--closed');
if (isClosedState(closedElement)) {
handleClose(observer, onClose);
}
});
});
});

observer.observe(document.body, {
childList: true,
subtree: true,
});

if (checkExistingClosedState(observer, onClose)) {
return;
}

setTimeout(() => {
checkExistingClosedState(observer, onClose);
}, 100);

setTimeout(() => {
observer.disconnect();
console.log('⏰ Close detection timeout');
}, timeoutMs);
};
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance Issue: MutationObserver Memory Leak

If closeHandled is set to true before the timeout callbacks execute, the observer will be disconnected but the timeout callbacks at lines 64 and 68 will still fire and potentially create references that prevent garbage collection.

Current:

setTimeout(() => {
  checkExistingClosedState(observer, onClose);
}, 100);

setTimeout(() => {
  observer.disconnect();
  console.log('⏰ Close detection timeout');
}, timeoutMs);

Fix:

const checkTimeout = setTimeout(() => {
  if (!closeHandled) {
    checkExistingClosedState(observer, onClose);
  }
}, 100);

const cleanupTimeout = setTimeout(() => {
  if (!closeHandled) {
    observer.disconnect();
    console.log('⏰ Close detection timeout');
  }
}, timeoutMs);

// Also clear these timeouts in handleClose:
const handleClose = (observer: MutationObserver, onClose: () => void): void => {
  if (closeHandled) return;
  closeHandled = true;
  console.log('☠️ Pull request closed!');
  clearTimeout(checkTimeout);
  clearTimeout(cleanupTimeout);
  onClose();
  observer.disconnect();
};

This ensures proper cleanup and prevents memory leaks.

Comment on lines 64 to 66
setTimeout(() => {
checkExistingClosedState(observer, onClose);
}, 100);
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Race Condition with Duplicate Checks

The checkExistingClosedState call at line 64 could trigger onClose() even if it was already called by the observer or the initial check. While closeHandled prevents the banner from showing twice, it's cleaner to avoid the race condition.

Current:

setTimeout(() => {
  checkExistingClosedState(observer, onClose);
}, 100);

Fix:

const recheckTimeout = setTimeout(() => {
  if (!closeHandled) {
    checkExistingClosedState(observer, onClose);
  }
}, 100);

Adds a guard to prevent unnecessary work when already handled.

Comment on lines 63 to 69

return true;
} catch (error) {
console.log('Image banner failed, using text banner:', error);
onHide();
return false;
}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintainability: Inconsistent Error Handling

The function catches all errors and logs them, but returns different boolean values. However, the error case calls onHide() immediately which could cause issues if the caller expects the banner lifecycle to complete normally.

Current:

} catch (error) {
  console.log('Image banner failed, using text banner:', error);
  onHide();
  return false;
}

Suggestion:

} catch (error) {
  console.error('Banner rendering failed:', error);
  // Don't call onHide() here - let the caller handle the failure
  return false;
}

Or alternatively, ensure the caller properly handles the false return value. The current pattern is confusing because onHide() is called both on success (after timeout) and on error (immediately).

@leochiu-a leochiu-a merged commit 3295b57 into main Nov 25, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants