Skip to content

Commit

Permalink
test: added tests for data fetching (#228)
Browse files Browse the repository at this point in the history
## PR Checklist

- [x] Addresses an existing open issue: fixes #227
- [x] That issue was marked as [`status: accepting
prs`](https://github.com/JoshuaKGoldberg/refined-saved-replies/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22)
- [x] Steps in
[CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/refined-saved-replies/blob/main/.github/CONTRIBUTING.md)
were taken

## Overview

Extracts and adds unit tests for data fetching helpers in
`content-script.ts`.

The rest of the file is mostly DOM creation, so it wasn't very unit
testable. Ah well.
  • Loading branch information
JoshuaKGoldberg authored Jan 12, 2024
1 parent 40fb754 commit 3533ed1
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 38 deletions.
53 changes: 15 additions & 38 deletions src/content-script.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import * as yaml from "js-yaml";
import Mustache from "mustache";

import { createElement } from "./elements.js";
import { fetchAsJson } from "./fetchAsJson.js";
import { fetchRepliesConfiguration } from "./fetchRepliesConfiguration.js";
import { fetchSettings } from "./fetchSettings.js";
import { getSoon } from "./getSoon.js";
import { isBodyWithReplies, isRepositorySettings } from "./validations.js";

// TODO: Add handling for a rejection
// https://github.com/JoshuaKGoldberg/refined-saved-replies/issues/2
Expand All @@ -24,44 +23,22 @@ async function main() {

const [, userOrOrganization, repository, , issueOrPR] =
window.location.pathname.split("/");
const locator = `${userOrOrganization}/${repository}`;

// 2. Fetch the REST API's JSON descriptions of the item and the repository's settings
const [itemDetails, repositorySettings] = await Promise.all([
fetchAsJson(
`https://api.github.com/repos/${userOrOrganization}/${repository}/issues/${issueOrPR}`,
),
fetchAsJson(
`https://api.github.com/repos/${userOrOrganization}/${repository}`,
),
]);

if (!isRepositorySettings(repositorySettings)) {
console.error("Invalid repository details:", repositorySettings);
const settings = await fetchSettings(issueOrPR, locator);
if (!settings) {
return;
}

// 3. Fetch the repository's .github/replies.yml
const { default_branch: defaultBranch } = repositorySettings;
const repliesUrl = `https://raw.githubusercontent.com/${userOrOrganization}/${repository}/${defaultBranch}/.github/replies.yml`;
const repliesResponse = await fetch(repliesUrl);
const { defaultBranch, itemDetails } = settings;

if (!repliesResponse.ok) {
if (repliesResponse.status !== 404) {
console.error(
"Non-ok response fetching replies:",
repliesResponse.statusText,
);
}

return;
}

const repliesBody = await repliesResponse.text();

// 4. Parse the replies body as yml
const repliesConfiguration = yaml.load(repliesBody);
if (!isBodyWithReplies(repliesConfiguration)) {
console.error("Invalid saved replies:", repliesConfiguration);
// 3. Fetch the repository's .github/replies.yml configuration
const repliesConfiguration = await fetchRepliesConfiguration(
defaultBranch,
locator,
);
if (!repliesConfiguration) {
return;
}

Expand All @@ -75,7 +52,7 @@ async function main() {
}

const onOpenSavedRepliesButtonClick = async () => {
// 6. Add the new replies to the saved reply dropdown
// 5. Add the new replies to the saved reply dropdown
const replyCategoriesDetailsMenus = await getSoon(() =>
Array.from(
document.querySelectorAll(`.Overlay-body .js-saved-reply-menu`),
Expand Down Expand Up @@ -167,7 +144,7 @@ async function main() {
);
}

// 8. Add a second button at the bottom of the modal for adding more
// 6. Add a second button at the bottom of the modal for adding more
// TODO: thanks for the heads up @keithamus :)
// https://github.com/primer/view_components/pull/2364
for (const modal of Array.from(
Expand Down Expand Up @@ -224,7 +201,7 @@ async function main() {
);
};

// 5. Add a listener to modify the saved reply dropdown upon creation
// 4. Add a listener to modify the saved reply dropdown upon creation
openSavedRepliesButton.addEventListener(
"click",
// TODO: Add handling for a rejection
Expand Down
58 changes: 58 additions & 0 deletions src/fetchRepliesConfiguration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

import { fetchRepliesConfiguration } from "./fetchRepliesConfiguration.js";

const mockFetch = vi.fn();
const mockError = vi.fn();

describe("fetchRepliesConfiguration", () => {
beforeEach(() => {
globalThis.fetch = mockFetch;
globalThis.console.error = mockError;
});

it("returns undefined and console errors when the fetch is not ok and status is not 404", async () => {
const statusText = "Oh no!";

mockFetch.mockResolvedValue({ ok: false, status: 500, statusText });

const actual = await fetchRepliesConfiguration("", "");

expect(actual).toBeUndefined();
expect(mockError).toHaveBeenCalledWith(
"Non-ok response fetching replies:",
statusText,
);
});

it("returns undefined and does not console error when the fetch is not ok and status is 404", async () => {
mockFetch.mockResolvedValue({ ok: false, status: 404 });

const actual = await fetchRepliesConfiguration("", "");

expect(actual).toBeUndefined();
expect(mockError).not.toHaveBeenCalled();
});

it("returns undefined and console errors when the fetch retrieves an invalid configuration", async () => {
mockFetch.mockResolvedValue({ ok: true, text: () => "" });

const actual = await fetchRepliesConfiguration("", "");

expect(actual).toBeUndefined();
expect(mockError).toHaveBeenCalledWith("Invalid saved replies:", undefined);
});

it("returns the configuration when fetch retrieves a valid configuration", async () => {
const configuration = { replies: [] };
mockFetch.mockResolvedValue({
ok: true,
text: () => JSON.stringify(configuration),
});

const actual = await fetchRepliesConfiguration("", "");

expect(actual).toEqual(configuration);
expect(mockError).not.toHaveBeenCalled();
});
});
34 changes: 34 additions & 0 deletions src/fetchRepliesConfiguration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as yaml from "js-yaml";

import { isBodyWithReplies } from "./validations.js";

export async function fetchRepliesConfiguration(
defaultBranch: string,
locator: string,
) {
const repliesResponse = await fetch(
`https://raw.githubusercontent.com/${locator}/${defaultBranch}/.github/replies.yml`,
);

if (!repliesResponse.ok) {
if (repliesResponse.status !== 404) {
console.error(
"Non-ok response fetching replies:",
repliesResponse.statusText,
);
}

return;
}

const repliesBody = await repliesResponse.text();

const repliesConfiguration = yaml.load(repliesBody);

if (!isBodyWithReplies(repliesConfiguration)) {
console.error("Invalid saved replies:", repliesConfiguration);
return;
}

return repliesConfiguration;
}
50 changes: 50 additions & 0 deletions src/fetchSettings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

import { fetchSettings } from "./fetchSettings.js";

const mockFetchAsJson = vi.fn();

vi.mock("./fetchAsJson.js", () => ({
get fetchAsJson() {
return mockFetchAsJson;
},
}));

const mockError = vi.fn();

describe("fetchSettings", () => {
beforeEach(() => {
globalThis.console.error = mockError;
});

it("returns undefined and console errors when the repository settings are invalid", async () => {
const repositorySettings = { invalid: true };

mockFetchAsJson
.mockResolvedValueOnce({})
.mockResolvedValueOnce(repositorySettings);

const actual = await fetchSettings("", "");

expect(actual).toBeUndefined();
expect(mockError).toHaveBeenCalledWith(
"Invalid repository details:",
repositorySettings,
);
});

it("returns the default branch and item details when the repository settings are valid", async () => {
const defaultBranch = "some-branch";
const itemDetails = { item: "details" };
const repositorySettings = { default_branch: defaultBranch };

mockFetchAsJson
.mockResolvedValueOnce(itemDetails)
.mockResolvedValueOnce(repositorySettings);

const actual = await fetchSettings("", "");

expect(actual).toEqual({ defaultBranch, itemDetails });
expect(mockError).not.toHaveBeenCalled();
});
});
18 changes: 18 additions & 0 deletions src/fetchSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { fetchAsJson } from "./fetchAsJson.js";
import { isRepositorySettings } from "./validations.js";

export async function fetchSettings(issueOrPR: string, locator: string) {
const [itemDetails, repositorySettings] = await Promise.all([
fetchAsJson(`https://api.github.com/repos/${locator}/issues/${issueOrPR}`),
fetchAsJson(`https://api.github.com/repos/${locator}`),
]);

if (!isRepositorySettings(repositorySettings)) {
console.error("Invalid repository details:", repositorySettings);
return;
}

const { default_branch: defaultBranch } = repositorySettings;

return { defaultBranch, itemDetails };
}

0 comments on commit 3533ed1

Please sign in to comment.