-
{
});
expect(screen.getByText('100%')).toBeInTheDocument();
-
+
// Decrypting matrix text should be fully resolved at 100%
- expect(screen.getByText('SYSTEM_ONLINE // LINK_ESTABLISHED')).toBeInTheDocument();
+ expect(
+ screen.getByText('SYSTEM_ONLINE // LINK_ESTABLISHED'),
+ ).toBeInTheDocument();
// Advance the remaining hold delay to trigger onComplete
act(() => {
diff --git a/src/data/Projects.ts b/src/data/Projects.ts
index 81fdebd..4344cc7 100644
--- a/src/data/Projects.ts
+++ b/src/data/Projects.ts
@@ -104,6 +104,12 @@ Core idea of this project is to build a place on the internet where coders can f
githubLink: 'https://github.com/WorldOfTech0/WOT',
link: 'https://worldoftech.co.in',
tags: [Language.react, Language.typescript, Language.i18n],
+ image: [
+ 'worldoftech/1.png',
+ 'worldoftech/2.png',
+ 'worldoftech/3.png',
+ 'worldoftech/4.png',
+ ],
keyPoints: [
'Built as a highly organized directory cataloging free tools across media, AI, privacy, books, and system utilities.',
'Features a responsive glassmorphic UI with quick category filters and a fast local search/filter system.',
@@ -133,8 +139,8 @@ Core idea of this project is to build a place on the internet where coders can f
status: Status.DEVELOPMENT,
description:
'A hardware-agnostic audio-networking application that turns nearby computers, smartphones, and tablets into a synchronized, unified speaker system using local Wi-Fi and Bluetooth protocols.',
- githubLink: 'https://github.com/AmitRaikwar-in/AudioMesh',
- image: ['mac/AR_Mac1.png', 'mac/AR_Mac2.png', 'mac/AR_Mac3.png'],
+ githubLink: 'https://github.com/AudioMesh',
+ image: ['audiomesh/Landing1.png', 'audiomesh/Landing2.png'],
link: 'https://audiomesh.amitraikwar.in/',
tags: [
Language.rust,
@@ -176,37 +182,26 @@ This library has most of the UI components and hooks required for building hourc
'This library is used in various projects and is being updated regularly.',
],
},
- {
- title: 'Galaxy UI Library',
- icon: ProjectName.GalaxyUI,
- status: Status.DEVELOPMENT,
- description:
- 'React typescript UI library for building web apps. Idea is to create a very fancy web application using this library. This library is currently hosted on NPM and is updated with new components and features regularly.',
- githubLink: 'https://www.github.com/onemanfighter/hourcoding-ui',
- link: 'http://galaxy-ui.netlify.com/',
- npmLink: 'https://www.npmjs.com/package/@galaxy_ui/ui',
- tags: [Language.react, Language.typescript, Language.i18n],
- keyPoints: [
- 'This website is designed for coding tutorials and programming articles.',
- 'It is built with React, Next.js, TypeScript, Chakra UI, and Tailwind CSS.',
- 'Utility hooks that facilitate the development of web applications.',
- 'Currently hosted on NPM and is being updated with new components and features regularly.',
- ],
- },
{
title: 'Growboard',
- icon: ProjectName.Dashwave,
- status: Status.DEVELOPMENT,
+ icon: ProjectName.Growboard,
+ status: Status.LIVE,
description: `A web app for managing stuffs in life like study, projects, expenses, secrets, passwords, writing journals, etc. A web app for managing stuff in life like study, projects, expenses, secrets, passwords, writing journals etc.
This project was started with a idea to manage everything at one place without maintaining something overwhelming or complicated.`,
- githubLink: 'https://www.github.com/onemanfighter/hourcoding-ui',
- link: 'https://dashwave.amitraikwar.com',
+ githubLink: 'https://www.github.com/Growboard/growboard',
+ link: 'https://gb.amitraikwar.com',
tags: [
Language.react,
Language.typescript,
Language.i18n,
Language.supabase,
],
+ image: [
+ 'growboard/LandingPage.png',
+ 'growboard/Login.png',
+ 'growboard/Expenses.png',
+ 'growboard/LandingPage.png',
+ ],
keyPoints: [
'Compatible with all devices and has a responsive design.',
'Built with React, TypeScript, Chakra UI, and Tailwind CSS.',
@@ -214,6 +209,23 @@ This library has most of the UI components and hooks required for building hourc
'This project is still under development and more features are being added regularly.',
],
},
+ {
+ title: 'Galaxy UI Library',
+ icon: ProjectName.GalaxyUI,
+ status: Status.DEVELOPMENT,
+ description:
+ 'React typescript UI library for building web apps. Idea is to create a very fancy web application using this library. This library is currently hosted on NPM and is updated with new components and features regularly.',
+ githubLink: 'https://www.github.com/onemanfighter/hourcoding-ui',
+ link: 'http://galaxy-ui.netlify.com/',
+ npmLink: 'https://www.npmjs.com/package/@galaxy_ui/ui',
+ tags: [Language.react, Language.typescript, Language.i18n],
+ keyPoints: [
+ 'This website is designed for coding tutorials and programming articles.',
+ 'It is built with React, Next.js, TypeScript, Chakra UI, and Tailwind CSS.',
+ 'Utility hooks that facilitate the development of web applications.',
+ 'Currently hosted on NPM and is being updated with new components and features regularly.',
+ ],
+ },
{
title: 'TestCov',
icon: ProjectName.TestCov,
diff --git a/src/hooks/__tests__/constants.test.ts b/src/hooks/__tests__/constants.test.ts
new file mode 100644
index 0000000..ad03df6
--- /dev/null
+++ b/src/hooks/__tests__/constants.test.ts
@@ -0,0 +1,11 @@
+import { SPRING_SETTING } from '../constants';
+
+describe('hooks constants', () => {
+ it('should export SPRING_SETTING correctly', () => {
+ expect(SPRING_SETTING).toEqual({
+ damping: 40,
+ stiffness: 1000,
+ restDelta: 0.001,
+ });
+ });
+});
diff --git a/src/hooks/__tests__/useIsIntersecting.test.tsx b/src/hooks/__tests__/useIsIntersecting.test.tsx
new file mode 100644
index 0000000..b140ee8
--- /dev/null
+++ b/src/hooks/__tests__/useIsIntersecting.test.tsx
@@ -0,0 +1,37 @@
+import { renderHook } from '@testing-library/react';
+import useIsIntersecting from '../useIsIntersecting';
+import { act } from 'react';
+
+class MockIntersectionObserver {
+ observe = jest.fn();
+ disconnect = jest.fn();
+ unobserve = jest.fn();
+ constructor(public callback: any) {
+ MockIntersectionObserver.lastInstance = this;
+ }
+ static lastInstance: MockIntersectionObserver | null = null;
+}
+global.IntersectionObserver = MockIntersectionObserver as any;
+
+describe('useIsIntersecting', () => {
+ beforeEach(() => {
+ MockIntersectionObserver.lastInstance = null;
+ });
+
+ it('should register observer and return value on intersection change', () => {
+ const ref = { current: document.createElement('div') };
+ const { result } = renderHook(() => useIsIntersecting(ref));
+
+ expect(MockIntersectionObserver.lastInstance).not.toBeNull();
+ expect(MockIntersectionObserver.lastInstance?.observe).toHaveBeenCalledWith(ref.current);
+
+ expect(result.current).toBe(false);
+
+ // Trigger callback
+ act(() => {
+ MockIntersectionObserver.lastInstance?.callback([{ isIntersecting: true }]);
+ });
+
+ expect(result.current).toBe(true);
+ });
+});
diff --git a/src/hooks/__tests__/useMousePositions.test.tsx b/src/hooks/__tests__/useMousePositions.test.tsx
new file mode 100644
index 0000000..e0ed688
--- /dev/null
+++ b/src/hooks/__tests__/useMousePositions.test.tsx
@@ -0,0 +1,17 @@
+import { renderHook, fireEvent } from '@testing-library/react';
+import useMousePositions from '../useMousePositions';
+import { act } from 'react';
+
+describe('useMousePositions', () => {
+ it('should track mouse position changes on mousemove', () => {
+ const { result } = renderHook(() => useMousePositions());
+
+ expect(result.current).toEqual({ x: 0, y: 0 });
+
+ act(() => {
+ fireEvent.mouseMove(window, { clientX: 150, clientY: 200 });
+ });
+
+ expect(result.current).toEqual({ x: 150, y: 200 });
+ });
+});
diff --git a/src/hooks/__tests__/useMoveToTop.test.tsx b/src/hooks/__tests__/useMoveToTop.test.tsx
new file mode 100644
index 0000000..d6e5982
--- /dev/null
+++ b/src/hooks/__tests__/useMoveToTop.test.tsx
@@ -0,0 +1,17 @@
+import { renderHook } from '@testing-library/react';
+import useMoveToTop from '../useMoveToTop';
+
+describe('useMoveToTop', () => {
+ it('should call window.scrollTo with top 0 and smooth behavior', () => {
+ const scrollToMock = jest.fn();
+ window.scrollTo = scrollToMock;
+
+ const { result } = renderHook(() => useMoveToTop());
+ result.current();
+
+ expect(scrollToMock).toHaveBeenCalledWith({
+ top: 0,
+ behavior: 'smooth',
+ });
+ });
+});
diff --git a/src/hooks/__tests__/useSpringMousePosition.test.tsx b/src/hooks/__tests__/useSpringMousePosition.test.tsx
new file mode 100644
index 0000000..e169a4e
--- /dev/null
+++ b/src/hooks/__tests__/useSpringMousePosition.test.tsx
@@ -0,0 +1,38 @@
+import { renderHook, fireEvent } from '@testing-library/react';
+import useSpringMousePosition from '../useSpringMousePosition';
+import { act } from 'react';
+
+jest.mock('framer-motion', () => {
+ const jumpMockX = jest.fn();
+ const jumpMockY = jest.fn();
+ return {
+ useMotionValue: jest.fn(() => ({
+ jump: jest.fn((val) => {
+ if (jumpMockX.mock.calls.length === 0) jumpMockX(val);
+ else jumpMockY(val);
+ }),
+ })),
+ useSpring: jest.fn((mv) => mv),
+ frame: {
+ read: jest.fn((cb) => cb()),
+ },
+ };
+});
+
+describe('useSpringMousePosition', () => {
+ it('should setup pointermove listeners and trigger on pointer move', () => {
+ const div = document.createElement('div');
+ Object.defineProperty(div, 'offsetLeft', { value: 10 });
+ Object.defineProperty(div, 'offsetTop', { value: 20 });
+ const ref = { current: div };
+
+ const { result } = renderHook(() => useSpringMousePosition(ref));
+
+ act(() => {
+ fireEvent(window, new MouseEvent('pointermove', { clientX: 100, clientY: 200 }));
+ });
+
+ expect(result.current.x).toBeDefined();
+ expect(result.current.y).toBeDefined();
+ });
+});
diff --git a/src/index.css b/src/index.css
index c9f60f5..d74a125 100644
--- a/src/index.css
+++ b/src/index.css
@@ -10,15 +10,55 @@ body,
color-scheme: dark;
}
-body::after {
- content: '';
- position: fixed;
- inset: 0;
- pointer-events: none;
- z-index: 9999;
- background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.5' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
- opacity: 0.06;
- mix-blend-mode: screen;
+@keyframes spin-border {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes float {
+ 0% {
+ transform: translateY(0);
+ }
+ 50% {
+ transform: translateY(-10px);
+ }
+ 100% {
+ transform: translateY(0);
+ }
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes marquee {
+ 0% {
+ transform: translateX(0%);
+ }
+ 100% {
+ transform: translateX(-100%);
+ }
+}
+
+@keyframes marquee-vertical {
+ 0% {
+ transform: translateY(0%);
+ }
+ 100% {
+ transform: translateY(-100%);
+ }
+}
+
+@keyframes shine {
+ 0% {
+ background-position: 100%;
+ }
+ 100% {
+ background-position: -100%;
+ }
}
/* width */
diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts
index 5d0b3c4..f2d274e 100644
--- a/src/react-app-env.d.ts
+++ b/src/react-app-env.d.ts
@@ -9,4 +9,3 @@ declare module '*.gltf' {
const src: string;
export default src;
}
-
diff --git a/src/screens/articles/ArticlesScreen.tsx b/src/screens/articles/ArticlesScreen.tsx
index 4ffbea8..d8915f8 100644
--- a/src/screens/articles/ArticlesScreen.tsx
+++ b/src/screens/articles/ArticlesScreen.tsx
@@ -113,7 +113,7 @@ const ArticlesScreen = () => {
paddingX={{ base: 1, md: 6 }}
position="relative"
>
-
+
{
rowGap={20}
ref={ref}
pos="relative"
- bg="black"
>
{
topColor="#5227ff"
bottomColor="#ff9ffc"
intensity={2}
- rotationSpeed={0.3}
+ rotationSpeed={1.3}
glowAmount={0.003}
pillarWidth={3}
pillarHeight={1.0}
noiseIntensity={0.6}
pillarRotation={90}
- interactive={true}
+ interactive={false}
mixBlendMode="screen"
quality="medium"
/>
diff --git a/src/services/backend/articles/__tests__/articles.test.ts b/src/services/backend/articles/__tests__/articles.test.ts
new file mode 100644
index 0000000..3cf4480
--- /dev/null
+++ b/src/services/backend/articles/__tests__/articles.test.ts
@@ -0,0 +1,104 @@
+import { GetRequest, PostRequest } from '../../client/client';
+import addArticle from '../addArticle';
+import addComment from '../addComment';
+import deleteArticle from '../deleteArticle';
+import getAllArticlesData from '../getAllArticlesData';
+import getArticleData from '../getArticleData';
+import getComments from '../getComments';
+import getEditorArticleData from '../getEditorArticleData';
+import likePageArticleData from '../likePageArticleData';
+import pingTest from '../pingTest';
+import updateArticle from '../updateArticle';
+import {
+ ADD_ARTICLE_URL,
+ ADD_COMMENT_URL,
+ DELETE_ARTICLE_URL,
+ ALL_ARTICLES_URL,
+ ARTICLES_URL,
+ GET_COMMENTS_URL,
+ EDITOR_ARTICLES_URL,
+ LIKE_ARTICLE_URL,
+ PING_URL,
+ UPDATE_ARTICLE_URL,
+} from '../constants';
+
+jest.mock('../../client/client', () => ({
+ GetRequest: jest.fn(),
+ PostRequest: jest.fn(),
+}));
+
+describe('Articles Backend Services', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('addArticle should make PostRequest', async () => {
+ (PostRequest as jest.Mock).mockResolvedValueOnce('add-article-success');
+ const result = await addArticle({ title: 'Test' });
+ expect(PostRequest).toHaveBeenCalledWith(ADD_ARTICLE_URL, { title: 'Test' });
+ expect(result).toBe('add-article-success');
+ });
+
+ it('addComment should make PostRequest', async () => {
+ (PostRequest as jest.Mock).mockResolvedValueOnce('add-comment-success');
+ const result = await addComment({ text: 'Test comment' });
+ expect(PostRequest).toHaveBeenCalledWith(ADD_COMMENT_URL, { text: 'Test comment' });
+ expect(result).toBe('add-comment-success');
+ });
+
+ it('deleteArticle should make GetRequest', async () => {
+ (GetRequest as jest.Mock).mockResolvedValueOnce('delete-article-success');
+ const result = await deleteArticle({ articleKey: 'key1' });
+ expect(GetRequest).toHaveBeenCalledWith(DELETE_ARTICLE_URL + '?articleKey=key1');
+ expect(result).toBe('delete-article-success');
+ });
+
+ it('getAllArticlesData should make GetRequest', async () => {
+ (GetRequest as jest.Mock).mockResolvedValueOnce('get-all-success');
+ const result = await getAllArticlesData();
+ expect(GetRequest).toHaveBeenCalledWith(ALL_ARTICLES_URL);
+ expect(result).toBe('get-all-success');
+ });
+
+ it('getArticleData should make GetRequest', async () => {
+ (GetRequest as jest.Mock).mockResolvedValueOnce('get-article-success');
+ const result = await getArticleData('key1');
+ expect(GetRequest).toHaveBeenCalledWith(ARTICLES_URL + '?articleKey=key1');
+ expect(result).toBe('get-article-success');
+ });
+
+ it('getComments should make GetRequest', async () => {
+ (GetRequest as jest.Mock).mockResolvedValueOnce('get-comments-success');
+ const result = await getComments('key1');
+ expect(GetRequest).toHaveBeenCalledWith(GET_COMMENTS_URL + '?articleKey=key1');
+ expect(result).toBe('get-comments-success');
+ });
+
+ it('getEditorArticleData should make GetRequest', async () => {
+ (GetRequest as jest.Mock).mockResolvedValueOnce('get-editor-success');
+ const result = await getEditorArticleData('key1');
+ expect(GetRequest).toHaveBeenCalledWith(EDITOR_ARTICLES_URL + '?articleKey=key1');
+ expect(result).toBe('get-editor-success');
+ });
+
+ it('likePageArticleData should make PostRequest', async () => {
+ (PostRequest as jest.Mock).mockResolvedValueOnce('like-success');
+ const result = await likePageArticleData('key1');
+ expect(PostRequest).toHaveBeenCalledWith(LIKE_ARTICLE_URL, 'key1');
+ expect(result).toBe('like-success');
+ });
+
+ it('pingTest should make GetRequest', async () => {
+ (GetRequest as jest.Mock).mockResolvedValueOnce('ping-success');
+ const result = await pingTest();
+ expect(GetRequest).toHaveBeenCalledWith(PING_URL);
+ expect(result).toBe('ping-success');
+ });
+
+ it('updateArticle should make PostRequest', async () => {
+ (PostRequest as jest.Mock).mockResolvedValueOnce('update-success');
+ const result = await updateArticle({ title: 'New title' });
+ expect(PostRequest).toHaveBeenCalledWith(UPDATE_ARTICLE_URL, { title: 'New title' });
+ expect(result).toBe('update-success');
+ });
+});
diff --git a/src/services/backend/client/__tests__/client.test.ts b/src/services/backend/client/__tests__/client.test.ts
new file mode 100644
index 0000000..d7a6a11
--- /dev/null
+++ b/src/services/backend/client/__tests__/client.test.ts
@@ -0,0 +1,59 @@
+const mockGet = jest.fn();
+const mockPost = jest.fn();
+
+jest.mock('axios', () => ({
+ create: jest.fn(() => ({
+ get: mockGet,
+ post: mockPost,
+ })),
+}));
+
+// Require after mocks are initialized to prevent ES6 hoisting issues
+const { GetRequest, PostRequest } = require('../client');
+
+describe('backend client', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('GetRequest', () => {
+ it('should return data on successful request', async () => {
+ mockGet.mockResolvedValueOnce({ data: 'success-get' });
+ const result = await GetRequest('/test-url');
+ expect(mockGet).toHaveBeenCalledWith('/test-url');
+ expect(result).toBe('success-get');
+ });
+
+ it('should log error and return undefined on failure', async () => {
+ const error = new Error('get-failed');
+ const consoleSpy = jest
+ .spyOn(console, 'error')
+ .mockImplementation(jest.fn());
+ mockGet.mockRejectedValueOnce(error);
+
+ const result = await GetRequest('/test-url');
+ expect(consoleSpy).toHaveBeenCalledWith(error);
+ expect(result).toBeUndefined();
+
+ consoleSpy.mockRestore();
+ });
+ });
+
+ describe('PostRequest', () => {
+ it('should return data on successful post', async () => {
+ mockPost.mockResolvedValueOnce({ data: 'success-post' });
+ const result = await PostRequest('/test-post-url', { payload: 'data' });
+ expect(mockPost).toHaveBeenCalledWith(
+ '/test-post-url',
+ { payload: 'data' },
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Access-Control-Allow-Origin': '*',
+ },
+ },
+ );
+ expect(result).toBe('success-post');
+ });
+ });
+});
diff --git a/src/services/hooks/articles/__tests__/hooksArticles.test.ts b/src/services/hooks/articles/__tests__/hooksArticles.test.ts
new file mode 100644
index 0000000..6305c09
--- /dev/null
+++ b/src/services/hooks/articles/__tests__/hooksArticles.test.ts
@@ -0,0 +1,162 @@
+import { renderHook } from '@testing-library/react';
+import useAddArticle from '../useAddArticle';
+import useAddComment from '../useAddComment';
+import useDeleteArticle from '../useDeleteArticle';
+import useGetAllArticlesData from '../useGetAllArticlesData';
+import useGetArticleData from '../useGetArticleData';
+import useGetComments from '../useGetComments';
+import useGetEditorArticleData from '../useGetEditorArticleData';
+import useLikeArticleData from '../useLikeArticleData';
+import usePingTest from '../usePingTest';
+import useUpdateArticle from '../useUpdateArticle';
+import { useToast } from '@chakra-ui/react';
+import { useCallQuery, useCallSBMutation } from '../../common';
+import {
+ addArticle,
+ addComment,
+ deleteArticle,
+ getArticlesData,
+ getArticleData,
+ getComments,
+ getEditorArticleData,
+ likePageArticleData,
+ pingTest,
+ updateArticle,
+} from '../../../backend';
+
+jest.mock('@chakra-ui/react', () => ({
+ useToast: jest.fn(),
+}));
+
+jest.mock('@tanstack/react-query', () => ({
+ useQueryClient: jest.fn(() => ({
+ invalidateQueries: jest.fn(),
+ })),
+}));
+
+jest.mock('../../common', () => ({
+ useCallQuery: jest.fn((args) => {
+ args.method();
+ return 'query-mock-result';
+ }),
+ useCallSBMutation: jest.fn((args) => {
+ args.method('mock-data');
+ const mockVariables = { articleKey: 'mock-key' };
+ if (args.mutationOptions?.onSuccess) args.mutationOptions.onSuccess('mock-response', mockVariables);
+ if (args.mutationOptions?.onError) args.mutationOptions.onError('mock-error', mockVariables);
+ return 'mutation-mock-result';
+ }),
+}));
+
+jest.mock('../../../backend', () => ({
+ addArticle: jest.fn(),
+ addComment: jest.fn(),
+ deleteArticle: jest.fn(),
+ getArticlesData: jest.fn(),
+ getArticleData: jest.fn(),
+ getComments: jest.fn(),
+ getEditorArticleData: jest.fn(),
+ likePageArticleData: jest.fn(),
+ pingTest: jest.fn(),
+ updateArticle: jest.fn(),
+}));
+
+describe('Articles hooks', () => {
+ const toastMock = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (useToast as jest.Mock).mockReturnValue(toastMock);
+ });
+
+ it('useAddArticle should set up and trigger toast on success/error', () => {
+ const { result } = renderHook(() => useAddArticle());
+ expect(useCallSBMutation).toHaveBeenCalled();
+ expect(addArticle).toHaveBeenCalledWith('mock-data');
+ expect(toastMock).toHaveBeenCalledTimes(2);
+ expect(toastMock).toHaveBeenNthCalledWith(1, expect.objectContaining({ status: 'success' }));
+ expect(toastMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ status: 'error' }));
+ expect(result.current).toBe('mutation-mock-result');
+ });
+
+ it('useAddComment should set up and trigger toast on success/error', () => {
+ const { result } = renderHook(() => useAddComment());
+ expect(useCallSBMutation).toHaveBeenCalled();
+ expect(addComment).toHaveBeenCalledWith('mock-data');
+ expect(toastMock).toHaveBeenCalledTimes(2);
+ expect(toastMock).toHaveBeenNthCalledWith(1, expect.objectContaining({ status: 'success' }));
+ expect(toastMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ status: 'error' }));
+ expect(result.current).toBe('mutation-mock-result');
+ });
+
+ it('useDeleteArticle should set up and trigger toast on success/error', () => {
+ const { result } = renderHook(() => useDeleteArticle());
+ expect(useCallSBMutation).toHaveBeenCalled();
+ expect(deleteArticle).toHaveBeenCalledWith('mock-data');
+ expect(toastMock).toHaveBeenCalledTimes(2);
+ expect(toastMock).toHaveBeenNthCalledWith(1, expect.objectContaining({ status: 'success' }));
+ expect(toastMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ status: 'error' }));
+ expect(result.current).toBe('mutation-mock-result');
+ });
+
+ it('useUpdateArticle should set up and trigger toast on success/error', () => {
+ const { result } = renderHook(() => useUpdateArticle());
+ expect(useCallSBMutation).toHaveBeenCalled();
+ expect(updateArticle).toHaveBeenCalledWith('mock-data');
+ expect(toastMock).toHaveBeenCalledTimes(2);
+ expect(toastMock).toHaveBeenNthCalledWith(1, expect.objectContaining({ status: 'success' }));
+ expect(toastMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ status: 'error' }));
+ expect(result.current).toBe('mutation-mock-result');
+ });
+
+ it('useLikeArticleData should set up and trigger toast on success/error', () => {
+ const { result } = renderHook(() => useLikeArticleData());
+ expect(useCallSBMutation).toHaveBeenCalled();
+ expect(likePageArticleData).toHaveBeenCalledWith('mock-data');
+ expect(toastMock).toHaveBeenCalledTimes(2);
+ expect(toastMock).toHaveBeenNthCalledWith(1, expect.objectContaining({ status: 'success' }));
+ expect(toastMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ status: 'error' }));
+ expect(result.current).toBe('mutation-mock-result');
+ });
+
+ it('useGetAllArticlesData should trigger query method', () => {
+ const { result } = renderHook(() => useGetAllArticlesData());
+ expect(useCallQuery).toHaveBeenCalled();
+ expect(getArticlesData).toHaveBeenCalled();
+ expect(result.current).toBe('query-mock-result');
+ });
+
+ it('useGetArticleData should trigger query method with key', () => {
+ const { result } = renderHook(() => useGetArticleData('key1'));
+ expect(useCallQuery).toHaveBeenCalled();
+ expect(getArticleData).toHaveBeenCalled();
+ expect(result.current).toBe('query-mock-result');
+ });
+
+ it('useGetComments should trigger query method with key', () => {
+ const { result } = renderHook(() => useGetComments('key1'));
+ expect(useCallQuery).toHaveBeenCalled();
+ expect(getComments).toHaveBeenCalled();
+ expect(result.current).toBe('query-mock-result');
+ });
+
+ it('useGetEditorArticleData should trigger query method with key', () => {
+ const { result } = renderHook(() => useGetEditorArticleData('key1'));
+ expect(useCallQuery).toHaveBeenCalled();
+ expect(getEditorArticleData).toHaveBeenCalled();
+ expect(result.current).toBe('query-mock-result');
+ });
+
+ it('useGetEditorArticleData should resolve to undefined when key is empty', () => {
+ const { result } = renderHook(() => useGetEditorArticleData(''));
+ expect(useCallQuery).toHaveBeenCalled();
+ expect(result.current).toBe('query-mock-result');
+ });
+
+ it('usePingTest should trigger query method', () => {
+ const { result } = renderHook(() => usePingTest());
+ expect(useCallQuery).toHaveBeenCalled();
+ expect(pingTest).toHaveBeenCalled();
+ expect(result.current).toBe('query-mock-result');
+ });
+});
diff --git a/src/services/hooks/common/__tests__/hooksCommon.test.ts b/src/services/hooks/common/__tests__/hooksCommon.test.ts
new file mode 100644
index 0000000..c601c2f
--- /dev/null
+++ b/src/services/hooks/common/__tests__/hooksCommon.test.ts
@@ -0,0 +1,58 @@
+import { useQuery, useMutation } from '@tanstack/react-query';
+import useCallQuery from '../useCallQuery';
+import useCallSBMutation from '../useCallSBMutation';
+
+jest.mock('@tanstack/react-query', () => ({
+ useQuery: jest.fn((options) => {
+ options.queryFn('test-request');
+ return 'query-result';
+ }),
+ useMutation: jest.fn((options) => {
+ options.mutationFn('test-request');
+ return 'mutation-result';
+ }),
+}));
+
+describe('Common React Query Hooks', () => {
+ const methodMock = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('useCallQuery', () => {
+ it('should invoke useQuery with correct args and trigger queryFn', () => {
+ const result = useCallQuery({
+ method: methodMock,
+ queryOptions: { queryKey: ['test-key'] },
+ });
+
+ expect(useQuery).toHaveBeenCalledWith(
+ expect.objectContaining({
+ queryKey: ['test-key'],
+ queryFn: expect.any(Function),
+ }),
+ );
+ expect(methodMock).toHaveBeenCalledWith('test-request');
+ expect(result).toBe('query-result');
+ });
+ });
+
+ describe('useCallSBMutation', () => {
+ it('should invoke useMutation with correct args and trigger mutationFn', () => {
+ const result = useCallSBMutation({
+ method: methodMock,
+ mutationOptions: { mutationKey: ['test-mut'] },
+ });
+
+ expect(useMutation).toHaveBeenCalledWith(
+ expect.objectContaining({
+ mutationKey: ['test-mut'],
+ mutationFn: expect.any(Function),
+ }),
+ );
+ expect(methodMock).toHaveBeenCalledWith('test-request');
+ expect(result).toBe('mutation-result');
+ });
+ });
+});
diff --git a/src/store/selectors/Search/__tests__/Search.selector.test.ts b/src/store/selectors/Search/__tests__/Search.selector.test.ts
new file mode 100644
index 0000000..4307bfe
--- /dev/null
+++ b/src/store/selectors/Search/__tests__/Search.selector.test.ts
@@ -0,0 +1,32 @@
+import { appStore } from '@store';
+import { act, renderHook } from '@testing-library/react';
+import {
+ selectSearchText,
+ setSearchTextSelector,
+ resetSearchTextSelector,
+} from '../Search.selector';
+
+describe('Search selector', () => {
+ it('should select search text, set search text, and reset search text', () => {
+ const { result: textResult } = renderHook(() => appStore(selectSearchText));
+ const { result: setResult } = renderHook(() => appStore(setSearchTextSelector));
+ const { result: resetResult } = renderHook(() => appStore(resetSearchTextSelector));
+
+ expect(textResult.current).toBe('');
+
+ act(() => {
+ setResult.current('test search');
+ });
+
+ // Re-render hook or re-read state to see update
+ const { result: updatedTextResult } = renderHook(() => appStore(selectSearchText));
+ expect(updatedTextResult.current).toBe('test search');
+
+ act(() => {
+ resetResult.current();
+ });
+
+ const { result: resetTextResult } = renderHook(() => appStore(selectSearchText));
+ expect(resetTextResult.current).toBe('');
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index b882db9..3de0849 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,7 +2,7 @@
# Manual changes might be lost - proceed with caution!
__metadata:
- version: 8
+ version: 10
cacheKey: 10c0
"@adobe/css-tools@npm:^4.0.1":