From c1ca6d84faa36ad06a174442e5851c59355f4c9d Mon Sep 17 00:00:00 2001 From: VictoriaBeilsten-Edmands Date: Fri, 20 Dec 2024 15:32:24 +0000 Subject: [PATCH 1/8] Add GH action for storybook deployment to GH pages --- .github/workflows/deploy-storybook.yaml | 43 +++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/deploy-storybook.yaml diff --git a/.github/workflows/deploy-storybook.yaml b/.github/workflows/deploy-storybook.yaml new file mode 100644 index 0000000..56c00a4 --- /dev/null +++ b/.github/workflows/deploy-storybook.yaml @@ -0,0 +1,43 @@ +name: Build and Publish storybook to GitHub Pages + +on: + push: + branches: + - "main" +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.build-publish.outputs.page_url }} + + permissions: + pages: write + id-token: write + + runs-on: ubuntu-latest + strategy: + matrix: + node-version: ["20.x"] + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + run_install: | + args: [ --force ] + + - name: Set Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: pnpm + + - name: Build and publish + id: build-publish + uses: bitovi/github-actions-storybook-to-github-pages@v1.0.3 + with: + checkout: false + path: storybook/storybook-static + install_command: echo Already done + build_command: pnpm build:storybook \ No newline at end of file From 1e6b88291d34632e78266a32e4e8e46ed4294162 Mon Sep 17 00:00:00 2001 From: VictoriaBeilsten-Edmands Date: Fri, 20 Dec 2024 15:33:03 +0000 Subject: [PATCH 2/8] Add GH action to run tests and build --- .github/workflows/test-build.yaml | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/test-build.yaml diff --git a/.github/workflows/test-build.yaml b/.github/workflows/test-build.yaml new file mode 100644 index 0000000..af2879a --- /dev/null +++ b/.github/workflows/test-build.yaml @@ -0,0 +1,39 @@ +# This workflow will install dependencies, run tests and lint + +name: Run CI + +on: + push: + branches: [ "main" ] + tags: ['v*'] + pull_request: + types: [ opened, synchronize ] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: ["20.x"] + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + uses: pnpm/action-setup@v4 + with: + run_install: | + args: [ --force ] + + - name: Set Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: pnpm + + - name: Run Typescript tests and lint client code + run: | + pnpm lint + pnpm jest + pnpm build \ No newline at end of file From a522c875b69ea6c46a881b02c724ec0718fcbfee Mon Sep 17 00:00:00 2001 From: VictoriaBeilsten-Edmands Date: Thu, 2 Jan 2025 16:05:53 +0000 Subject: [PATCH 3/8] Add GH action to build and publish NPM package --- .github/workflows/npm-publish.yaml | 45 ++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/npm-publish.yaml diff --git a/.github/workflows/npm-publish.yaml b/.github/workflows/npm-publish.yaml new file mode 100644 index 0000000..7cff1e6 --- /dev/null +++ b/.github/workflows/npm-publish.yaml @@ -0,0 +1,45 @@ +# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created +# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages + +name: Node.js Package + +on: + push: + tags: + - v* + + +jobs: + publish-npm: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: | + args: [ --force ] + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: '20.x' + registry-url: https://registry.npmjs.org/ + scope: '@diamondlightsource' + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm build + + - name: Test + run: pnpm jest + + - name: Publish + run: pnpm publish -r --no-git-checks --access public + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN_DLS}} \ No newline at end of file From 4dc47f04f3a79af2f6a1937cd118de2243f3349a Mon Sep 17 00:00:00 2001 From: VictoriaBeilsten-Edmands Date: Thu, 2 Jan 2025 16:07:14 +0000 Subject: [PATCH 4/8] Update and add config files --- babel.config.js | 12 ++++------ eslint.config.js | 57 ++++++++++++++++++++++++++++++++++++++++++++ jest.config.js | 10 ++++---- package.json | 10 ++++++++ tsconfig.eslint.json | 10 ++++++++ 5 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 eslint.config.js create mode 100644 tsconfig.eslint.json diff --git a/babel.config.js b/babel.config.js index 7e8f306..7318df1 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,7 +1,5 @@ -module.exports = { - presets: [ - "@babel/preset-env", - ["@babel/preset-react",{runtime: 'automatic'}], - "@babel/preset-typescript", - ], -}; +export const presets = [ + "@babel/preset-env", + ["@babel/preset-react", { runtime: "automatic" }], + "@babel/preset-typescript", +]; diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..d538b44 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,57 @@ +import { FlatCompat } from "@eslint/eslintrc"; +import js from "@eslint/js"; +import tsPlugin from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import reactPlugin from "eslint-plugin-react"; +import prettierPlugin from "eslint-plugin-prettier"; + +const compat = new FlatCompat(); + +export default [ + {ignores: [ + "**/storybook-static/**", + "**/*.css", + "**/*.json", + "**/*.d.ts", + "**/jest.config.ts", + "**/dist/*", + "eslint.config.js", + "**/*.html", + "**/*.svg", + "**/*.md", + "jest.config.js", + "rollup.config.mjs", + "babel.config.js", + ]}, + js.configs.recommended, + ...compat.extends("plugin:@typescript-eslint/recommended"), + ...compat.extends("plugin:react/recommended"), + ...compat.extends("prettier"), + { + languageOptions: { + parser: tsParser, + parserOptions: { + project: "tsconfig.eslint.json" + }, + }, + plugins: { + "@typescript-eslint": tsPlugin, + prettier: prettierPlugin, + react: reactPlugin, + }, + rules: { + "react/react-in-jsx-scope": "off", + "no-console": "off", + "prettier/prettier": "error", + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_" }, + ], + }, + settings: { + react: { + version: "18", + }, + }, + }, +]; diff --git a/jest.config.js b/jest.config.js index a6b29cf..9ec2561 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,4 @@ -module.exports = { - testEnvironment: "jsdom", - moduleNameMapper: { - '^.+.(svg)$': 'jest-transform-stub', - } -}; \ No newline at end of file +export const testEnvironment = "jsdom"; +export const moduleNameMapper = { + "^.+.(svg)$": "jest-transform-stub", +}; diff --git a/package.json b/package.json index 201da1d..58f55fd 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "@diamondlightsource/sci-react-ui", "version": "0.0.1", "description": "A collection of react components based on MUI.", + "type": "module", "author": "DLS", "license": "ISC", "repository": { @@ -11,6 +12,7 @@ "scripts": { "build": "tsc -b && rollup --config rollup.config.mjs", "lint": "eslint .", + "lint:tsc": "pnpm tsc --noEmit", "rollup": "rollup --config rollup.config.mjs", "jest": "jest --config jest.config.js", "jest:coverage": "jest --coverage", @@ -22,7 +24,15 @@ "module": "dist/index.esm.js", "types": "dist/index.d.ts", "dependencies": { + "@eslint/eslintrc": "^3.2.0", "@mui/icons-material": "^6.1.7", + "@typescript-eslint/eslint-plugin": "^8.18.1", + "@typescript-eslint/parser": "^8.18.1", + "eslint": "^9.17.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^5.1.0", "jest-transform-stub": "^2.0.0", "react-icons": "^5.3.0" }, diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 0000000..434adc6 --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "src", + "src/types", + ".storybook/**/*.tsx", + ".storybook/**/*.ts", + ".storybook/**/*.js", + ] +} From 10c69e54cc9cb92dd9f9802679638b4a415ae975 Mon Sep 17 00:00:00 2001 From: VictoriaBeilsten-Edmands Date: Thu, 2 Jan 2025 16:10:34 +0000 Subject: [PATCH 5/8] Fix Prettier and linting errors --- .storybook/ThemeSwapper.tsx | 39 ++-- .storybook/main.ts | 5 +- .storybook/preview.tsx | 70 ++++--- rollup.config.mjs | 92 ++++----- src/components/Breadcrumbs.stories.tsx | 52 ++--- src/components/Breadcrumbs.test.tsx | 264 +++++++++++++------------ src/components/Breadcrumbs.tsx | 153 +++++++------- src/components/Footer.test.tsx | 4 +- src/components/Navbar.tsx | 2 +- src/components/User.stories.tsx | 6 +- src/components/User.test.tsx | 171 ++++++++-------- src/components/User.tsx | 110 ++++++----- src/components/VisitInput.stories.tsx | 2 +- src/components/VisitInput.test.tsx | 10 +- src/styles/colours.ts | 3 +- src/styles/components.tsx | 11 +- src/themes/ThemeProvider.test.tsx | 113 ++++++----- src/themes/ThemeProvider.tsx | 30 ++- src/utils/diamond.test.ts | 11 +- src/utils/diamond.ts | 20 +- tsconfig.json | 2 +- 21 files changed, 623 insertions(+), 547 deletions(-) diff --git a/.storybook/ThemeSwapper.tsx b/.storybook/ThemeSwapper.tsx index 206b375..71b36bb 100644 --- a/.storybook/ThemeSwapper.tsx +++ b/.storybook/ThemeSwapper.tsx @@ -1,27 +1,38 @@ -import {useColorScheme} from "@mui/material"; +import { useColorScheme } from "@mui/material"; import * as React from "react"; -import {useEffect} from "react"; +import { useEffect } from "react"; + +interface Globals { + theme: string; + themeMode: string; +} + +interface Context { + globals: Globals; +} export interface ThemeSwapperProps { - context: any, + context: Context; children: React.ReactNode; } -export const TextLight = 'Mode: Light' -export const TextDark = 'Mode: Dark' +export const TextLight = "Mode: Light"; +export const TextDark = "Mode: Dark"; -const ThemeSwapper = ( {context, children}: ThemeSwapperProps ) => { +const ThemeSwapper = ({ context, children }: ThemeSwapperProps) => { const { mode, setMode } = useColorScheme(); //if( !mode ) return - + useEffect(() => { const selectedThemeMode = context.globals.themeMode || TextLight; - setMode((selectedThemeMode == TextLight) ? "light" : "dark") - },[context.globals.themeMode]); - - return
- {children} -
+ setMode(selectedThemeMode == TextLight ? "light" : "dark"); + }, [context.globals.themeMode]); + + return ( +
+ {children} +
+ ); }; -export { ThemeSwapper }; \ No newline at end of file +export { ThemeSwapper, Context }; diff --git a/.storybook/main.ts b/.storybook/main.ts index 6426ab2..d9f3c9b 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -7,8 +7,9 @@ const config: StorybookConfig = { "@storybook/addon-essentials", "@chromatic-com/storybook", "@storybook/addon-interactions", - '@storybook/addon-links', - 'storybook-dark-mode' + "@storybook/addon-links", + "@storybook/addon-toolbars", + "storybook-dark-mode", ], framework: { name: "@storybook/react-webpack5", diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 38cdfd0..056f01e 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,64 +1,70 @@ -import React from 'react'; -import {CssBaseline} from "@mui/material"; +import React from "react"; +import { CssBaseline } from "@mui/material"; import type { Preview } from "@storybook/react"; -import {ThemeProvider} from '../src' -import {GenericTheme, DiamondTheme} from '../src' +import { ThemeProvider } from "../src"; +import { GenericTheme, DiamondTheme } from "../src"; -import {ThemeSwapper, TextLight, TextDark} from "./ThemeSwapper"; +import { Context, ThemeSwapper, TextLight, TextDark } from "./ThemeSwapper"; -const TextThemeBase = 'Theme: Generic' -const TextThemeDiamond = 'Theme: Diamond' +const TextThemeBase = "Theme: Generic"; +const TextThemeDiamond = "Theme: Diamond"; export const decorators = [ - (StoriesWithPadding:any) => { - return
- -
+ (StoriesWithPadding: React.FC) => { + return ( +
+ +
+ ); }, - (StoriesWithThemeSwapping:any, context: any) => { - return - - + (StoriesWithThemeSwapping: React.FC, context: Context) => { + return ( + + + + ); }, - (StoriesWithThemeProvider:any, context:any) => { + (StoriesWithThemeProvider: React.FC, context: Context) => { const selectedTheme = context.globals.theme || TextThemeBase; const selectedThemeMode = context.globals.themeMode || TextLight; - return - - - + return ( + + + + + ); }, ]; const preview: Preview = { globalTypes: { theme: { - description: 'Global theme for components', + description: "Global theme for components", toolbar: { - title: 'Theme', - icon: 'cog', + title: "Theme", + icon: "cog", items: [TextThemeBase, TextThemeDiamond], dynamicTitle: true, }, }, themeMode: { - description: 'Global theme mode for components', + description: "Global theme mode for components", toolbar: { - title: 'Theme Mode', - icon: 'mirror', + title: "Theme Mode", + icon: "mirror", items: [TextLight, TextDark], dynamicTitle: true, }, }, }, initialGlobals: { - theme: 'Theme: Diamond', - themeMode: 'Mode: Light', + theme: "Theme: Diamond", + themeMode: "Mode: Light", }, parameters: { controls: { @@ -68,7 +74,7 @@ const preview: Preview = { }, }, backgrounds: { disable: true }, - layout: 'fullscreen', + layout: "fullscreen", }, }; diff --git a/rollup.config.mjs b/rollup.config.mjs index 1421867..a575d94 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -9,51 +9,51 @@ import image from "@rollup/plugin-image"; import packageJson from "./package.json" with { type: "json" }; export default [ - { - input: "src/index.ts", - output: { - format: "cjs", - file: packageJson.main - }, - plugins: [ - peerDepsExternal({ - includeDependencies: true, - }), - image(), - resolve(), - commonjs(), - terser(), - typescript({ - tsconfig: "./tsconfig.json", - exclude: ["**/*.stories.*", "**/*.test.*"], - }), - postcss({ - extensions: [".css"], - }), - ], + { + input: "src/index.ts", + output: { + format: "cjs", + file: packageJson.main, }, - { - input: "src/index.ts", - output: { - format: "esm", - sourcemap: true, - file: packageJson.module - }, - plugins: [ - peerDepsExternal({ - includeDependencies: true, - }), - image(), - resolve(), - commonjs(), - terser(), - typescript({ - tsconfig: "./tsconfig.json", - exclude: ["**/*.stories.*", "**/*.test.*"], - }), - postcss({ - extensions: [".css"], - }), - ], - } + plugins: [ + peerDepsExternal({ + includeDependencies: true, + }), + image(), + resolve(), + commonjs(), + terser(), + typescript({ + tsconfig: "./tsconfig.json", + exclude: ["**/*.stories.*", "**/*.test.*"], + }), + postcss({ + extensions: [".css"], + }), + ], + }, + { + input: "src/index.ts", + output: { + format: "esm", + sourcemap: true, + file: packageJson.module, + }, + plugins: [ + peerDepsExternal({ + includeDependencies: true, + }), + image(), + resolve(), + commonjs(), + terser(), + typescript({ + tsconfig: "./tsconfig.json", + exclude: ["**/*.stories.*", "**/*.test.*"], + }), + postcss({ + extensions: [".css"], + }), + ], + }, ]; diff --git a/src/components/Breadcrumbs.stories.tsx b/src/components/Breadcrumbs.stories.tsx index 6589399..b4b03d5 100644 --- a/src/components/Breadcrumbs.stories.tsx +++ b/src/components/Breadcrumbs.stories.tsx @@ -1,47 +1,47 @@ import { Meta, StoryObj } from "@storybook/react"; -import { Breadcrumbs} from "./Breadcrumbs"; +import { Breadcrumbs } from "./Breadcrumbs"; const meta: Meta = { - title: "SciReactUI/Navigation/Breadcrumbs", - component: Breadcrumbs, - tags: ["autodocs"], + title: "SciReactUI/Navigation/Breadcrumbs", + component: Breadcrumbs, + tags: ["autodocs"], }; export default meta; type Story = StoryObj; export const Default: Story = { - args: { - path: "/first/second/third/last/" - }, -} + args: { + path: "/first/second/third/last/", + }, +}; export const ShortPath: Story = { - args: { - path: "just one" - }, + args: { + path: "just one", + }, }; export const LongPath: Story = { - args: { - path: "/first/the second/third/fourth/almost last/last one/" - }, + args: { + path: "/first/the second/third/fourth/almost last/last one/", + }, }; export const Empty: Story = { - args: { - path: "" - }, + args: { + path: "", + }, }; export const ColorChange: Story = { - args: { - path: ["first","second","third","last"], - rootProps: { - sx: { backgroundColor: "blue" } - }, - muiBreadcrumbsProps: { - sx: { color: "yellow" } - } - }, + args: { + path: ["first", "second", "third", "last"], + rootProps: { + sx: { backgroundColor: "blue" }, + }, + muiBreadcrumbsProps: { + sx: { color: "yellow" }, + }, + }, }; diff --git a/src/components/Breadcrumbs.test.tsx b/src/components/Breadcrumbs.test.tsx index 2bf44f6..5d922f6 100644 --- a/src/components/Breadcrumbs.test.tsx +++ b/src/components/Breadcrumbs.test.tsx @@ -3,131 +3,139 @@ import { Breadcrumbs, getCrumbs } from "./Breadcrumbs"; import "@testing-library/jest-dom"; describe("Breadcrumbs", () => { - - const crumbFirst = "first", - crumbFirstTitle = "First", - crumbSecond = "second", - crumbSecondTitle = "Second", - crumbLast = "last one", - crumbLastTitle = "Last one", - defaultStringPath = `/${crumbFirst}/${crumbSecond}/${crumbLast}`, - defaultArrayPath = [crumbFirst,crumbSecond,crumbLast]; - - - function testHomeExists( renderResult : RenderResult ) { - const {getByTestId} = renderResult; - const homeIcon = getByTestId("HomeIcon"); - - expect(homeIcon).toBeInTheDocument() - expect(homeIcon.parentElement).toHaveAttribute("href","/") - } - - function testCrumbsExist( renderResult : RenderResult ) { - const {getAllByRole, getByRole, getByText, queryByRole} = renderResult; - - expect(getAllByRole("link")).toHaveLength(3) - - testHomeExists(renderResult) - - let crumb = getByRole("link", {name: crumbFirstTitle}) - expect(crumb).toBeInTheDocument() - expect(crumb).toHaveAttribute("href", `/${crumbFirst}`) - - crumb = getByRole("link", {name: crumbSecondTitle}) - expect(crumb).toBeInTheDocument() - expect(crumb).toHaveAttribute("href", `/${crumbFirst}/${crumbSecond}`) - - expect(queryByRole("link", {name: crumbLastTitle})).not.toBeInTheDocument() - expect(getByText(crumbLastTitle)).toBeInTheDocument() - } - - it("should render without errors", () => { - render(); - }); - - it("should use a path as string", () => { - testCrumbsExist( - render() - ); - }) - - it("should use a path as array", () => { - testCrumbsExist( - render() - ); - }); - - it("should show just home when an empty string", () => { - const renderResult = render(); - testHomeExists(renderResult) - expect(renderResult.getAllByRole("link")).toHaveLength(1) - }); - - it("should show just home when an empty array", () => { - const renderResult = render(); - - testHomeExists(renderResult) - expect(renderResult.getAllByRole("link")).toHaveLength(1) - }); -}) - -describe("getCrumbs", ()=>{ - const correctCrumbs = [{ - name: "First", - href: "/first" - },{ - name: "Second", - href: "/first/second" - },{ - name: "Last one", - href: "/first/second/last one" - }] - - it("should match if path string", () => { - expect(getCrumbs("/first/second/last one")).toStrictEqual(correctCrumbs); - }); - - it("should match if last slash included", () => { - expect(getCrumbs("/first/second/last one/")).toStrictEqual(correctCrumbs); - }); - - it("should match if first slash excluded", () => { - expect(getCrumbs("first/second/last one")).toStrictEqual(correctCrumbs); - }); - - it("should match if first slash excluded and last slash included", () => { - expect(getCrumbs("first/second/last one")).toStrictEqual(correctCrumbs); - }); - - it("should match path string with multi separators", () => { - expect(getCrumbs("///first//second/last one")).toStrictEqual(correctCrumbs); - }); - - it("should return an empty array when an empty string is passed", () => { - expect(getCrumbs("")).toStrictEqual([]); - }); - - it("should return an empty array when spaces are passed", () => { - expect(getCrumbs(" ")).toStrictEqual([]); - }); - - it("should match if path array", () => { - expect(getCrumbs( ["first","second","last one"])).toStrictEqual(correctCrumbs); - }); - - it("should match if path array with empty", () => { - expect(getCrumbs( ["first","second","last one",""])).toStrictEqual(correctCrumbs); - }); - - it("should match by removing empty item", () => { - expect(getCrumbs( ["first","second","last one",""])).toStrictEqual(correctCrumbs); - }); - - it("should match by removing spaces only", () => { - expect(getCrumbs( ["first","second","last one"," "])).toStrictEqual(correctCrumbs); - }); - - it("should return an empty array when an empty array is passed", () => { - expect(getCrumbs([])).toStrictEqual([]); - }); -}) \ No newline at end of file + const crumbFirst = "first", + crumbFirstTitle = "First", + crumbSecond = "second", + crumbSecondTitle = "Second", + crumbLast = "last one", + crumbLastTitle = "Last one", + defaultStringPath = `/${crumbFirst}/${crumbSecond}/${crumbLast}`, + defaultArrayPath = [crumbFirst, crumbSecond, crumbLast]; + + function testHomeExists(renderResult: RenderResult) { + const { getByTestId } = renderResult; + const homeIcon = getByTestId("HomeIcon"); + + expect(homeIcon).toBeInTheDocument(); + expect(homeIcon.parentElement).toHaveAttribute("href", "/"); + } + + function testCrumbsExist(renderResult: RenderResult) { + const { getAllByRole, getByRole, getByText, queryByRole } = renderResult; + + expect(getAllByRole("link")).toHaveLength(3); + + testHomeExists(renderResult); + + let crumb = getByRole("link", { name: crumbFirstTitle }); + expect(crumb).toBeInTheDocument(); + expect(crumb).toHaveAttribute("href", `/${crumbFirst}`); + + crumb = getByRole("link", { name: crumbSecondTitle }); + expect(crumb).toBeInTheDocument(); + expect(crumb).toHaveAttribute("href", `/${crumbFirst}/${crumbSecond}`); + + expect( + queryByRole("link", { name: crumbLastTitle }), + ).not.toBeInTheDocument(); + expect(getByText(crumbLastTitle)).toBeInTheDocument(); + } + + it("should render without errors", () => { + render(); + }); + + it("should use a path as string", () => { + testCrumbsExist(render()); + }); + + it("should use a path as array", () => { + testCrumbsExist(render()); + }); + + it("should show just home when an empty string", () => { + const renderResult = render(); + testHomeExists(renderResult); + expect(renderResult.getAllByRole("link")).toHaveLength(1); + }); + + it("should show just home when an empty array", () => { + const renderResult = render(); + + testHomeExists(renderResult); + expect(renderResult.getAllByRole("link")).toHaveLength(1); + }); +}); + +describe("getCrumbs", () => { + const correctCrumbs = [ + { + name: "First", + href: "/first", + }, + { + name: "Second", + href: "/first/second", + }, + { + name: "Last one", + href: "/first/second/last one", + }, + ]; + + it("should match if path string", () => { + expect(getCrumbs("/first/second/last one")).toStrictEqual(correctCrumbs); + }); + + it("should match if last slash included", () => { + expect(getCrumbs("/first/second/last one/")).toStrictEqual(correctCrumbs); + }); + + it("should match if first slash excluded", () => { + expect(getCrumbs("first/second/last one")).toStrictEqual(correctCrumbs); + }); + + it("should match if first slash excluded and last slash included", () => { + expect(getCrumbs("first/second/last one")).toStrictEqual(correctCrumbs); + }); + + it("should match path string with multi separators", () => { + expect(getCrumbs("///first//second/last one")).toStrictEqual(correctCrumbs); + }); + + it("should return an empty array when an empty string is passed", () => { + expect(getCrumbs("")).toStrictEqual([]); + }); + + it("should return an empty array when spaces are passed", () => { + expect(getCrumbs(" ")).toStrictEqual([]); + }); + + it("should match if path array", () => { + expect(getCrumbs(["first", "second", "last one"])).toStrictEqual( + correctCrumbs, + ); + }); + + it("should match if path array with empty", () => { + expect(getCrumbs(["first", "second", "last one", ""])).toStrictEqual( + correctCrumbs, + ); + }); + + it("should match by removing empty item", () => { + expect(getCrumbs(["first", "second", "last one", ""])).toStrictEqual( + correctCrumbs, + ); + }); + + it("should match by removing spaces only", () => { + expect(getCrumbs(["first", "second", "last one", " "])).toStrictEqual( + correctCrumbs, + ); + }); + + it("should return an empty array when an empty array is passed", () => { + expect(getCrumbs([])).toStrictEqual([]); + }); +}); diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx index 162c379..6f3a5cf 100644 --- a/src/components/Breadcrumbs.tsx +++ b/src/components/Breadcrumbs.tsx @@ -1,92 +1,101 @@ // Adapted from https://github.com/DiamondLightSource/web-ui-components import { - Breadcrumbs as Mui_Breadcrumbs, - BreadcrumbsProps as Mui_BreadcrumbsProps, - Container, - Link, - Paper, - PaperProps, - Typography, - useTheme, + Breadcrumbs as Mui_Breadcrumbs, + BreadcrumbsProps as Mui_BreadcrumbsProps, + Container, + Link, + Paper, + PaperProps, + Typography, + useTheme, } from "@mui/material"; -import HomeIcon from '@mui/icons-material/Home'; -import NavigateNextIcon from '@mui/icons-material/NavigateNext'; +import HomeIcon from "@mui/icons-material/Home"; +import NavigateNextIcon from "@mui/icons-material/NavigateNext"; interface BreadcrumbsProps { - path: string | string[]; - rootProps?: PaperProps, - muiBreadcrumbsProps?: Mui_BreadcrumbsProps, + path: string | string[]; + rootProps?: PaperProps; + muiBreadcrumbsProps?: Mui_BreadcrumbsProps; } type CrumbData = { - name: string, - href: string -} + name: string; + href: string; +}; /** * Create CrumbData from crumb parts with links * @param path A single string path, or an array of string parts */ -export function getCrumbs(path : string | string[] ) : CrumbData[] { - - if( typeof path === "string") { - path = path.split("/") - } - - const crumbs = path.filter((item)=>item.trim() !== "") - - return crumbs.map((crumb, i) => { - return { - name: crumb.charAt(0).toUpperCase() + crumb.slice(1), - href: "/" + crumbs.slice(0, i + 1).join("/") - } - }) - +export function getCrumbs(path: string | string[]): CrumbData[] { + if (typeof path === "string") { + path = path.split("/"); + } + + const crumbs = path.filter((item) => item.trim() !== ""); + + return crumbs.map((crumb, i) => { + return { + name: crumb.charAt(0).toUpperCase() + crumb.slice(1), + href: "/" + crumbs.slice(0, i + 1).join("/"), + }; + }); } +const Breadcrumbs = ({ + path, + rootProps, + muiBreadcrumbsProps, +}: BreadcrumbsProps) => { + const theme = useTheme(); + const crumbs: CrumbData[] = getCrumbs(path); + + return ( + + + } + sx={{ color: theme.palette.primary.contrastText }} + {...muiBreadcrumbsProps} + > + + + -const Breadcrumbs = ({path, rootProps, muiBreadcrumbsProps}: BreadcrumbsProps) => { - const theme = useTheme(); - const crumbs: CrumbData[] = getCrumbs(path) - - return ( - - - } - sx={{color: theme.palette.primary.contrastText}} - {...muiBreadcrumbsProps} - > - - - - - {crumbs.map((crumb, i, all) => { - if( i < all.length - 1 ) - return ( - - {crumb.name} - - ); - else { - return ( - {crumb.name} - ); - } - })} - - - - - ); + {crumbs.map((crumb, i, all) => { + if (i < all.length - 1) + return ( + + {crumb.name} + + ); + else { + return ( + + {crumb.name} + + ); + } + })} + + + + ); }; export { Breadcrumbs }; -export type { BreadcrumbsProps } \ No newline at end of file +export type { BreadcrumbsProps }; diff --git a/src/components/Footer.test.tsx b/src/components/Footer.test.tsx index c58e882..9bbf443 100644 --- a/src/components/Footer.test.tsx +++ b/src/components/Footer.test.tsx @@ -51,7 +51,7 @@ describe("Footer", () => { {lineOneText} - + , ); await waitFor(() => { @@ -74,7 +74,7 @@ describe("Footer", () => { {linkOneText} {linkTwoText} - + , ); await waitFor(() => { diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index fcb724b..792ebbf 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -133,7 +133,7 @@ const Navbar = ({ width: "100%", alignItems: "center", justifyContent: "space-between", - borderRadius:0 + borderRadius: 0, }} > = { title: "SciReactUI/Control/User", @@ -31,13 +31,13 @@ export const LoggedInLongName: Story = { export const LoggedInChangeColor: Story = { args: { color: "red", - user: { name: "Name Surname", fedid: "abc12345" } + user: { name: "Name Surname", fedid: "abc12345" }, }, }; export const LoggedInReplaceAvatar: Story = { args: { user: { name: "Name Surname", fedid: "abc12345" }, - avatar: JL + avatar: JL, }, }; diff --git a/src/components/User.test.tsx b/src/components/User.test.tsx index f174519..3e10498 100644 --- a/src/components/User.test.tsx +++ b/src/components/User.test.tsx @@ -5,84 +5,95 @@ import { Avatar } from "@mui/material"; import { User } from "./User"; describe("User", () => { - it("should render", () => { - render( {}} onLogout={() => {}} user={null} />); - render( {}} user={null} />); - render( {}} user={null} />); - render(); - }); - - it("should display login button when not authenticated", () => { - const {getByText} = render( {}} onLogout={() => {}} user={null} />); - const loginButton = getByText("Login"); - - expect(loginButton).toBeInTheDocument() - }); - - it("should display logout menuitem when authenticated", () => { - const {getByRole, getByText} = render( {}} onLogout={() => {}} user={{ name: "Name", fedid: "FedID" }} />); - - const userMenu = getByRole("button"); - fireEvent.click(userMenu); - - const logoutMenuItem = getByText("Logout"); - expect(logoutMenuItem).toBeInTheDocument(); - }); - - it("should fire login callback when button is clicked", () => { - const loginCallback = jest.fn(); - const {getByText} = render(); - - const loginButton = getByText("Login"); - fireEvent.click(loginButton); - - expect(loginCallback).toHaveBeenCalled(); - }); - - it("should fire logout callback when button is clicked", () => { - const logoutCallback = jest.fn(); - const {getByRole, getByText} = render( - , - ); - const userMenu = getByRole("button"); - fireEvent.click(userMenu); - - const logoutMenuItem = getByText("Logout"); - fireEvent.click(logoutMenuItem); - - expect(logoutCallback).toHaveBeenCalled(); - }); - - it("should display name and FedID", () => { - const name = "A Name", - fedId = "FED14000"; - const {getByText} = render(); - - expect(getByText(name)).toBeInTheDocument(); - expect(getByText(fedId)).toBeInTheDocument(); - }); - - it("should not have logout when no onLogout", () => { - const {getByRole, queryByText} = render( - , - ); - const userMenu = getByRole("button"); - fireEvent.click(userMenu); - - const logoutMenuItem = queryByText("Logout"); - expect(logoutMenuItem).not.toBeInTheDocument() - }); - - it("should render a new avatar", () => { - const avatarInitials = "MW" - const {queryByText} = render( - {avatarInitials}} - />, - ); - - const avatar = queryByText(avatarInitials); - expect(avatar).toBeInTheDocument() - }); - + it("should render", () => { + render( {}} onLogout={() => {}} user={null} />); + render( {}} user={null} />); + render( {}} user={null} />); + render(); + }); + + it("should display login button when not authenticated", () => { + const { getByText } = render( + {}} onLogout={() => {}} user={null} />, + ); + const loginButton = getByText("Login"); + + expect(loginButton).toBeInTheDocument(); + }); + + it("should display logout menuitem when authenticated", () => { + const { getByRole, getByText } = render( + {}} + onLogout={() => {}} + user={{ name: "Name", fedid: "FedID" }} + />, + ); + + const userMenu = getByRole("button"); + fireEvent.click(userMenu); + + const logoutMenuItem = getByText("Logout"); + expect(logoutMenuItem).toBeInTheDocument(); + }); + + it("should fire login callback when button is clicked", () => { + const loginCallback = jest.fn(); + const { getByText } = render(); + + const loginButton = getByText("Login"); + fireEvent.click(loginButton); + + expect(loginCallback).toHaveBeenCalled(); + }); + + it("should fire logout callback when button is clicked", () => { + const logoutCallback = jest.fn(); + const { getByRole, getByText } = render( + , + ); + const userMenu = getByRole("button"); + fireEvent.click(userMenu); + + const logoutMenuItem = getByText("Logout"); + fireEvent.click(logoutMenuItem); + + expect(logoutCallback).toHaveBeenCalled(); + }); + + it("should display name and FedID", () => { + const name = "A Name", + fedId = "FED14000"; + const { getByText } = render(); + + expect(getByText(name)).toBeInTheDocument(); + expect(getByText(fedId)).toBeInTheDocument(); + }); + + it("should not have logout when no onLogout", () => { + const { getByRole, queryByText } = render( + , + ); + const userMenu = getByRole("button"); + fireEvent.click(userMenu); + + const logoutMenuItem = queryByText("Logout"); + expect(logoutMenuItem).not.toBeInTheDocument(); + }); + + it("should render a new avatar", () => { + const avatarInitials = "MW"; + const { queryByText } = render( + {avatarInitials}} + />, + ); + + const avatar = queryByText(avatarInitials); + expect(avatar).toBeInTheDocument(); + }); }); diff --git a/src/components/User.tsx b/src/components/User.tsx index b665527..ee88c1b 100644 --- a/src/components/User.tsx +++ b/src/components/User.tsx @@ -1,9 +1,17 @@ // Adapted from https://github.com/DiamondLightSource/web-ui-components -import {Avatar, Button, Box, Link, Stack, Typography, useTheme } from "@mui/material"; +import { + Avatar, + Button, + Box, + Link, + Stack, + Typography, + useTheme, +} from "@mui/material"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; -import {ReactNode, useState} from "react"; +import { ReactNode, useState } from "react"; import { MdLogin } from "react-icons/md"; @@ -16,31 +24,31 @@ interface UserProps { user: AuthState | null; onLogin?: () => void; onLogout?: () => void; - avatar?: ReactNode, - color?: string + avatar?: ReactNode; + color?: string; } const User = ({ user, onLogin, onLogout, avatar, color }: UserProps) => { const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); - + const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; const handleClose = () => { setAnchorEl(null); }; - + const handleLogin = () => { - if(onLogin) onLogin() - } + if (onLogin) onLogin(); + }; const handleLogout = () => { - handleClose() - if(onLogout) onLogout() - } - + handleClose(); + if (onLogout) onLogout(); + }; + const theme = useTheme(); - + return ( <> @@ -59,47 +67,54 @@ const User = ({ user, onLogin, onLogout, avatar, color }: UserProps) => { }} > - {avatar || - - } + {avatar || ( + + )}
{user.name ? user.name : user.fedid} - {user.name && + {user.name ? user.name : user.fedid} + + {user.name && ( + {user.fedid}} + > + {user.fedid} + + )}
- {onLogout && - - Logout - - - } + {onLogout && ( + + + Logout + + + )} ) : ( @@ -116,5 +132,5 @@ const User = ({ user, onLogin, onLogout, avatar, color }: UserProps) => { ); }; -export { User } -export type { AuthState, UserProps } \ No newline at end of file +export { User }; +export type { AuthState, UserProps }; diff --git a/src/components/VisitInput.stories.tsx b/src/components/VisitInput.stories.tsx index 71b5a9f..c35faef 100644 --- a/src/components/VisitInput.stories.tsx +++ b/src/components/VisitInput.stories.tsx @@ -1,6 +1,6 @@ import { Meta, StoryObj } from "@storybook/react"; -import {VisitInput} from "./VisitInput"; +import { VisitInput } from "./VisitInput"; const meta: Meta = { title: "SciReactUI/Control/VisitInput", diff --git a/src/components/VisitInput.test.tsx b/src/components/VisitInput.test.tsx index 2f2f5a8..65bfa49 100644 --- a/src/components/VisitInput.test.tsx +++ b/src/components/VisitInput.test.tsx @@ -26,7 +26,7 @@ it("should not render submit button", () => { it("should produce visit and parameters on submit", () => { const onSubmit = jest.fn(); const { getByTestId } = render( - + , ); const visitField = within(getByTestId("visit-field")).getByRole("textbox"); fireEvent.change(visitField, { target: { value: "zz12345-7" } }); @@ -40,7 +40,7 @@ it("should produce visit and parameters on submit", () => { }, { fedid: "abc98765", - } + }, ); }); @@ -57,7 +57,7 @@ it("should produce visit on submit", () => { proposalNumber: 12345, number: 7, }, - undefined + undefined, ); }); @@ -68,7 +68,7 @@ it("should update visit on submit", () => { onSubmit={onSubmit} visit={{ proposalCode: "xx", proposalNumber: 98765, number: 4 }} parameters={{ fedid: "abc98765" }} - /> + />, ); const visitField = within(getByTestId("visit-field")).getByRole("textbox"); fireEvent.change(visitField, { target: { value: "zz12345-7" } }); @@ -82,6 +82,6 @@ it("should update visit on submit", () => { }, { fedid: "abc98765", - } + }, ); }); diff --git a/src/styles/colours.ts b/src/styles/colours.ts index c7d7bb8..f6d6b35 100644 --- a/src/styles/colours.ts +++ b/src/styles/colours.ts @@ -1,7 +1,6 @@ // Colours from https://github.com/DiamondLightSource/web-ui-components import { DiamondTheme } from "../themes/DiamondTheme"; - const colours = { diamond: { 50: { default: "#FBFBFB", _dark: "#525151" }, // white @@ -24,7 +23,7 @@ const colours = { secondaryLight: DiamondTheme.palette.secondary.light, secondaryDark: DiamondTheme.palette.secondary.dark, secondaryContrastText: DiamondTheme.palette.secondary.contrastText, - } + }, }; const fillColours = ["#ff5733", "#19D3FF", "#FF9B40", "#FF2677", "#FF9B40"]; diff --git a/src/styles/components.tsx b/src/styles/components.tsx index 0a9ebc7..a2656ad 100644 --- a/src/styles/components.tsx +++ b/src/styles/components.tsx @@ -1,6 +1,15 @@ +import React from "react"; import MuiButton from "@mui/material/Button"; -const Button = ({ customVariant = "default", ...props }) => { +interface ButtonProps { + customVariant?: string; + [key: string]: unknown; +} + +const Button: React.FC = ({ + customVariant = "default", + ...props +}) => { return ; }; diff --git a/src/themes/ThemeProvider.test.tsx b/src/themes/ThemeProvider.test.tsx index 9ada13a..9c06ef4 100644 --- a/src/themes/ThemeProvider.test.tsx +++ b/src/themes/ThemeProvider.test.tsx @@ -1,86 +1,97 @@ import "@testing-library/jest-dom"; -import {render} from "@testing-library/react"; +import { render } from "@testing-library/react"; -import {createTheme, Theme} from "@mui/material/styles"; +import { createTheme, Theme } from "@mui/material/styles"; -import {ThemeProvider} from "./ThemeProvider"; -import {BaseThemeOptions} from "./BaseTheme"; -import {GenericTheme} from "./GenericTheme"; -import {DiamondTheme} from "./DiamondTheme"; +import { ThemeProvider } from "./ThemeProvider"; +import { BaseThemeOptions } from "./BaseTheme"; +import { GenericTheme } from "./GenericTheme"; +import { DiamondTheme } from "./DiamondTheme"; +import Button from "@mui/material/Button"; - -jest.mock("@mui/material/CssBaseline", () => () => { - return
; +jest.mock("@mui/material/CssBaseline", () => { + const MockCssBaseline = () => { + return
; + }; + MockCssBaseline.displayName = "MockCssBaseline"; + return MockCssBaseline; }); const buttonText = "a test button", - testApp =
+ testApp = ( +

H1

Paragraph

Footer
+ ); describe("ThemeProvider Component", () => { - it("should render", () => { render(); }); - + it("should render with button", () => { - const {getByText} = render( - {testApp} - ); - - expect(getByText(buttonText)).toBeInTheDocument() + const { getByText } = render({testApp}); + + expect(getByText(buttonText)).toBeInTheDocument(); }); - + it("should render with generic theme", () => { - const {getByText} = render( - {testApp} - ); - - expect(getByText(buttonText)).toBeInTheDocument() + const { getByText } = render( + {testApp}, + ); + + expect(getByText(buttonText)).toBeInTheDocument(); }); - + it("should render with diamond theme", () => { - const {getByText} = render( - {testApp} - ); - - expect(getByText(buttonText)).toBeInTheDocument() + const { getByText } = render( + {testApp}, + ); + + expect(getByText(buttonText)).toBeInTheDocument(); }); - - + it("should render with a new theme", () => { const NewTheme: Theme = createTheme({ ...BaseThemeOptions, - }) - const {getByText} = render( - {testApp} - ); - - expect(getByText(buttonText)).toBeInTheDocument() + }); + const { getByText } = render( + {testApp}, + ); + expect(getByText(buttonText)).toBeInTheDocument(); }); -}) +}); describe("ThemeProvider CssBaseline Component", () => { - it("should render with CssBaseline", () => { - const {queryByTestId} = render(); - expect(queryByTestId("Mock_CssBaseline")).toBeInTheDocument() + const { queryByTestId } = render(); + expect(queryByTestId("Mock_CssBaseline")).toBeInTheDocument(); }); it("should render without CssBaseline", () => { - const {queryByTestId} = render(); - expect(queryByTestId("Mock_CssBaseline")).not.toBeInTheDocument() - }) - + const { queryByTestId } = render(); + expect(queryByTestId("Mock_CssBaseline")).not.toBeInTheDocument(); + }); + it("should render with app but without CssBaseline", () => { - const {getByText} = render( - {testApp} - ); - - expect(getByText(buttonText)).toBeInTheDocument() + const { getByText } = render( + {testApp}, + ); + + expect(getByText(buttonText)).toBeInTheDocument(); }); -}) \ No newline at end of file + + it("should render with button but without CssBaseline", async () => { + const buttonText = "A button"; + const { findByText } = render( + + + , + ); + + expect(await findByText(buttonText)); + }); +}); diff --git a/src/themes/ThemeProvider.tsx b/src/themes/ThemeProvider.tsx index 5f2545d..d4328d3 100644 --- a/src/themes/ThemeProvider.tsx +++ b/src/themes/ThemeProvider.tsx @@ -1,28 +1,26 @@ -import * as React from "react"; - import { ThemeProvider as Mui_ThemeProvider } from "@mui/material/styles"; -import {CssBaseline} from "@mui/material"; +import { CssBaseline } from "@mui/material"; import { GenericTheme } from "./GenericTheme"; -import {ThemeProviderProps as Mui_ThemeProviderProps} from "@mui/material/styles/ThemeProvider"; +import { ThemeProviderProps as Mui_ThemeProviderProps } from "@mui/material/styles/ThemeProvider"; interface ThemeProviderProps extends Partial { - baseline?: boolean + baseline?: boolean; } const ThemeProvider = function ({ - children, - theme = GenericTheme, - baseline = true, - defaultMode = "system", - ...props - }: ThemeProviderProps) { + children, + theme = GenericTheme, + baseline = true, + defaultMode = "system", + ...props +}: ThemeProviderProps) { return ( - - {baseline && } - {children} - + + {baseline && } + {children} + ); }; export { ThemeProvider }; -export type { ThemeProviderProps }; \ No newline at end of file +export type { ThemeProviderProps }; diff --git a/src/utils/diamond.test.ts b/src/utils/diamond.test.ts index bb8ca36..1eb65a8 100644 --- a/src/utils/diamond.test.ts +++ b/src/utils/diamond.test.ts @@ -1,6 +1,5 @@ import { visitRegex, Visit, visitToText, regexToVisit } from "./diamond"; describe("visitRegex", () => { - const validStrings = ["ab123456-78", "xy1-99", "cd123-456", "fg456789-2"]; validStrings.forEach((str) => { test(`should match valid string '${str}'`, () => { @@ -27,12 +26,12 @@ describe("visitRegex", () => { { str: "ab12-34-56", name: "extra number" }, ]; - invalidStrings.forEach(({ str, name }) => { - test(`should not match invalid string '${str}' (${name})`, () => { - const result = visitRegex.test(str); - expect(result).toBe(false); - }); + invalidStrings.forEach(({ str, name }) => { + test(`should not match invalid string '${str}' (${name})`, () => { + const result = visitRegex.test(str); + expect(result).toBe(false); }); + }); test("should get three correct groups", () => { const str = "ab12-34"; diff --git a/src/utils/diamond.ts b/src/utils/diamond.ts index 409bb5b..a5d189e 100644 --- a/src/utils/diamond.ts +++ b/src/utils/diamond.ts @@ -7,22 +7,20 @@ interface Visit { } const visitToText = (visit?: Visit): string => { - return( - visit - ? `${visit.proposalCode}${visit.proposalNumber.toFixed( - 0 - )}-${visit.number.toFixed(0)}` - : "") -} + return visit + ? `${visit.proposalCode}${visit.proposalNumber.toFixed( + 0, + )}-${visit.number.toFixed(0)}` + : ""; +}; const regexToVisit = (parsedVisit: RegExpExecArray): Visit => { - return ({ + return { proposalCode: parsedVisit[1], proposalNumber: Number(parsedVisit[2]), number: Number(parsedVisit[3]), - }) -} - + }; +}; export { regexToVisit, visitRegex, visitToText }; export type { Visit }; diff --git a/tsconfig.json b/tsconfig.json index 8f995ed..d561567 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,4 +28,4 @@ "src/types" ], "rootDir": "src" -} +} \ No newline at end of file From 85b0762f1a8920a1c618e01517748d67c79b8192 Mon Sep 17 00:00:00 2001 From: VictoriaBeilsten-Edmands Date: Thu, 2 Jan 2025 16:11:07 +0000 Subject: [PATCH 6/8] Get copyright year dynamically --- src/components/Footer.test.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Footer.test.tsx b/src/components/Footer.test.tsx index 9bbf443..ddbf4f2 100644 --- a/src/components/Footer.test.tsx +++ b/src/components/Footer.test.tsx @@ -17,12 +17,13 @@ describe("Footer", () => { test("Should render copyright only", async () => { const copyrightText = "add text here"; + const currentYear = new Date().getFullYear(); render(