Skip to content

Commit e2a4fc2

Browse files
authored
feat: Build support for new github pr experience (#152)
Adds support for the new github PR experience. This could have been a huge refactor, but I decided to do it more surgically so as to not let this blow out of proportion. Tested the whole matrix of new (new, old), (unified, split), (chrome, ff). Will test again on main before release. Closes #151
1 parent 4386d50 commit e2a4fc2

File tree

2 files changed

+132
-13
lines changed

2 files changed

+132
-13
lines changed

src/content/github/pr/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
export const lineSelector = ".js-file-line";
1+
export const oldLineSelector = ".js-file-line";
2+
export const newLineSelector = ".diff-line-row";

src/content/github/pr/main.tsx

Lines changed: 130 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
clearAnimation,
1010
clearAnnotations,
1111
} from "../common/animation";
12-
import { lineSelector } from "./constants";
12+
import { oldLineSelector, newLineSelector } from "./constants";
1313
import { colors } from "../common/constants";
1414
import { print } from "src/utils";
1515
import { getConsent, getPRReport } from "../common/fetchers";
@@ -82,19 +82,41 @@ async function execute() {
8282
updateContainer(head, patch, change);
8383

8484
globals.coverageReport = transformReport(coverageReport.files);
85-
animateAndAnnotateLines(lineSelector, annotateLine);
85+
86+
annotateLines();
8687
}
8788

