Repository: gitify-app/gitify
Pull request #1240 (2024-06-14 18:00:15)

refactor: create `Header` component
Creating of a Header component which also fixes a different top margin on two diff pages (4 vs 2)


Patch:
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
new file mode 100644
index 00000000..011735cf
--- /dev/null
+++ b/src/components/Header.tsx
@@ -0,0 +1,29 @@
+import { ArrowLeftIcon } from '@primer/octicons-react';
+import type { ReactNode } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+interface IHeader {
+  children: ReactNode;
+}
+
+export const Header = ({ children }: IHeader) => {
+  const navigate = useNavigate();
+  return (
+    <div className="mx-8 mt-2 flex items-center justify-between py-2">
+      <button
+        type="button"
+        className="focus:outline-none"
+        title="Go Back"
+        onClick={() => navigate(-1)}
+      >
+        <ArrowLeftIcon
+          size={20}
+          className="hover:text-gray-400"
+          aria-label="Go Back"
+        />
+      </button>
+
+      <h3 className="text-lg font-semibold">{children}</h3>
+    </div>
+  );
+};
diff --git a/src/routes/Accounts.tsx b/src/routes/Accounts.tsx
index ede6e594..1d7572e2 100644
--- a/src/routes/Accounts.tsx
+++ b/src/routes/Accounts.tsx
@@ -1,5 +1,4 @@
 import {
-  ArrowLeftIcon,
   KeyIcon,
   PersonIcon,
   PlusIcon,
@@ -8,11 +7,10 @@ import {
 
 import { type FC, useCallback, useContext } from 'react';
 import { useNavigate } from 'react-router-dom';
-
-import { AppContext } from '../context/App';
-
+import { Header } from '../components/Header';
 import { AuthMethodIcon } from '../components/icons/AuthMethodIcon';
 import { PlatformIcon } from '../components/icons/PlatformIcon';
+import { AppContext } from '../context/App';
 import { BUTTON_CLASS_NAME } from '../styles/gitify';
 import type { Account } from '../types';
 import { getAccountUUID } from '../utils/auth/utils';
@@ -47,23 +45,7 @@ export const AccountsRoute: FC = () => {
 
   return (
     <div className="flex h-screen flex-col" data-testid="accounts">
-      <div className="mx-8 mt-2 flex items-center justify-between py-2">
-        <button
-          type="button"
-          className="focus:outline-none"
-          title="Go Back"
-          onClick={() => navigate(-1)}
-        >
-          <ArrowLeftIcon
-            size={20}
-            className="hover:text-gray-400"
-            aria-label="Go Back"
-          />
-        </button>
-
-        <h3 className="text-lg font-semibold">Accounts</h3>
-      </div>
-
+      <Header>Accounts</Header>
       <div className="flex-grow overflow-x-auto px-8">
         <div className="mt-4 flex flex-col text-sm">
           {auth.accounts.map((account) => (
diff --git a/src/routes/LoginWithOAuthApp.tsx b/src/routes/LoginWithOAuthApp.tsx
index fa908312..25e0c64b 100644
--- a/src/routes/LoginWithOAuthApp.tsx
+++ b/src/routes/LoginWithOAuthApp.tsx
@@ -1,12 +1,7 @@
-import {
-  ArrowLeftIcon,
-  BookIcon,
-  PersonIcon,
-  SignInIcon,
-} from '@primer/octicons-react';
+import { BookIcon, PersonIcon, SignInIcon } from '@primer/octicons-react';
 import { type FC, useCallback, useContext } from 'react';
 import { Form, type FormRenderProps } from 'react-final-form';
-import { useNavigate } from 'react-router-dom';
+import { Header } from '../components/Header';
 import { Button } from '../components/fields/Button';
 import { FieldInput } from '../components/fields/FieldInput';
 import { AppContext } from '../context/App';
@@ -58,7 +53,6 @@ export const validate = (values: IValues): IFormErrors => {
 
 export const LoginWithOAuthApp: FC = () => {
   const { loginWithOAuthApp } = useContext(AppContext);
-  const navigate = useNavigate();
 
   const renderForm = (formProps: FormRenderProps) => {
     const { handleSubmit, submitting, pristine, values } = formProps;
@@ -130,26 +124,10 @@ export const LoginWithOAuthApp: FC = () => {
 
   return (
     <div>
-      <div className="mx-8 mt-4 flex items-center justify-between py-2">
-        <button
-          type="button"
-          className="focus:outline-none"
-          title="Go Back"
-          onClick={() => navigate(-1)}
-        >
-          <ArrowLeftIcon
-            size={20}
-            className="hover:text-gray-400"
-            aria-label="Go Back"
-          />
-        </button>
-
-        <h3 className="justify-center text-lg font-semibold">
-          <PersonIcon size={20} className="mr-2" />
-          Login with OAuth App
-        </h3>
-      </div>
-
+      <Header>
+        <PersonIcon size={20} className="mr-2" />
+        Login with OAuth App
+      </Header>
       <div className="px-8">
         <Form
           initialValues={{
diff --git a/src/routes/LoginWithPersonalAccessToken.tsx b/src/routes/LoginWithPersonalAccessToken.tsx
index 486788cc..944d7177 100644
--- a/src/routes/LoginWithPersonalAccessToken.tsx
+++ b/src/routes/LoginWithPersonalAccessToken.tsx
@@ -1,12 +1,8 @@
-import {
-  ArrowLeftIcon,
-  BookIcon,
-  KeyIcon,
-  SignInIcon,
-} from '@primer/octicons-react';
+import { BookIcon, KeyIcon, SignInIcon } from '@primer/octicons-react';
 import { type FC, useCallback, useContext, useState } from 'react';
 import { Form, type FormRenderProps } from 'react-final-form';
 import { useNavigate } from 'react-router-dom';
+import { Header } from '../components/Header';
 import { Button } from '../components/fields/Button';
 import { FieldInput } from '../components/fields/FieldInput';
 import { AppContext } from '../context/App';
@@ -138,25 +134,10 @@ export const LoginWithPersonalAccessToken: FC = () => {
 
   return (
     <div>
-      <div className="mx-8 mt-4 flex items-center justify-between py-2">
-        <button
-          type="button"
-          className="focus:outline-none"
-          title="Go Back"
-          onClick={() => navigate(-1)}
-        >
-          <ArrowLeftIcon
-            size={20}
-            className="hover:text-gray-400"
-            aria-label="Go Back"
-          />
-        </button>
-
-        <h3 className="justify-center text-lg font-semibold">
-          <KeyIcon size={18} className="mr-2" />
-          Login with Personal Access Token
-        </h3>
-      </div>
+      <Header>
+        <KeyIcon size={18} className="mr-2" />
+        Login with Personal Access Token
+      </Header>
 
       <div className="px-8">
         <Form
diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx
index 5b8f8a0f..cae398c5 100644
--- a/src/routes/Settings.tsx
+++ b/src/routes/Settings.tsx
@@ -1,5 +1,4 @@
 import {
-  ArrowLeftIcon,
   CheckIcon,
   CommentIcon,
   IssueClosedIcon,
@@ -17,6 +16,7 @@ import {
   useState,
 } from 'react';
 import { useNavigate } from 'react-router-dom';
+import { Header } from '../components/Header';
 import { Checkbox } from '../components/fields/Checkbox';
 import { RadioGroup } from '../components/fields/RadioGroup';
 import { AppContext } from '../context/App';
@@ -55,23 +55,7 @@ export const SettingsRoute: FC = () => {
 
   return (
     <div className="flex h-screen flex-col" data-testid="settings">
-      <div className="mx-8 mt-2 flex items-center justify-between py-2">
-        <button
-          type="button"
-          className="focus:outline-none"
-          title="Go Back"
-          onClick={() => navigate(-1)}
-        >
-          <ArrowLeftIcon
-            size={20}
-            className="hover:text-gray-400"
-            aria-label="Go Back"
-          />
-        </button>
-
-        <h3 className="text-lg font-semibold">Settings</h3>
-      </div>
-
+      <Header>Settings</Header>
       <div className="flex-grow overflow-x-auto px-8">
         <fieldset className="mb-3">
           <legend id="appearance" className="mb-1 mt-2 font-semibold">


Test patch:
diff --git a/src/components/Header.test.tsx b/src/components/Header.test.tsx
new file mode 100644
index 00000000..aec69a95
--- /dev/null
+++ b/src/components/Header.test.tsx
@@ -0,0 +1,26 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import { Header } from './Header';
+
+const mockNavigate = jest.fn();
+jest.mock('react-router-dom', () => ({
+  ...jest.requireActual('react-router-dom'),
+  useNavigate: () => mockNavigate,
+}));
+
+describe('components/Header.tsx', () => {
+  it('should render itself & its children', () => {
+    const tree = render(<Header>Test Header</Header>);
+
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('should navigate back', () => {
+    render(<Header>Test Header</Header>);
+
+    fireEvent.click(screen.getByLabelText('Go Back'));
+    expect(mockNavigate).toHaveBeenNthCalledWith(1, -1);
+
+    expect(mockNavigate).toHaveBeenCalledTimes(1);
+    expect(mockNavigate).toHaveBeenCalledWith(-1);
+  });
+});
diff --git a/src/components/__snapshots__/Header.test.tsx.snap b/src/components/__snapshots__/Header.test.tsx.snap
new file mode 100644
index 00000000..98717155
--- /dev/null
+++ b/src/components/__snapshots__/Header.test.tsx.snap
@@ -0,0 +1,124 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`components/Header.tsx should render itself & its children 1`] = `
+{
+  "asFragment": [Function],
+  "baseElement": <body>
+    <div>
+      <div
+        class="mx-8 mt-2 flex items-center justify-between py-2"
+      >
+        <button
+          class="focus:outline-none"
+          title="Go Back"
+          type="button"
+        >
+          <svg
+            aria-label="Go Back"
+            class="hover:text-gray-400"
+            fill="currentColor"
+            focusable="false"
+            height="20"
+            role="img"
+            style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+            viewBox="0 0 16 16"
+            width="20"
+          >
+            <path
+              d="M7.78 12.53a.75.75 0 0 1-1.06 0L2.47 8.28a.75.75 0 0 1 0-1.06l4.25-4.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L4.81 7h7.44a.75.75 0 0 1 0 1.5H4.81l2.97 2.97a.75.75 0 0 1 0 1.06Z"
+            />
+          </svg>
+        </button>
+        <h3
+          class="text-lg font-semibold"
+        >
+          Test Header
+        </h3>
+      </div>
+    </div>
+  </body>,
+  "container": <div>
+    <div
+      class="mx-8 mt-2 flex items-center justify-between py-2"
+    >
+      <button
+        class="focus:outline-none"
+        title="Go Back"
+        type="button"
+      >
+        <svg
+          aria-label="Go Back"
+          class="hover:text-gray-400"
+          fill="currentColor"
+          focusable="false"
+          height="20"
+          role="img"
+          style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+          viewBox="0 0 16 16"
+          width="20"
+        >
+          <path
+            d="M7.78 12.53a.75.75 0 0 1-1.06 0L2.47 8.28a.75.75 0 0 1 0-1.06l4.25-4.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L4.81 7h7.44a.75.75 0 0 1 0 1.5H4.81l2.97 2.97a.75.75 0 0 1 0 1.06Z"
+          />
+        </svg>
+      </button>
+      <h3
+        class="text-lg font-semibold"
+      >
+        Test Header
+      </h3>
+    </div>
+  </div>,
+  "debug": [Function],
+  "findAllByAltText": [Function],
+  "findAllByDisplayValue": [Function],
+  "findAllByLabelText": [Function],
+  "findAllByPlaceholderText": [Function],
+  "findAllByRole": [Function],
+  "findAllByTestId": [Function],
+  "findAllByText": [Function],
+  "findAllByTitle": [Function],
+  "findByAltText": [Function],
+  "findByDisplayValue": [Function],
+  "findByLabelText": [Function],
+  "findByPlaceholderText": [Function],
+  "findByRole": [Function],
+  "findByTestId": [Function],
+  "findByText": [Function],
+  "findByTitle": [Function],
+  "getAllByAltText": [Function],
+  "getAllByDisplayValue": [Function],
+  "getAllByLabelText": [Function],
+  "getAllByPlaceholderText": [Function],
+  "getAllByRole": [Function],
+  "getAllByTestId": [Function],
+  "getAllByText": [Function],
+  "getAllByTitle": [Function],
+  "getByAltText": [Function],
+  "getByDisplayValue": [Function],
+  "getByLabelText": [Function],
+  "getByPlaceholderText": [Function],
+  "getByRole": [Function],
+  "getByTestId": [Function],
+  "getByText": [Function],
+  "getByTitle": [Function],
+  "queryAllByAltText": [Function],
+  "queryAllByDisplayValue": [Function],
+  "queryAllByLabelText": [Function],
+  "queryAllByPlaceholderText": [Function],
+  "queryAllByRole": [Function],
+  "queryAllByTestId": [Function],
+  "queryAllByText": [Function],
+  "queryAllByTitle": [Function],
+  "queryByAltText": [Function],
+  "queryByDisplayValue": [Function],
+  "queryByLabelText": [Function],
+  "queryByPlaceholderText": [Function],
+  "queryByRole": [Function],
+  "queryByTestId": [Function],
+  "queryByText": [Function],
+  "queryByTitle": [Function],
+  "rerender": [Function],
+  "unmount": [Function],
+}
+`;
diff --git a/src/routes/__snapshots__/LoginWithOAuthApp.test.tsx.snap b/src/routes/__snapshots__/LoginWithOAuthApp.test.tsx.snap
index 6f640adc..8f443ca2 100644
--- a/src/routes/__snapshots__/LoginWithOAuthApp.test.tsx.snap
+++ b/src/routes/__snapshots__/LoginWithOAuthApp.test.tsx.snap
@@ -7,7 +7,7 @@ exports[`routes/LoginWithOAuthApp.tsx renders correctly 1`] = `
     <div>
       <div>
         <div
-          class="mx-8 mt-4 flex items-center justify-between py-2"
+          class="mx-8 mt-2 flex items-center justify-between py-2"
         >
           <button
             class="focus:outline-none"
@@ -31,7 +31,7 @@ exports[`routes/LoginWithOAuthApp.tsx renders correctly 1`] = `
             </svg>
           </button>
           <h3
-            class="justify-center text-lg font-semibold"
+            class="text-lg font-semibold"
           >
             <svg
               aria-hidden="true"
@@ -207,7 +207,7 @@ exports[`routes/LoginWithOAuthApp.tsx renders correctly 1`] = `
   "container": <div>
     <div>
       <div
-        class="mx-8 mt-4 flex items-center justify-between py-2"
+        class="mx-8 mt-2 flex items-center justify-between py-2"
       >
         <button
           class="focus:outline-none"
@@ -231,7 +231,7 @@ exports[`routes/LoginWithOAuthApp.tsx renders correctly 1`] = `
           </svg>
         </button>
         <h3
-          class="justify-center text-lg font-semibold"
+          class="text-lg font-semibold"
         >
           <svg
             aria-hidden="true"
diff --git a/src/routes/__snapshots__/LoginWithPersonalAccessToken.test.tsx.snap b/src/routes/__snapshots__/LoginWithPersonalAccessToken.test.tsx.snap
index 233f82e2..5bfd8c9e 100644
--- a/src/routes/__snapshots__/LoginWithPersonalAccessToken.test.tsx.snap
+++ b/src/routes/__snapshots__/LoginWithPersonalAccessToken.test.tsx.snap
@@ -7,7 +7,7 @@ exports[`routes/LoginWithPersonalAccessToken.tsx renders correctly 1`] = `
     <div>
       <div>
         <div
-          class="mx-8 mt-4 flex items-center justify-between py-2"
+          class="mx-8 mt-2 flex items-center justify-between py-2"
         >
           <button
             class="focus:outline-none"
@@ -31,7 +31,7 @@ exports[`routes/LoginWithPersonalAccessToken.tsx renders correctly 1`] = `
             </svg>
           </button>
           <h3
-            class="justify-center text-lg font-semibold"
+            class="text-lg font-semibold"
           >
             <svg
               aria-hidden="true"
@@ -196,7 +196,7 @@ exports[`routes/LoginWithPersonalAccessToken.tsx renders correctly 1`] = `
   "container": <div>
     <div>
       <div
-        class="mx-8 mt-4 flex items-center justify-between py-2"
+        class="mx-8 mt-2 flex items-center justify-between py-2"
       >
         <button
           class="focus:outline-none"
@@ -220,7 +220,7 @@ exports[`routes/LoginWithPersonalAccessToken.tsx renders correctly 1`] = `
           </svg>
         </button>
         <h3
-          class="justify-center text-lg font-semibold"
+          class="text-lg font-semibold"
         >
           <svg
             aria-hidden="true"