Skip to content
Open
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
71 changes: 47 additions & 24 deletions src/content/github/file/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import {
lineSelector,
noVirtLineSelector,
} from "./utils/constants";
import {
waitForElement,
ElementNotFoundError,
} from "./utils/waitForElement";
import {
animateAndAnnotateLines,
clearAnimation,
Expand Down Expand Up @@ -78,9 +82,15 @@ async function main(): Promise<void> {
print("file not detected at current URL");
return;
}
globals.coverageButton = createCoverageButton();
globals.coverageButton = await createCoverageButton();
await process(urlMetadata);
} catch (e) {
if (e instanceof ElementNotFoundError) {
// Raw button not present on this page (e.g. binary files or GitHub DOM
// not yet rendered). This is expected on some file types — skip silently.
print("Raw button not found, skipping coverage overlay");
return;
}
if (Sentry) {
Sentry.captureException(e);
}
Expand Down Expand Up @@ -162,16 +172,24 @@ async function process(metadata: FileMetadata): Promise<void> {
previousElement: globals.coverageButton!,
selectedOptions: selectedFlags,
onClick: handleFlagClick,
}).then(({ button, list }) => {
globals.flagsButton = button;
globals.flagsDrop = new Drop({
target: button,
content: list,
classes: "drop-theme-arrows codecov-z1 codecov-bg-white",
position: "bottom right",
openOn: "click",
})
.then(({ button, list }) => {
globals.flagsButton = button;
globals.flagsDrop = new Drop({
target: button,
content: list,
classes: "drop-theme-arrows codecov-z1 codecov-bg-white",
position: "bottom right",
openOn: "click",
});
})
.catch((e) => {
if (e instanceof ElementNotFoundError) {
print("Raw button not found, skipping flags dropdown");
return;
}
throw e;
});
});
}

const components = await getComponents(metadata);
Expand Down Expand Up @@ -201,16 +219,24 @@ async function process(metadata: FileMetadata): Promise<void> {
previousElement: globals.coverageButton!,
onClick: handleComponentClick,
selectedOptions: selectedComponents,
}).then(({ button, list }) => {
globals.componentsButton = button;
globals.componentsDrop = new Drop({
target: button,
content: list,
classes: "drop-theme-arrows codecov-z1 codecov-bg-white",
position: "bottom right",
openOn: "click",
})
.then(({ button, list }) => {
globals.componentsButton = button;
globals.componentsDrop = new Drop({
target: button,
content: list,
classes: "drop-theme-arrows codecov-z1 codecov-bg-white",
position: "bottom right",
openOn: "click",
});
})
.catch((e) => {
if (e instanceof ElementNotFoundError) {
print("Raw button not found, skipping components dropdown");
return;
}
throw e;
});
});
}

// If commit sha is defined use that, otherwise just branch name
Expand Down Expand Up @@ -291,11 +317,8 @@ async function process(metadata: FileMetadata): Promise<void> {
animateAndAnnotateLines(noVirtLineSelector, annotateLine);
}

function createCoverageButton() {
const rawButton = document.querySelector('[data-testid="raw-button"]');
if (!rawButton) {
throw new Error("Raw button not found");
}
async function createCoverageButton() {
const rawButton = await waitForElement('[data-testid="raw-button"]');
const codecovButton = rawButton.cloneNode(true) as HTMLElement;
codecovButton.addEventListener("click", (event) => {
event.preventDefault();
Expand Down
6 changes: 2 additions & 4 deletions src/content/github/file/utils/dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import browser from "webextension-polyfill";
import _ from "lodash";
import React from "dom-chef";
import { waitForElement } from "./waitForElement";

export async function createDropdown({
title,
Expand All @@ -17,10 +18,7 @@ export async function createDropdown({
previousElement: HTMLElement;
selectedOptions: string[];
}) {
const rawButton = document.querySelector('[data-testid="raw-button"]');
if (!rawButton) {
throw new Error("Raw button not found");
}
const rawButton = await waitForElement('[data-testid="raw-button"]');
const dropdownButton = rawButton.cloneNode(true) as HTMLElement;
const textNode = dropdownButton.querySelector('[data-component="text"]');
if (!textNode || !textNode.parentElement)
Expand Down
45 changes: 45 additions & 0 deletions src/content/github/file/utils/waitForElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Thrown by waitForElement when the element does not appear within the timeout.
*/
export class ElementNotFoundError extends Error {
constructor(selector: string, timeoutMs: number) {
super(`Element "${selector}" not found within ${timeoutMs}ms`);
this.name = "ElementNotFoundError";
}
}

/**
* Waits for a DOM element matching `selector` to appear in the document.
* Uses MutationObserver for efficiency. Resolves with the element when found,
* or rejects with an ElementNotFoundError after `timeoutMs` milliseconds.
*/
export function waitForElement(
selector: string,
timeoutMs = 5000
): Promise<Element> {
return new Promise((resolve, reject) => {
const existing = document.querySelector(selector);
if (existing) {
resolve(existing);
return;
}

let timeoutId: ReturnType<typeof setTimeout>;

const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
clearTimeout(timeoutId);
observer.disconnect();
resolve(el);
}
});

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

timeoutId = setTimeout(() => {
observer.disconnect();
reject(new ElementNotFoundError(selector, timeoutMs));
}, timeoutMs);
});
}
Loading