88-
function createContainer() {
89-
const parent = document.getElementsByClassName("pr-review-tools").item(0)!;
89+
function isNewExperience() {
90+
const toolbar = document.querySelector(
91+
"section[class*=' PullRequestFilesToolbar-module__toolbar']"
92+
);
93+
return !!toolbar;
94+
}
9095

96+
function createContainer() {
9197
const element = (
92-
<div className="codecov-flex float-left mr-4" id="coverage-report-data">
93-
<div className="my-auto mr-6">Loading coverage report...</div>
98+
<div className="ml-auto" id="coverage-report-data">
99+
<div className="ml-auto mr-6">Loading coverage report...</div>
94100
</div>
95101
);
96102

97-
parent.prepend(element);
103+
if (!isNewExperience()) {
104+
// Old experience
105+
const parent = document
106+
.getElementsByClassName("pr-review-tools")
107+
.item(0)?.parentElement;
108+
109+
parent?.insertBefore(element, parent.lastElementChild);
110+
111+
return;
112+
}
113+
114+
// New experience code
115+
const parent = document.querySelector(
116+
"section[class*=' PullRequestFilesToolbar-module__toolbar']"
117+
)!;
118+
119+
parent.insertBefore(element, parent.lastChild!);
98120
}
99121

100122
function getMetadataFromURL(): { [key: string]: string } | null {
@@ -111,7 +133,7 @@ const handleToggleClick: React.MouseEventHandler = (event) => {
111133
const button = event.target as HTMLElement;
112134
const isInactive = button.getAttribute("data-inactive");
113135
if (isInactive == "true") {
114-
animateAndAnnotateLines(lineSelector, annotateLine);
136+
annotateLines();
115137
button.removeAttribute("data-inactive");
116138
button.innerText = "Hide Coverage";
117139
} else {
@@ -172,7 +194,38 @@ function transformReport(filesReport: any) {
172194
return result;
173195
}
174196

175-
function annotateLine(line: HTMLElement) {
197+
function annotateLines() {
198+
if (!isNewExperience()) {
199+
// old selector/annotation logic
200+
animateAndAnnotateLines(oldLineSelector, oldAnnotateLine);
201+
} else {
202+
// new selector/annotation logic
203+
animateAndAnnotateLines(newLineSelector, newAnnotateLine);
204+
}
205+
}
206+
207+
function clearAnimationAndAnnotations() {
208+
if (!isNewExperience()) {
209+
// old selector/annotation logic
210+
clearAnimation(oldLineSelector, oldAnnotateLine);
211+
clearAnnotations((line: HTMLElement) => (line.style.boxShadow = "inherit"));
212+
} else {
213+
// new selector/annotation logic
214+
clearAnimation(newLineSelector, newAnnotateLine);
215+
clearAnnotations((line: HTMLElement) => {
216+
if (line.children.length < 3) {
217+
return;
218+
}
219+
let child = line.lastElementChild as HTMLElement;
220+
if (child.style.boxShadow !== "inherit") {
221+
child.style.boxShadow = "inherit";
222+
return;
223+
}
224+
});
225+
}
226+
}
227+
228+
function oldAnnotateLine(line: HTMLElement) {
176229
if (line.getAttribute("data-split-side") === "left") {
177230
// split diff view: ignore deleted line
178231
return;
@@ -203,9 +256,74 @@ function annotateLine(line: HTMLElement) {
203256
}
204257
}
205258

206-
function clearAnimationAndAnnotations() {
207-
clearAnimation(lineSelector, annotateLine);
208-
clearAnnotations((line: HTMLElement) => (line.style.boxShadow = "inherit"));
259+
function newAnnotateLine(line: HTMLElement) {
260+
const secondChild = line.children[1];
261+
const thirdChild = line.children[2];
262+
263+
if (!secondChild || !thirdChild) {
264+
return;
265+
}
266+
267+
// If the second child of the row is a line number cell (possibly empty), we're looking at a unified diff.
268+
const isUnifiedDiff = line
269+
.querySelectorAll("td[class*=' diff-line-number']")
270+
.values()
271+
.toArray()
272+
.includes(secondChild);
273+
274+
// New line number cell is in cell 2 in a unified diff and cell 3 in a split diff.
275+
const newLineNumberCell = isUnifiedDiff ? secondChild : thirdChild;
276+
277+
// We want to ignore deleted lines.
278+
// If the new line number cell does not contain a line number, then the line was deleted.
279+
if (!newLineNumberCell.textContent) {
280+
return;
281+
}
282+
283+
// This is not a deleted line, grab the line number and find coverage value.
284+
const lineNumber = newLineNumberCell.textContent;
285+
286+
// Get the file name.
287+
// Up to the shared root of the file section then down to the file header
288+
//
289+
// For some reason the text content here contains three invisible bytes
290+
// adding up to one utf-8 character, which we need to remove.
291+
//
292+
// >> e = new TextEncoder()
293+
// >> e.encode(newLineNumberCell.textContent)
294+
// Uint8Array(59) [ 226, 128, 142, 97, 112, 112, 115, 47, 119, 111, … ]
295+
// >> e.encode("apps/worker/services/test_analytics/ta_process_flakes.py")
296+
// Uint8Array(56) [ 97, 112, 112, 115, 47, 119, 111, 114, 107, 101, … ]
297+
// >> e.encode(newLineNumberCell.textContent.slice(1))
298+
// Uint8Array(56) [ 97, 112, 112, 115, 47, 119, 111, 114, 107, 101, … ]
299+
//
300+
// Idk why these are here, but we can just remove them.
301+
302+
const fileNameContainer = line
303+
.closest("div[class^='Diff-module__diffTargetable']")
304+
?.querySelector("h3[class^='DiffFileHeader-module__file-name']");
305+
const fileName = fileNameContainer?.textContent?.slice(1);
306+
if (!fileName) {
307+
return;
308+
}
309+
310+
const status =
311+
globals.coverageReport?.[fileName]?.lines[lineNumber]?.coverage["head"];
312+
if (status == null) {
313+
return;
314+
}
315+
316+
const lineContentCell = newLineNumberCell.nextSibling as HTMLElement;
317+
const borderStylePrefix = "inset 2px 0 ";
318+
if (status === CoverageStatus.COVERED) {
319+
lineContentCell.style.boxShadow = `${borderStylePrefix} ${colors.green}`;
320+
} else if (status === CoverageStatus.UNCOVERED) {
321+
lineContentCell.style.boxShadow = `${borderStylePrefix} ${colors.red}`;
322+
} else if (status === CoverageStatus.PARTIAL) {
323+
lineContentCell.style.boxShadow = `${borderStylePrefix} ${colors.yellow}`;
324+
} else {
325+
lineContentCell.style.boxShadow = "inherit";
326+
}
209327
}
210328

211329
await init();

0 commit comments

Comments
 (0)