diff --git a/src/app/[lang]/__tests__/card.test.tsx b/src/app/[lang]/__tests__/card.test.tsx
index 3ba6f57e..b0a57475 100644
--- a/src/app/[lang]/__tests__/card.test.tsx
+++ b/src/app/[lang]/__tests__/card.test.tsx
@@ -5,6 +5,7 @@ import Article from '../Article';
describe('Article component', () => {
it('should render correct element', () => {
+ // Arrange
const data: DataFrontmatter = {
id: 'test_id',
title: 'title test',
@@ -17,13 +18,11 @@ describe('Article component', () => {
tags: ['tag_A', 'tag_B'],
description: 'description test',
};
-
render();
-
const article = screen.getByRole('article');
const image = screen.getByRole('img');
const heading = screen.getByRole('heading');
-
+ // Assert
expect(article).toBeInTheDocument();
expect(image).toHaveAttribute('src', data.image);
expect(image).toHaveAttribute('alt', `${data.title} cover`);
diff --git a/src/app/[lang]/__tests__/footer.test.tsx b/src/app/[lang]/__tests__/footer.test.tsx
index 089cbbad..f64fe575 100644
--- a/src/app/[lang]/__tests__/footer.test.tsx
+++ b/src/app/[lang]/__tests__/footer.test.tsx
@@ -3,12 +3,11 @@ import Footer from '../Footer';
describe('Footer component', () => {
it('should render correct element', () => {
+ // Arrange
const footerCopyright = 'footer copyright';
-
render();
-
const footer = screen.getByRole('contentinfo');
-
+ // Assert
expect(footer).toBeInTheDocument();
expect(footer).toHaveTextContent(footerCopyright);
});
diff --git a/src/app/[lang]/__tests__/header.test.tsx b/src/app/[lang]/__tests__/header.test.tsx
index fdb2fb49..f17bbe08 100644
--- a/src/app/[lang]/__tests__/header.test.tsx
+++ b/src/app/[lang]/__tests__/header.test.tsx
@@ -1,89 +1,76 @@
import { act, render, screen, waitFor } from '@testing-library/react';
import Header, { Avatar } from '../Header';
+const scrollDownTo = (to: number) => {
+ act(() => {
+ window.scrollY = to - 1;
+ window.dispatchEvent(new Event('scroll'));
+ window.scrollY = to;
+ window.dispatchEvent(new Event('scroll'));
+ });
+}
+const scrollUpTo = (to: number) => {
+ act(() => {
+ window.scrollY = to + 1;
+ window.dispatchEvent(new Event('scroll'));
+ window.scrollY = to;
+ window.dispatchEvent(new Event('scroll'));
+ });
+}
+
describe('Header component', () => {
const avatar = (
);
it('should render correct element', () => {
+ // Arrange
render();
-
const brandLink = screen.getByRole('img');
-
+ // Assert
expect(brandLink).toBeInTheDocument();
expect(brandLink.parentElement).toHaveAttribute('href', '/');
});
it('should hide header on scroll down and show on scroll up', async () => {
+ // Arrange
render();
-
const header = screen.getByRole('banner');
-
expect(header.tagName).toBe('HEADER');
- act(() => {
- window.scrollY = 19;
- window.dispatchEvent(new Event('scroll'));
- window.scrollY = 20;
- window.dispatchEvent(new Event('scroll'));
- });
-
+ // Act
+ scrollDownTo(20);
+ // Assert
await waitFor(() => {
expect(header).toHaveStyle({ '--header-translate-y': '0px' });
});
-
- act(() => {
- window.scrollY = 98;
- window.dispatchEvent(new Event('scroll'));
- window.scrollY = 99;
- window.dispatchEvent(new Event('scroll'));
- });
-
+ // Act
+ scrollDownTo(99);
+ // Assert
await waitFor(() => {
expect(header).toHaveStyle({ '--header-translate-y': '0px' });
});
-
- act(() => {
- window.scrollY = 149;
- window.dispatchEvent(new Event('scroll'));
- window.scrollY = 150;
- window.dispatchEvent(new Event('scroll'));
- });
-
+ // Act
+ scrollDownTo(150);
+ // Assert
await waitFor(() => {
expect(header).toHaveStyle({ '--header-translate-y': '50px' });
});
-
- act(() => {
- window.scrollY = 299;
- window.dispatchEvent(new Event('scroll'));
- window.scrollY = 300;
- window.dispatchEvent(new Event('scroll'));
- });
-
+ // Act
+ scrollDownTo(800);
+ // Assert
await waitFor(() => {
expect(header).toHaveStyle({ '--header-translate-y': '50px' });
});
-
- act(() => {
- window.scrollY = 501;
- window.dispatchEvent(new Event('scroll'));
- window.scrollY = 500;
- window.dispatchEvent(new Event('scroll'));
- });
-
+ // Act
+ scrollUpTo(500);
+ // Assert
await waitFor(() => {
expect(header).toHaveStyle({ '--header-translate-y': '300px' });
});
-
- act(() => {
- window.scrollY = 1;
- window.dispatchEvent(new Event('scroll'));
- window.scrollY = 0;
- window.dispatchEvent(new Event('scroll'));
- });
-
+ // Act
+ scrollUpTo(0);
+ // Assert
await waitFor(() => {
expect(header).toHaveStyle({ '--header-translate-y': '300px' });
});
diff --git a/src/app/[lang]/__tests__/layout.test.tsx b/src/app/[lang]/__tests__/layout.test.tsx
index 63ac307a..e4a8c775 100644
--- a/src/app/[lang]/__tests__/layout.test.tsx
+++ b/src/app/[lang]/__tests__/layout.test.tsx
@@ -3,24 +3,21 @@ import { locales } from '~/i18n';
import mockPathname from '~/tests/navigation';
import Layout, { generateMetadata, generateStaticParams } from '../layout';
-describe('I18n layout component', () => {
+describe('I18n layout', () => {
it('should render correct element', async () => {
+ // Arrange
const testText = 'Test layout component';
-
mockPathname.mockReturnValueOnce('/');
-
const layout = await Layout({
children:
+
{children}
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx
index c9795ef4..99678784 100644
--- a/src/app/not-found.tsx
+++ b/src/app/not-found.tsx
@@ -1,12 +1,17 @@
import type { Metadata } from 'next';
-import NotFound from '@/components/NotFound';
+import { H2 } from '@/components/Heading';
export const metadata: Metadata = {
title: 'Page not found',
};
function NotFoundPage() {
- return
;
+ return (
+
+
Page not found
+
Sorry, the page you are looking for does not exist.
+
+ );
}
export default NotFoundPage;
diff --git a/src/components/CodeBox/codeBox.test.tsx b/src/components/CodeBox/codeBox.test.tsx
index d20eed8c..88e57268 100644
--- a/src/components/CodeBox/codeBox.test.tsx
+++ b/src/components/CodeBox/codeBox.test.tsx
@@ -5,13 +5,12 @@ import CodeBox from '.';
describe('CodeBox component', () => {
it('should render correct element', () => {
- const code = 'Hello world!';
-
+ // Arrange
+ const code = 'should render correct element';
render(
{code});
-
const copyButton = screen.getByRole('button');
const codeBox = screen.getByText(code);
-
+ // Assert
expect(copyButton).toBeInTheDocument();
expect(codeBox).toBeInTheDocument();
expect(codeBox).toHaveTextContent(code);
@@ -19,27 +18,23 @@ describe('CodeBox component', () => {
});
it('should correctly copy the text when clicked', async () => {
+ // Arrange
Object.assign(navigator, {
clipboard: {
writeText: jest.fn().mockImplementation(() => Promise.resolve()),
},
});
-
- const code = 'Hello world!';
-
+ const code = 'should correctly copy the text when clicked';
render(
{code});
-
const copyButton = screen.getByRole('button');
-
expect(screen.queryByText('複製成功!')).toHaveClass('opacity-0');
-
+ // Act
await userEvent.click(copyButton);
await userEvent.click(copyButton);
-
+ // Assert
expect(screen.queryByText('複製成功!')).not.toHaveClass('opacity-0');
expect(navigator.clipboard.writeText).toBeCalledTimes(2);
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(code);
-
await waitFor(
() => {
expect(screen.queryByText('複製成功!')).toHaveClass('opacity-0');
diff --git a/src/components/Comment/comment.test.tsx b/src/components/Comment/comment.test.tsx
index d8d5559e..a586c67c 100644
--- a/src/components/Comment/comment.test.tsx
+++ b/src/components/Comment/comment.test.tsx
@@ -17,15 +17,13 @@ jest.mock('next-themes', () => ({
describe('Comment component', () => {
it.each(['light', 'dark'])('should render correct element', async (theme) => {
+ // Arrange
mockUseTheme.mockReturnValue({ resolvedTheme: theme });
-
render(
);
-
const comment = screen.getByTestId('comment');
const expectedProps: GiscusProps = JSON.parse(JSON.stringify(giscusConfigs));
-
expectedProps.theme = `noborder_${theme}`;
-
+ // Assert
expect(comment).toBeInTheDocument();
expect(getGiscusProps).toHaveBeenCalledWith(expectedProps);
});
diff --git a/src/components/Container/container.test.tsx b/src/components/Container/container.test.tsx
index a6da061d..99c05f43 100644
--- a/src/components/Container/container.test.tsx
+++ b/src/components/Container/container.test.tsx
@@ -7,10 +7,10 @@ describe('Container component', () => {
[undefined, 'DIV'],
['main', 'MAIN'],
] as const)('should render correct element', async (as, expectedTagName) => {
+ // Arrange
render(
);
-
const container = screen.getByTestId('container');
-
+ // Assert
expect(container).toBeInTheDocument();
expect(container.tagName).toBe(expectedTagName);
});
diff --git a/src/components/Heading/heading.test.tsx b/src/components/Heading/heading.test.tsx
index 22db7928..76f3150f 100644
--- a/src/components/Heading/heading.test.tsx
+++ b/src/components/Heading/heading.test.tsx
@@ -4,12 +4,11 @@ import Heading, { H1, H2, H3, H4, H5, H6 } from '.';
describe('Heading component', () => {
it('should render correct element', () => {
+ // Arrange
const name = 'The heading text';
-
render(
{name});
-
const heading = screen.getByRole('heading');
-
+ // Assert
expect(heading).toBeInTheDocument();
expect(heading).toHaveTextContent(name);
expect(heading.tagName).toBe('H2');
@@ -23,12 +22,11 @@ describe('Heading component', () => {
[H5, 'H5'],
[H6, 'H6'],
])('should render correct tag name', (Component, expected) => {
+ // Arrange
const name = `The ${expected} tag`;
-
render(
{name});
-
const heading = screen.getByRole('heading');
-
+ // Assert
expect(heading).toHaveTextContent(name);
expect(heading.tagName).toBe(expected);
});
diff --git a/src/components/Image/image.test.tsx b/src/components/Image/image.test.tsx
index 80c52b88..a6b356c2 100644
--- a/src/components/Image/image.test.tsx
+++ b/src/components/Image/image.test.tsx
@@ -6,29 +6,28 @@ describe('Image component', () => {
it.each([
{
alt: 'Test image without specific dimensions',
+ parentTagName: 'PICTURE',
},
{
width: 100,
height: 100,
alt: 'Test image with specific dimensions',
+ parentTagName: 'DIV',
},
{
width: 100,
height: 100,
alt: 'Test image with base64',
+ parentTagName: 'DIV',
base64:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP87wMAAlABTQluYBcAAAAASUVORK5CYII=',
},
- ])('should render correct element', ({ alt, ...props }) => {
- render(
);
-
- const checkParentElement = props.width === undefined;
-
+ ])('should render correct element', ({ alt, parentTagName, ...props }) => {
+ // Arrange
+ render(
);
const image = screen.getByAltText(alt);
+ // Assert
expect(image.tagName).toBe('IMG');
-
- if (checkParentElement) {
- expect(image.parentElement?.tagName).toBe('PICTURE');
- }
+ expect(image.parentElement?.tagName).toBe(parentTagName);
});
});
diff --git a/src/components/Link/link.test.tsx b/src/components/Link/link.test.tsx
index e54b3d80..19787270 100644
--- a/src/components/Link/link.test.tsx
+++ b/src/components/Link/link.test.tsx
@@ -11,10 +11,10 @@ describe('Link component', () => {
['https://external.com', 'The external link text', 'https://external.com'],
[{ pathname: '/internal' }, 'The object href text', '/internal'],
])('should render correct element', (href, name, expectedHref) => {
+ // Arrange
render(
{name});
-
const link = screen.getByRole('link', { name });
-
+ // Assert
expect(link).toBeInTheDocument();
expect(link).toHaveTextContent(name);
expect(link).toHaveAttribute('href', expectedHref);
@@ -29,15 +29,13 @@ describe('Link component', () => {
])(
'should render correct link element with pathname %s',
(pathname, expected) => {
+ // Arrange
const name = 'internal link';
const href = '/internal';
-
mockPathname.mockReturnValueOnce(pathname);
-
render(
{name});
-
const link = screen.getByRole('link', { name });
-
+ // Assert
expect(link).toHaveAttribute('href', expected);
}
);
diff --git a/src/components/List/list.test.tsx b/src/components/List/list.test.tsx
index 9e43ec0c..21cf6a2e 100644
--- a/src/components/List/list.test.tsx
+++ b/src/components/List/list.test.tsx
@@ -3,17 +3,14 @@ import List from '.';
describe('List component', () => {
it('should render correct element', () => {
+ // Arrange
const items = [
{ id: 'id_1', text: 'text_1' },
{ id: 'id_2', text: 'text_2' },
];
-
- const Component = (data: (typeof items)[number]) =>
{data.text}
;
-
- render(
);
-
+ render(
data.text} items={items} />);
const list = screen.getByRole('list');
-
+ // Assert
expect(list).toBeInTheDocument();
expect(list.childNodes.length).toBe(items.length);
expect(list).toHaveTextContent(items[0].text);
diff --git a/src/components/NotFound/NotFound.tsx b/src/components/NotFound/NotFound.tsx
deleted file mode 100644
index 396a927a..00000000
--- a/src/components/NotFound/NotFound.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-'use client';
-
-import { Dictionary, getDictionary } from '~/i18n';
-import useI18n from '@/hooks/useI18n';
-import { H1 } from '../Heading';
-import { useEffect, useState } from 'react';
-
-function NotFound() {
- const { lang } = useI18n();
- const [dictionary, setDictionary] = useState(null);
-
- useEffect(() => {
- getDictionary(lang).then(setDictionary);
- }, [lang]);
-
- if (!dictionary) return null;
-
- return (
-
-
{dictionary.notFound.message}
-
- );
-}
-
-export default NotFound;
diff --git a/src/components/NotFound/index.tsx b/src/components/NotFound/index.tsx
deleted file mode 100644
index 2d0c4855..00000000
--- a/src/components/NotFound/index.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-import NotFound from './NotFound';
-
-export default NotFound;
diff --git a/src/components/NotFound/notFound.test.tsx b/src/components/NotFound/notFound.test.tsx
deleted file mode 100644
index ecd411f9..00000000
--- a/src/components/NotFound/notFound.test.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { render, screen } from '@testing-library/react';
-
-import mockPathname from '~/tests/navigation';
-import en from '~/i18n/locales/en';
-import NotFound from '.';
-
-describe('NotFound component', () => {
- it('should render correct element', async () => {
- mockPathname.mockReturnValueOnce('/en/test/path');
-
- render();
-
- const heading = await screen.findByRole('heading');
-
- expect(heading).toBeInTheDocument();
- expect(heading).toHaveTextContent(en.notFound.message);
- });
-});
diff --git a/src/components/TableOfContents/tableOfContents.test.tsx b/src/components/TableOfContents/tableOfContents.test.tsx
index 1f494d56..45512d01 100644
--- a/src/components/TableOfContents/tableOfContents.test.tsx
+++ b/src/components/TableOfContents/tableOfContents.test.tsx
@@ -27,8 +27,8 @@ describe('TableOfContents component', () => {
});
it('should render correct element', () => {
+ // Arrange
const source = '# H1\n## H2_1\n### H3_1-1\n## H2_2\n### H3_2-1\n';
-
mockObserverCallback.mockReturnValueOnce([
{
isIntersecting: false,
@@ -39,12 +39,10 @@ describe('TableOfContents component', () => {
target: { id: 'H3_2-1' },
},
])
-
render();
-
const toc = screen.getByRole('navigation');
const anchorLinks = screen.getAllByRole('link');
-
+ // Assert
expect(toc).toBeInTheDocument();
expect(anchorLinks.length).toBe(4);
expect(anchorLinks[0]).toHaveAttribute('href', '#H2_1');
diff --git a/src/components/ThemeSwitcher/index.tsx b/src/components/ThemeSwitcher/index.tsx
index 4cc3324c..ab35c909 100644
--- a/src/components/ThemeSwitcher/index.tsx
+++ b/src/components/ThemeSwitcher/index.tsx
@@ -1,3 +1,3 @@
-import ThemeSwitcherProps from './ThemeSwitcher';
+import ThemeSwitcher from './ThemeSwitcher';
-export default ThemeSwitcherProps;
+export default ThemeSwitcher;
diff --git a/src/components/ThemeSwitcher/themeSwitcher.test.tsx b/src/components/ThemeSwitcher/themeSwitcher.test.tsx
index 75c59f85..087f5d87 100644
--- a/src/components/ThemeSwitcher/themeSwitcher.test.tsx
+++ b/src/components/ThemeSwitcher/themeSwitcher.test.tsx
@@ -17,30 +17,28 @@ function TestThemeComponent({ children }: PropsWithChildren) {
describe('ThemeSwitch component', () => {
it('should render correct element', async () => {
+ // Arrange
setDeviceTheme('light');
-
render();
-
const button = screen.getByRole('button');
-
+ // Assert
expect(button).toBeInTheDocument();
});
it('should correctly switch the theme when clicked', async () => {
+ // Arrange
setDeviceTheme('dark');
-
render();
-
const button = screen.getByRole('button');
-
+ // Act
await userEvent.click(button);
-
+ // Assert
expect(document.documentElement.getAttribute('style')).toBe(
'color-scheme: light;'
);
-
+ // Act
await userEvent.click(button);
-
+ // Assert
expect(document.documentElement.getAttribute('style')).toBe(
'color-scheme: dark;'
);
diff --git a/src/hooks/__tests__/useAutoReset.test.ts b/src/hooks/__tests__/useAutoReset.test.ts
index d1095835..f12c92b6 100644
--- a/src/hooks/__tests__/useAutoReset.test.ts
+++ b/src/hooks/__tests__/useAutoReset.test.ts
@@ -3,16 +3,16 @@ import useAutoReset from '../useAutoReset';
describe('useAutoReset hook', () => {
it('should reset state to initial value after a specified delay', async () => {
+ // Arrange
const initialValue = false;
const newValue = true;
const { result } = renderHook(() => useAutoReset(initialValue));
-
+ // Assert
expect(result.current[0]).toBe(initialValue);
-
+ // Act
act(() => result.current[1](newValue));
-
+ // Assert
expect(result.current[0]).toBe(newValue);
-
await waitFor(
() => expect(result.current[0]).toBe(initialValue),
{ timeout: 2000 }
diff --git a/src/hooks/__tests__/useI18n.test.ts b/src/hooks/__tests__/useI18n.test.ts
index c2119fdb..2cc02924 100644
--- a/src/hooks/__tests__/useI18n.test.ts
+++ b/src/hooks/__tests__/useI18n.test.ts
@@ -11,10 +11,10 @@ describe('useI18n hook', () => {
])(
'should return the correct language code and dictionary',
(pathname, expected) => {
+ // Arrange
mockPathname.mockReturnValueOnce(pathname);
-
const { result } = renderHook(() => useI18n());
-
+ // Assert
expect(result.current.lang).toBe(expected);
}
);
diff --git a/src/hooks/__tests__/useIntersectionObserver.test.ts b/src/hooks/__tests__/useIntersectionObserver.test.ts
index a7756535..5ebcf5da 100644
--- a/src/hooks/__tests__/useIntersectionObserver.test.ts
+++ b/src/hooks/__tests__/useIntersectionObserver.test.ts
@@ -21,15 +21,15 @@ describe('useIntersectionObserver hook', () => {
});
it('should observe elements when elementRef is provided', () => {
+ // Arrange
const elementRef = {
current: [
document.createElement('h2'),
document.createElement('h2')
]
}
-
renderHook(() => useIntersectionObserver({ elementRef }));
-
+ // Assert
expect(mockIntersectionObserver).toBeCalledTimes(1);
expect(mockObserve).toBeCalledTimes(2);
expect(mockObserve).toBeCalledWith(elementRef.current[0]);
@@ -37,27 +37,27 @@ describe('useIntersectionObserver hook', () => {
});
it('should observe and disconnect elements when setElementRef is called', () => {
+ // Arrange
const elementRef = {
current: [
document.createElement('h2'),
document.createElement('h2')
]
}
-
const { result } = renderHook(() => useIntersectionObserver());
-
+ // Assert
expect(mockIntersectionObserver).toBeCalledTimes(1);
expect(mockObserve).toBeCalledTimes(0);
-
+ // Act
act(() => result.current[1](elementRef.current));
-
+ // Assert
expect(mockDisconnect).toBeCalledTimes(0);
expect(mockObserve).toBeCalledTimes(2);
expect(mockObserve).toBeCalledWith(elementRef.current[0]);
expect(mockObserve).toHaveBeenLastCalledWith(elementRef.current[1]);
-
+ // Act
act(() => result.current[1](elementRef.current[0]));
-
+ // Assert
expect(mockDisconnect).toBeCalledTimes(1);
expect(mockObserve).toBeCalledTimes(3);
expect(mockObserve).toHaveBeenLastCalledWith(elementRef.current[0]);
diff --git a/src/hooks/__tests__/useMounted.test.ts b/src/hooks/__tests__/useMounted.test.ts
index 549852e9..82743822 100644
--- a/src/hooks/__tests__/useMounted.test.ts
+++ b/src/hooks/__tests__/useMounted.test.ts
@@ -3,8 +3,9 @@ import useIsMounted from '../useIsMounted';
describe('useIsMounted hook', () => {
it('should return true if component is mounted', () => {
+ // Arrange
const { result } = renderHook(() => useIsMounted());
-
+ // Assert
expect(result.current).toBeTruthy();
});
});
diff --git a/src/hooks/__tests__/useScroll.test.ts b/src/hooks/__tests__/useScroll.test.ts
index 3f204d2a..ff81679d 100644
--- a/src/hooks/__tests__/useScroll.test.ts
+++ b/src/hooks/__tests__/useScroll.test.ts
@@ -10,17 +10,23 @@ describe('useScroll hook', () => {
scrollHandler.mockClear();
});
- it('should call the scroll handler when window is scrolled', async () => {
+ it('should not call the scroll handler initially', () => {
+ // Arrange
renderHook(() => useScroll({ handler: scrollHandler }));
-
+ // Assert
expect(scrollHandler).not.toBeCalled();
+ });
+ it('should call the scroll handler when window is scrolled', async () => {
+ // Arrange
+ renderHook(() => useScroll({ handler: scrollHandler }));
+ // Act
act(() => {
window.scrollX = 100;
window.scrollY = 200;
window.dispatchEvent(new Event('scroll'));
});
-
+ // Assert
await waitFor(() => {
expect(scrollHandler).toBeCalled();
expect(scrollHandler).toBeCalledTimes(1);
@@ -29,23 +35,21 @@ describe('useScroll hook', () => {
});
it('should call the scroll handler when a specified element is scrolled', async () => {
+ // Arrange
const element = document.createElement('div');
-
renderHook(() =>
useScroll({
ref: { current: element },
handler: scrollHandler,
})
);
-
- expect(scrollHandler).not.toBeCalled();
-
+ // Act
act(() => {
element.scrollLeft = 150;
element.scrollTop = 250;
element.dispatchEvent(new Event('scroll'));
});
-
+ // Assert
await waitFor(() => {
expect(scrollHandler).toBeCalled();
expect(scrollHandler).toBeCalledTimes(1);
@@ -54,90 +58,91 @@ describe('useScroll hook', () => {
});
it('should switch the element being listened to for scrolling when using register', async () => {
+ // Arrange
const element = document.createElement('main');
const { result } = renderHook(() =>
useScroll({
handler: scrollHandler,
})
);
-
+ // Act
act(() => {
window.scrollX = 0;
window.scrollY = 80;
window.dispatchEvent(new Event('scroll'));
});
-
+ // Assert
await waitFor(() => {
expect(scrollHandler).toBeCalled();
expect(scrollHandler).toBeCalledTimes(1);
- expect(scrollHandler).toBeCalledWith({ x: 0, y: 80 }, window);
+ expect(scrollHandler).toHaveBeenLastCalledWith({ x: 0, y: 80 }, window);
});
-
+ // Act
act(() => {
result.current.register(element);
element.scrollLeft = 180;
element.scrollTop = 280;
element.dispatchEvent(new Event('scroll'));
});
-
+ // Assert
await waitFor(() => {
- expect(scrollHandler).toBeCalled();
expect(scrollHandler).toBeCalledTimes(2);
- expect(scrollHandler).toBeCalledWith({ x: 180, y: 280 }, element);
+ expect(scrollHandler).toHaveBeenLastCalledWith(
+ { x: 180, y: 280 },
+ element
+ );
});
});
it('should stop listening to scroll events when remove is called', async () => {
+ // Arrange
const { result } = renderHook(() =>
useScroll({
handler: scrollHandler,
})
);
-
- expect(scrollHandler).not.toBeCalled();
-
+ // Act
act(() => {
result.current.remove();
window.scrollX = 100;
window.scrollY = 0;
window.dispatchEvent(new Event('scroll'));
});
-
+ // Assert
await waitFor(() => {
expect(scrollHandler).not.toBeCalled();
});
});
it('should stop listening to scroll events when the component is unmounted', async () => {
+ // Arrange
const { unmount } = renderHook(() =>
useScroll({
handler: scrollHandler,
})
);
-
- expect(scrollHandler).not.toBeCalled();
-
+ // Act
unmount();
-
act(() => {
window.scrollX = 100;
window.scrollY = 0;
window.dispatchEvent(new Event('scroll'));
});
-
+ // Assert
await waitFor(() => {
expect(scrollHandler).not.toBeCalled();
});
});
it('should trigger the scroll handler on initial render', async () => {
+ // Arrange
renderHook(() =>
useScroll({
handler: scrollHandler,
initial: true,
})
);
-
+ // Assert
await waitFor(() => {
expect(scrollHandler).toBeCalled();
expect(scrollHandler).toBeCalledTimes(1);
diff --git a/src/plugins/__tests__/rehypeImageMetadata.test.ts b/src/plugins/__tests__/rehypeImageMetadata.test.ts
index 9742d2d3..84f2de97 100644
--- a/src/plugins/__tests__/rehypeImageMetadata.test.ts
+++ b/src/plugins/__tests__/rehypeImageMetadata.test.ts
@@ -32,6 +32,7 @@ describe('Rehype image metadata function', () => {
});
it('should format paragraph with image element to a div', async () => {
+ // Arrange
const testImageNode: ImageNode = {
type: 'element',
tagName: 'img',
@@ -44,15 +45,17 @@ describe('Rehype image metadata function', () => {
tagName: 'p',
children: [testImageNode],
};
+ // Act
const transformer = rehypeImageMetadata();
const result = await transformer(testTree);
-
+ // Assert
expect(result.tagName).toBe('div');
});
it.each(['/test.jpeg', '/public/test.jpeg'])(
'should set image properties for src: %s',
async (src) => {
+ // Arrange
const width = 2;
const height = 1;
const fileType = 'png';
@@ -67,16 +70,20 @@ describe('Rehype image metadata function', () => {
info: { format: fileType },
data: base64,
});
+ // Act
const transformer = rehypeImageMetadata();
const result = await transformer(testImageNode);
-
- expect(result.tagName).toBe(testImageNode.tagName);
- expect(result.properties.src).toBe(testImageNode.properties.src);
- expect(result.properties.width).toBe(width);
- expect(result.properties.height).toBe(height);
- expect(result.properties.base64).toBe(
- `data:image/${fileType};base64,${base64}`
- );
+ // Assert
+ expect(result).toStrictEqual({
+ type: 'element',
+ tagName: testImageNode.tagName,
+ properties: {
+ src: testImageNode.properties.src,
+ width,
+ height,
+ base64: `data:image/${fileType};base64,${base64}`,
+ },
+ });
}
);
});
diff --git a/src/utils/__tests__/clipboard.test.ts b/src/utils/__tests__/clipboard.test.ts
index dfbce4e4..ac493e89 100644
--- a/src/utils/__tests__/clipboard.test.ts
+++ b/src/utils/__tests__/clipboard.test.ts
@@ -26,26 +26,30 @@ describe('Copy to clipboard function', () => {
});
it('should return error with reject', async () => {
+ // Arrange
expect.assertions(1);
-
- await expect(copyToClipboard('test')).rejects.toStrictEqual(
- TypeError('document.execCommand is not a function')
- );
+ const expected = TypeError('document.execCommand is not a function');
+ // Act
+ const result = copyToClipboard('test');
+ // Assert
+ await expect(result).rejects.toStrictEqual(expected);
});
it('should call document execCommand', () => {
+ // Arrange
setDeviceClipboard('old');
-
+ // Act
copyToClipboard('test');
-
+ // Assert
expect(document.execCommand).toBeCalled();
});
it('should call navigator clipboard writeText', () => {
+ // Arrange
setDeviceClipboard('new');
-
+ // Act
copyToClipboard('test');
-
+ // Assert
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('test');
});
});
diff --git a/src/utils/__tests__/cn.test.ts b/src/utils/__tests__/cn.test.ts
index db64d979..c4647f00 100644
--- a/src/utils/__tests__/cn.test.ts
+++ b/src/utils/__tests__/cn.test.ts
@@ -2,8 +2,13 @@ import cn from '../cn';
describe('TailwindCSS className merge function', () => {
it('should return current className', () => {
- const className = cn('bg-blue-500 text-white/90', { 'bg-red-500': true });
-
- expect(className).toBe('text-white/90 bg-red-500');
+ // Arrange
+ const baseClassName = 'bg-blue-500 text-white/90';
+ const mergeClassName = { 'bg-red-500': true };
+ const expected = 'text-white/90 bg-red-500';
+ // Act
+ const className = cn(baseClassName, mergeClassName);
+ // Assert
+ expect(className).toBe(expected);
});
});
diff --git a/src/utils/__tests__/date.test.ts b/src/utils/__tests__/date.test.ts
index 931b6b93..3d5b4497 100644
--- a/src/utils/__tests__/date.test.ts
+++ b/src/utils/__tests__/date.test.ts
@@ -2,26 +2,29 @@ import { compareDates, formatDate } from '../date';
describe('Format date function', () => {
it('should display the date format in Taiwan', () => {
- const date = formatDate(new Date('2023/7/8'));
-
- expect(date).toBe('2023年7月8日');
+ // Arrange
+ const date = new Date('2023/7/8');
+ const expected = '2023年7月8日';
+ // Act
+ const result = formatDate(date);
+ // Assert
+ expect(result).toBe(expected);
});
it('should display the date format in American', () => {
- const date = formatDate('2023/7/8', 'en-us');
-
- expect(date).toBe('July 8, 2023');
+ // Arrange
+ const date = '2023/7/8';
+ const expected = 'July 8, 2023';
+ // Act
+ const result = formatDate(date, 'en-us');
+ // Assert
+ expect(result).toBe(expected);
});
});
describe('Compare dates function', () => {
- it('should return correct relative order', () => {
- expect(compareDates('2023/12/25', '2023/1/1')).toBe(-1);
- expect(compareDates('2023/10/10', '2023/12/31')).toBe(1);
- expect(compareDates('2023/2/28', '2023/2/28')).toBe(1);
- });
-
- it('should sort dates in ascending and descending order', () => {
+ it('should sort dates in descending order', () => {
+ // Arrange
const dates: DateOrDateString[] = [
'2023/2/2',
'2023/11/1',
@@ -29,17 +32,39 @@ describe('Compare dates function', () => {
'2023/10/1',
'2023/1/1',
];
- const descendingExpected = [
+ const expected = [
'2023/11/1',
'2023/10/1',
'2023/2/2',
'2023/2/1',
'2023/1/1',
];
-
- expect(dates.sort(compareDates)).toStrictEqual(descendingExpected);
- expect(
- dates.sort((date1, date2) => compareDates(date1, date2, true))
- ).toStrictEqual(descendingExpected.concat().reverse());
+ // Act
+ const result = dates.sort(compareDates);
+ // Assert
+ expect(result).toStrictEqual(expected);
+ });
+ it('should sort dates in ascending order', () => {
+ // Arrange
+ const dates: DateOrDateString[] = [
+ '2023/2/2',
+ '2023/11/1',
+ '2023/2/1',
+ '2023/10/1',
+ '2023/1/1',
+ ];
+ const expected = [
+ '2023/1/1',
+ '2023/2/1',
+ '2023/2/2',
+ '2023/10/1',
+ '2023/11/1',
+ ];
+ // Act
+ const result = dates.sort((date1, date2) =>
+ compareDates(date1, date2, true)
+ );
+ // Assert
+ expect(result).toStrictEqual(expected);
});
});
diff --git a/src/utils/__tests__/generateRSS.test.ts b/src/utils/__tests__/generateRSS.test.ts
index 54dde271..17eb2063 100644
--- a/src/utils/__tests__/generateRSS.test.ts
+++ b/src/utils/__tests__/generateRSS.test.ts
@@ -4,6 +4,7 @@ import { defaultLocale } from '~/i18n';
import generateRSS, { PUBLIC_FEED_PATH } from '../generateRSS';
const mockWriteFile = jest.fn();
+const mockAtom1 = (options: unknown) => `atom1 - ${JSON.stringify(options)}`;
jest.mock('feed', () => ({
Feed: class MockFeed {
@@ -11,7 +12,7 @@ jest.mock('feed', () => ({
constructor(options: FeedOptions) {
this._options = options;
}
- atom1 = () => `atom1 - ${JSON.stringify(this._options)}`;
+ atom1 = () => mockAtom1(this._options);
},
}));
@@ -27,35 +28,41 @@ describe('Generate RSS function', () => {
});
it('should generate the rss for the specified language', () => {
+ // Arrange
const testFeedOptions: FeedOptions = {
id: 'http://test.generate.rss',
title: 'test generate RSS',
copyright: `Copyright © ${new Date().getFullYear()}`,
language: 'en',
};
-
+ const expectedFileName = 'atom.en.xml';
+ const expectedFileContent = mockAtom1(testFeedOptions);
+ // Act
generateRSS(testFeedOptions);
-
+ // Assert
expect(mockWriteFile).toBeCalledTimes(1);
expect(mockWriteFile).toBeCalledWith(
- path.join(PUBLIC_FEED_PATH, 'atom.en.xml'),
- `atom1 - ${JSON.stringify(testFeedOptions)}`
+ path.join(PUBLIC_FEED_PATH, expectedFileName),
+ expectedFileContent
);
});
it('should generate the rss for the default language', () => {
+ // Arrange
const testFeedOptions: FeedOptions = {
id: 'http://test.generate.rss',
title: 'test generate RSS',
copyright: `Copyright © ${new Date().getFullYear()}`,
};
-
+ const expectedFileName = `atom.${defaultLocale}.xml`;
+ const expectedFileContent = mockAtom1(testFeedOptions);
+ // Act
generateRSS(testFeedOptions);
-
+ // Assert
expect(mockWriteFile).toBeCalledTimes(1);
expect(mockWriteFile).toBeCalledWith(
- path.join(PUBLIC_FEED_PATH, `atom.${defaultLocale}.xml`),
- `atom1 - ${JSON.stringify(testFeedOptions)}`
+ path.join(PUBLIC_FEED_PATH, expectedFileName),
+ expectedFileContent
);
});
});
diff --git a/src/utils/__tests__/getLocale.test.ts b/src/utils/__tests__/getLocale.test.ts
index 5539d41c..43ff71aa 100644
--- a/src/utils/__tests__/getLocale.test.ts
+++ b/src/utils/__tests__/getLocale.test.ts
@@ -2,10 +2,15 @@ import { defaultLocale } from '~/i18n';
import getLocale from '../getLocale';
describe('get locale function', () => {
- it('should return undefined when acceptLanguage does not match and defaultLocale is not provided', () => {
- expect(getLocale()).toBe(undefined);
- expect(getLocale('/not/match/locale')).toBe(undefined);
- });
+ it.each([undefined, '/not/match/locale'])(
+ 'should return undefined when acceptLanguage does not match and defaultLocale is not provided',
+ (param) => {
+ // Act
+ const result = getLocale(param);
+ // Assert
+ expect(result).toBe(undefined);
+ }
+ );
it.each([
['zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7', 'zh'],
@@ -14,7 +19,10 @@ describe('get locale function', () => {
])(
'should extract the preferred language code from the Accept-Language header',
(acceptLanguage, expected) => {
- expect(getLocale(acceptLanguage, defaultLocale)).toBe(expected);
+ // Act
+ const result = getLocale(acceptLanguage, defaultLocale);
+ // Assert
+ expect(result).toBe(expected);
}
);
@@ -26,7 +34,10 @@ describe('get locale function', () => {
])(
'should extract the language code from the path string',
(pathname, expected) => {
- expect(getLocale(pathname, defaultLocale)).toBe(expected);
+ // Act
+ const result = getLocale(pathname, defaultLocale);
+ // Assert
+ expect(result).toBe(expected);
}
);
});
diff --git a/src/utils/__tests__/math.test.ts b/src/utils/__tests__/math.test.ts
index ffa613a0..b4bfe1a0 100644
--- a/src/utils/__tests__/math.test.ts
+++ b/src/utils/__tests__/math.test.ts
@@ -1,11 +1,20 @@
import { toFixedNumber } from '../math';
describe('math function', () => {
- it('should round a number to the specified number of decimal places', () => {
- const input = 1.3456;
- expect(toFixedNumber(0)(input)).toBe(1);
- expect(toFixedNumber(1)(input)).toBe(1.3);
- expect(toFixedNumber(2)(input)).toBe(1.35);
- expect(toFixedNumber(3)(input)).toBe(1.346);
- });
+ it.each([
+ [0, 1],
+ [1, 1.3],
+ [2, 1.35],
+ [3, 1.346],
+ ])(
+ 'should round a number to the specified number of decimal places',
+ (digits, expected) => {
+ // Arrange
+ const input = 1.3456;
+ // Act
+ const result = toFixedNumber(digits)(input);
+ // Assert
+ expect(result).toBe(expected);
+ }
+ );
});
diff --git a/src/utils/__tests__/mdx.test.ts b/src/utils/__tests__/mdx.test.ts
index 1602f0c8..22826c5a 100644
--- a/src/utils/__tests__/mdx.test.ts
+++ b/src/utils/__tests__/mdx.test.ts
@@ -30,13 +30,14 @@ describe('Get post list function', () => {
});
it('should get post list descending by date', async () => {
+ // Arrange
mockReaddir.mockReturnValueOnce(['test_A.md', 'test_B.mdx', 'test_C.md']);
mockReadFile.mockReturnValueOnce(mockFileA);
mockReadFile.mockReturnValueOnce(mockFileB);
mockReadFile.mockReturnValueOnce(mockFileC);
-
+ // Act
const postList = await getAllDataFrontmatter('posts');
-
+ // Assert
expect(postList).toStrictEqual([
{ id: 'test_C', date: '2023/07/09' },
{ id: 'test_A', date: '2023/07/08' },
@@ -45,15 +46,15 @@ describe('Get post list function', () => {
});
it('should return same id error with reject', async () => {
+ // Arrange
+ const expected = Error('Duplicate id "test_A" found in "test_A.mdx"');
mockReaddir.mockReturnValueOnce(['test_A.md', 'test_A.mdx']);
mockReadFile.mockReturnValue('');
expect.assertions(1);
-
- await expect(
- getAllDataFrontmatter('posts')
- ).rejects.toStrictEqual(
- Error('Duplicate id "test_A" found in "test_A.mdx"')
- );
+ // Act
+ const result = getAllDataFrontmatter('posts');
+ // Assert
+ await expect(result).rejects.toStrictEqual(expected);
});
});
@@ -65,11 +66,12 @@ describe('Get post data function', () => {
});
it('should get post data by mdx file', async () => {
+ // Arrange
mockExists.mockReturnValueOnce(true);
mockReadFile.mockReturnValueOnce(mockFileB);
-
+ // Act
const postData = await getDataById('posts', 'test_B');
-
+ // Assert
expect(postData).toStrictEqual({
content: '測試文章B',
frontmatter: { date: '2023/07/07' },
@@ -79,11 +81,12 @@ describe('Get post data function', () => {
});
it('should get post data by md file', async () => {
+ // Arrange
mockExists.mockReturnValueOnce(false);
mockReadFile.mockReturnValueOnce(mockFileC);
-
+ // Act
const postData = await getDataById('posts', 'test_C');
-
+ // Assert
expect(postData).toStrictEqual({
content: '測試文章C',
frontmatter: { date: '2023/07/09' },
@@ -93,13 +96,14 @@ describe('Get post data function', () => {
});
it('should return null when trying to get post data for a non-existing file', async () => {
+ // Arrange
mockExists.mockReturnValueOnce(false);
mockReadFile.mockImplementation(() => {
throw new Error('File not found');
});
-
+ // Act
const postData = await getDataById('posts', 'not_found');
-
+ // Assert
expect(postData).toBe(null);
});
});
diff --git a/tailwind.config.js b/tailwind.config.js
index 1c9bec6b..9a7f9981 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -57,7 +57,7 @@ const tailwindcssConfig = {
},
});
addComponents({
- '.lattice': {
+ '.bg-lattice': {
'--lattice-base-color': theme('colors.stone.50'),
'--lattice-line-color': 'rgba(0, 0, 0, 0.04)',
'--lattice-ratio': '10%',
@@ -80,7 +80,7 @@ const tailwindcssConfig = {
var(--lattice-base-color);`,
backgroundSize: 'var(--lattice-size);',
},
- '.dark .lattice': {
+ '.dark .bg-lattice': {
'--lattice-base-color': theme('colors.stone.950'),
'--lattice-line-color': 'rgba(255, 255, 255, 0.04)',
},
@@ -112,19 +112,19 @@ const tailwindcssConfig = {
});
addUtilities({
'.multiline-ellipsis': {
+ overflow: 'hidden',
display: '-webkit-box',
'-webkit-box-orient': 'vertical',
'-webkit-line-clamp': 'var(--webkit-line-clamp, 2)',
- overflow: 'hidden',
- }
+ },
});
- [3, 4, 5].forEach(number => {
+ [3, 4, 5].forEach((number) => {
addUtilities({
[`.multiline-ellipsis-${number}`]: {
'--webkit-line-clamp': String(number),
- }
+ },
});
- })
+ });
}),
],
};