From 3533ed175213b0316894a29604efa6ecdbd6bd59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Fri, 12 Jan 2024 04:06:31 -0500 Subject: [PATCH] test: added tests for data fetching (#228) ## 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. --- src/content-script.ts | 53 +++++++----------------- src/fetchRepliesConfiguration.test.ts | 58 +++++++++++++++++++++++++++ src/fetchRepliesConfiguration.ts | 34 ++++++++++++++++ src/fetchSettings.test.ts | 50 +++++++++++++++++++++++ src/fetchSettings.ts | 18 +++++++++ 5 files changed, 175 insertions(+), 38 deletions(-) create mode 100644 src/fetchRepliesConfiguration.test.ts create mode 100644 src/fetchRepliesConfiguration.ts create mode 100644 src/fetchSettings.test.ts create mode 100644 src/fetchSettings.ts diff --git a/src/content-script.ts b/src/content-script.ts index 1eabcb98..eeff9584 100644 --- a/src/content-script.ts +++ b/src/content-script.ts @@ -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 @@ -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; } @@ -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`), @@ -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( @@ -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 diff --git a/src/fetchRepliesConfiguration.test.ts b/src/fetchRepliesConfiguration.test.ts new file mode 100644 index 00000000..078c34b9 --- /dev/null +++ b/src/fetchRepliesConfiguration.test.ts @@ -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(); + }); +}); diff --git a/src/fetchRepliesConfiguration.ts b/src/fetchRepliesConfiguration.ts new file mode 100644 index 00000000..81b342dc --- /dev/null +++ b/src/fetchRepliesConfiguration.ts @@ -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; +} diff --git a/src/fetchSettings.test.ts b/src/fetchSettings.test.ts new file mode 100644 index 00000000..7c33aa3c --- /dev/null +++ b/src/fetchSettings.test.ts @@ -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(); + }); +}); diff --git a/src/fetchSettings.ts b/src/fetchSettings.ts new file mode 100644 index 00000000..3e5c4084 --- /dev/null +++ b/src/fetchSettings.ts @@ -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 }; +}