Skip to content

Commit

Permalink
[#443] GitHub adapter broken after GitHub redesign
Browse files Browse the repository at this point in the history
This updates the GitHub adapter to work with the redesigned issue pages.
As we do not know now GitHub Enterprise looks like for the majority of
users, we keep the previous logic for the "legacy" strategy of the
adapter.

To keep the change and the adapter simple, I dropped support selecting
issues on the issues page. Instead, we only support the issue show pages
now. On the long run, it may make sense to look into supporting GitHub
projects too.

The new GitHub adapter supports GitHub issue types and won't look for
the "bug" label any more.

Ticket: #443

Fixes: #443
  • Loading branch information
klappradla committed Feb 3, 2025
1 parent aebf574 commit 93ececb
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 114 deletions.
10 changes: 5 additions & 5 deletions src/core/adapters/dom-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,26 @@ export function trim(string: string | null) {

// Finders

export function $find(selector: string, context: HTMLDocument | HTMLElement) {
export function $find(selector: string, context: Document | HTMLElement) {
return context.querySelector<HTMLElement>(selector);
}

export function $all(selector: string, context: HTMLDocument | HTMLElement) {
export function $all(selector: string, context: Document | HTMLElement) {
return Array.from(context.querySelectorAll<HTMLElement>(selector));
}

export function $has(selector: string, context: HTMLDocument | HTMLElement) {
export function $has(selector: string, context: Document | HTMLElement) {
return $find(selector, context) !== null;
}

// Properties

export function $text(selector: string, context: HTMLDocument | HTMLElement) {
export function $text(selector: string, context: Document | HTMLElement) {
const node = $find(selector, context);
return node ? trim(node.textContent) : null;
}

export function $value(selector: string, context: HTMLDocument | HTMLElement) {
export function $value(selector: string, context: Document | HTMLElement) {
const node = $find(selector, context);
return node ? trim(node.getAttribute("value")) : null;
}
105 changes: 34 additions & 71 deletions src/core/adapters/github.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,84 +9,59 @@ import scan, { selectors } from "./github";
const pages = {
default: {
issuepage: `
<div class="js-issues-results">
<h1 class="gh-header-title">
<span class="js-issue-title">A Random GitHub Issue</span>
<span class="gh-header-number">#12</span>
</h1>
<div data-testid="issue-viewer-container">
<div data-component="TitleArea" data-size-variant="medium">
<h1 data-component="PH_Title" data-hidden="false">
<bdi class="markdown-title" data-testid="issue-title">A Random GitHub Issue</bdi>
<span>#12</span>
</h1>
</div>
</div>`,
bugpage: `
<div class="js-issues-results">
<h1 class="gh-header-title">
<span class="js-issue-title">A Random GitHub Issue</span>
<span class="gh-header-number">#12</span>
</h1>
<div class="js-issue-labels">
<a class="IssueLabel hx_IssueLabel" data-name="bug">bug</a>
<div data-testid="issue-viewer-container">
<div aria-label="Header" role="region" data-testid="issue-header">
<div data-component="TitleArea" data-size-variant="medium">
<h1 data-component="PH_Title" data-hidden="false">
<bdi class="markdown-title" data-testid="issue-title">A Random GitHub Issue</bdi>
<span>#12</span>
</h1>
</div>
</div>
</div>`,
indexpage: `
<div class="js-check-all-container">
<div>
<div id="issue_12" class="js-issue-row selected">
<input type="checkbox" class="js-issues-list-check" name="issues[]" value="12">
<a href="/bitcrowd/tickety-tick/issues/12" class="h4 js-navigation-open">
A Selected GitHub Issue
</a>
<span class="lh-default">
<a href="#" class="IssueLabel">bug</a>
</span>
</div>
<div id="issue_11" class="js-issue-row">
<input type="checkbox" class="js-issues-list-check" name="issues[]" value="11">
<a href="/bitcrowd/tickety-tick/issues/11" class="h4 js-navigation-open">
A GitHub Issue
</a>
<div data-testid="issue-viewer-metadata-pane">
<div data-testid="sidebar-section">
<div data-testid="issue-type-container">
<div>
<a href="/bitcrowd/tickety-tick/issues?q=type:&quot;Bug&quot;">
<span data-color="RED">
<span>
<span>Bug</span>
</span>
</span>
</a>
</div>
</div>
</div>
</div>`,
</div>
</div>`,
},
legacy: {
issuepage: `
<div class="issues-listing">
<div class="js-issues-results">
<h1 class="gh-header-title">
<span class="js-issue-title">A Random GitHub Issue</span>
<span class="gh-header-number">#12</span>
</h1>
</div>`,

bugpage: `
<div class="issues-listing">
<div class="js-issues-results">
<h1 class="gh-header-title">
<span class="js-issue-title">A Random GitHub Issue</span>
<span class="gh-header-number">#12</span>
</h1>
<div class="sidebar-labels">
<a class="label" title="bug">bug</a>
<div class="js-issue-labels">
<a class="IssueLabel hx_IssueLabel" data-name="bug">bug</a>
</div>
</div>`,

indexpage: `
<div class="issues-listing">
<ul>
<li id="issue_12" class="js-issue-row selected">
<input type="checkbox" class="js-issues-list-check" name="issues[]" value="12">
<a href="/bitcrowd/tickety-tick/issues/12" class="h4 js-navigation-open">
A Selected GitHub Issue
</a>
<span class="labels">
<a href="#" class="label">bug</a>
</span>
</li>
<li id="issue_11" class="js-issue-row">
<input type="checkbox" class="js-issues-list-check" name="issues[]" value="11">
<a href="/bitcrowd/tickety-tick/issues/11" class="h4 js-navigation-open">
A GitHub Issue
</a>
<span class="labels"></span>
</li>
</ul>
</div>`,
},
};

Expand Down Expand Up @@ -119,7 +94,7 @@ Object.keys(selectors).forEach((variant) => {
]);
});

it("recognizes issues labelled as bugs", async () => {
it("recognizes issue types", async () => {
const result = await scan(url("issues/12"), doc(html.bugpage));
expect(result).toEqual([
{
Expand All @@ -130,17 +105,5 @@ Object.keys(selectors).forEach((variant) => {
},
]);
});

it("extracts tickets from issues index pages", async () => {
const result = await scan(url("issues"), doc(html.indexpage));
expect(result).toEqual([
{
id: "12",
title: "A Selected GitHub Issue",
type: "bug",
url: "/~https://github.com/test-org/test-project/issues/12",
},
]);
});
});
});
54 changes: 16 additions & 38 deletions src/core/adapters/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import type { TicketData } from "../types";
import { $all, $has, $text, $value } from "./dom-helpers";
import { $has, $text } from "./dom-helpers";
import { hasRequiredDetails } from "./utils";

/**
Expand All @@ -17,57 +17,35 @@ import { hasRequiredDetails } from "./utils";
*/
export const selectors = {
default: {
issuesPage: ".js-check-all-container .js-issue-row.selected",
issuesPageLabel: ".lh-default .IssueLabel",
issuePage: ".js-issues-results .gh-header-number",
issuePageLabel: '.js-issue-labels .IssueLabel[data-name="bug" i]',
issuePage: 'div[data-testid="issue-viewer-container"]',
issueId: 'div[data-component="TitleArea"] span',
issueTitle: 'div[data-component="TitleArea"] bdi.markdown-title',
},
legacy: {
issuesPage: ".issues-listing .js-issue-row.selected",
issuesPageLabel: ".labels .label",
issuePage: ".issues-listing .gh-header-number",
issuePageLabel: '.sidebar-labels .label[title="bug"]',
issuePage: ".js-issues-results .gh-header-number",
issueId: ".gh-header-number",
issueTitle: ".js-issue-title",
},
};

async function attempt(
doc: Document,
select: (typeof selectors)[keyof typeof selectors],
) {
// issue list page
if ($has(select.issuesPage, doc)) {
const issues = $all(select.issuesPage, doc);

const tickets = issues.reduce(
(acc, issue) => {
const id = $value("input.js-issues-list-check", issue);
const title = $text("a.js-navigation-open", issue);

if (!id || !title) return acc;

const labels = $all(select.issuesPageLabel, issue);
const type = labels.some((l) => /bug/i.test(`${l.textContent}`))
? "bug"
: "feature";

const ticket = { id, title, type };
acc.push(ticket);
return acc;
},
<TicketData[]>[],
);

return tickets;
}

// single issue page
if ($has(select.issuePage, doc)) {
const id = $text(".gh-header-number", doc)?.replace(/^#/, "");
const title = $text(".js-issue-title", doc);
const id = $text(select.issueId, doc)?.replace(/^#/, "");
const title = $text(select.issueTitle, doc);

if (!id || !title) return [];

const type = $has(select.issuePageLabel, doc) ? "bug" : "feature";
const type =
$text('div[data-testid="issue-type-container"]', doc)
?.toLowerCase()
.trim() || $has('.js-issue-labels .IssueLabel[data-name="bug" i]', doc)
? "bug"
: "feature";

const tickets = [{ id, title, type }];
return tickets;
}
Expand Down

0 comments on commit 93ececb

Please sign in to comment.