diff --git a/.env.example b/.env.example index 5ea9f001..d8516b00 100644 --- a/.env.example +++ b/.env.example @@ -1 +1 @@ -VITE_API_URL="http://localhost:8083" \ No newline at end of file +VITE_API_URL="http://localhost:8080/v1/" \ No newline at end of file diff --git a/src/components/Dashboard/MyCollections/MyCollectionsContent/index.test.tsx b/src/components/Dashboard/MyCollections/MyCollectionsContent/index.test.tsx index cc9e0997..1a19ffc4 100644 --- a/src/components/Dashboard/MyCollections/MyCollectionsContent/index.test.tsx +++ b/src/components/Dashboard/MyCollections/MyCollectionsContent/index.test.tsx @@ -3,7 +3,7 @@ import { MantineProvider } from '@mantine/core'; import MyCollectionsContent from './index'; import Collection from '../../../../models/user/collection'; import { mockCollection } from '../../../../test-utils/mocks/models/collection'; -import { mockPagination } from '../../../../test-utils/mocks'; +import { mockPagination } from '../../../../test-utils/mocks/pagination'; // Mock the CollectionCard component jest.mock('./CollectionCard', () => ({ diff --git a/src/components/Dashboard/MyCollections/index.test.tsx b/src/components/Dashboard/MyCollections/index.test.tsx index d51fdd89..fe30c765 100644 --- a/src/components/Dashboard/MyCollections/index.test.tsx +++ b/src/components/Dashboard/MyCollections/index.test.tsx @@ -4,7 +4,7 @@ import MyCollections from './index'; import User from '../../../models/user/user'; import { UserCollectionsContext, UserCollectionsContextState } from '../../../contexts/UserCollectionsContext'; import CollectionManagementRequests from '../../../services/requests/CollectionManagementRequests'; -import { mockPagination } from '../../../test-utils/mocks'; +import { mockPagination } from '../../../test-utils/mocks/pagination'; import { renderWithProviders } from '../../../test-utils'; import Collection from '../../../models/user/collection'; import CollectionItem from '../../../models/user/collection-items'; diff --git a/src/components/Template/Page/index.test.tsx b/src/components/Template/Page/index.test.tsx index c5d54536..e66ef233 100644 --- a/src/components/Template/Page/index.test.tsx +++ b/src/components/Template/Page/index.test.tsx @@ -17,7 +17,8 @@ describe('Page', () => { renderWithRouter(); // Check for sidebar - expect(screen.getByRole('link', { name: /home/i })).toBeInTheDocument(); + const homeLink = document.querySelector('a[href="/"]'); + expect(homeLink).toBeInTheDocument(); // Check for navigation expect(screen.getByText('Header Title')).toBeInTheDocument(); diff --git a/src/contexts/UserCollectionsContext.tsx b/src/contexts/UserCollectionsContext.tsx index eb88a1a0..5ef4629b 100644 --- a/src/contexts/UserCollectionsContext.tsx +++ b/src/contexts/UserCollectionsContext.tsx @@ -1,9 +1,9 @@ import { BasePaginatedContextProviderProps, BasePaginatedContextState, createCallbacks, - defaultBaseContext, + defaultBaseContext, prepareContextState, } from './BasePaginatedContext'; -import React, {PropsWithChildren, useCallback, useEffect, useMemo, useState, Dispatch, SetStateAction} from 'react'; +import React, {PropsWithChildren, useEffect, useState} from 'react'; import Collection from "../models/user/collection"; /** @@ -13,13 +13,12 @@ export interface UserCollectionsContextState extends BasePaginatedContextState(), // Specify Model type + ...defaultBaseContext(), loadAll: true, order: { 'created_at': 'desc', @@ -38,47 +37,15 @@ export interface UserCollectionsContextProviderProps extends BasePaginatedContex } export const UserCollectionsContextProvider: React.FC> = (props => { - const [userCollectionsState, setUserCollectionsState] = useState(initialPersistentContextState); - - const wrappedSetUserCollectionsState: Dispatch>> = - useCallback((action) => { - setUserCollectionsState(currentUserCollectionsState => { - const baseStateChanges = typeof action === 'function' - ? (action as (prevState: BasePaginatedContextState) => BasePaginatedContextState)(currentUserCollectionsState) - : action; - return { ...currentUserCollectionsState, ...baseStateChanges } as UserCollectionsContextState; - }); - }, [setUserCollectionsState]); - + const [userCollectionsState, setUserCollectionsState] = useState(persistentContext); useEffect(() => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - setUserCollectionsState(_prevState => { - let newState = createDefaultState(); - - newState = createCallbacks( - wrappedSetUserCollectionsState, - newState, - `/users/${props.userId}/collections` - ); - - newState.refreshData(false).catch(console.error); + prepareContextState(setUserCollectionsState, userCollectionsState, '/users/' + props.userId + '/collections') + }, [props.userId, userCollectionsState]); - return { - ...newState, - loadedData: [], - lastLoadedPage: undefined, - noResults: false, - refreshing: true, - initialLoadComplete: false, - initiated: true, - }; - }); - }, [props.userId, setUserCollectionsState, wrappedSetUserCollectionsState]); - - const fullContext = useMemo(() => ({ - ...userCollectionsState, - ...createCallbacks(wrappedSetUserCollectionsState, userCollectionsState, `/users/${props.userId}/collections`) - }), [userCollectionsState, wrappedSetUserCollectionsState, props.userId]); + const fullContext = { + ...persistentContext, + ...createCallbacks(setUserCollectionsState, persistentContext, '/users/' + props.userId + '/collections') + } return ( diff --git a/src/test-utils/mocks/api.ts b/src/test-utils/mocks/api.ts index 4561c98f..a3e2d12d 100644 --- a/src/test-utils/mocks/api.ts +++ b/src/test-utils/mocks/api.ts @@ -1,19 +1,5 @@ -/* eslint-disable */ -// @ts-nocheck -import { mockUser } from './models/user'; -import { mockCategory } from './models/category'; -import { mockCollection } from './models/collection'; -import { mockCollectionItem } from './models/collection-items'; -import { mockOrganization } from './models/organization'; -import { mockBusiness } from './models/business'; -import { mockPostResponse } from './models/post-response'; -import { mockLocation } from './models/location'; -import { mockRole } from './models/role'; -import { mockOrganizationManager } from '../../../models/organization/organization-manager'; -import { mockPage } from '../../../models/page'; - export interface ApiConfig { - params?: Record; + params?: Record; signal?: AbortSignal; } @@ -28,81 +14,19 @@ export interface Page { data: T[]; } -// Export mock data for use in tests -export { mockPlatform, mockPlatformGroup }; - const api = { - get: jest.fn().mockImplementation((url: string, config?: ApiConfig): Promise> => { + get: jest.fn().mockImplementation((url: string, config?: ApiConfig): Promise>> => { // Parse URL parameters from both URL and config const urlParams = new URLSearchParams(url.split('?')[1] || ''); const configParams = config?.params || {}; // Merge URL params with config params Object.entries(configParams).forEach(([key, value]) => { - if (value !== undefined) { + if (value !== undefined && value !== null) { urlParams.set(key, value.toString()); } }); - const expand = urlParams.get('expand[platforms]') === '*'; - const page = parseInt(urlParams.get('page') || '1'); - const limit = parseInt(urlParams.get('limit') || '10'); - - // Handle paginated endpoints - if (url === '/platforms') { - const response: Page = { - current_page: page, - last_page: 1, - total: 1, - data: [mockPlatform] - }; - return Promise.resolve({ data: response }); - } - - if (url === '/platform-groups') { - const response: Page = { - current_page: page, - last_page: 1, - total: 1, - data: [mockPlatformGroup] - }; - return Promise.resolve({ data: response }); - } - - // Handle single platform group endpoint - const platformGroupMatch = url.match(/\/platform-groups\/(\d+)/); - if (platformGroupMatch) { - const groupId = parseInt(platformGroupMatch[1]); - if (groupId === mockPlatformGroup.id) { - const response = { - ...mockPlatformGroup, - platforms: expand ? [mockPlatform] : undefined - }; - return Promise.resolve({ data: response }); - } - return Promise.reject({ - response: { - data: { error: 'Platform group not found' }, - status: 404 - } - }); - } - - // Handle single platform endpoint - const platformMatch = url.match(/\/platforms\/(\d+)/); - if (platformMatch) { - const platformId = parseInt(platformMatch[1]); - if (platformId === mockPlatform.id) { - return Promise.resolve({ data: mockPlatform }); - } - return Promise.reject({ - response: { - data: { error: 'Platform not found' }, - status: 404 - } - }); - } - // Default response for unhandled endpoints return Promise.resolve({ data: { @@ -114,29 +38,15 @@ const api = { }); }), - post: jest.fn().mockImplementation((url: string, data: any): Promise> => { - if (url === '/platform-groups') { - return Promise.resolve({ data: { ...mockPlatformGroup, ...data } }); - } - if (url === '/platforms') { - return Promise.resolve({ data: { ...mockPlatform, ...data } }); - } + post: jest.fn().mockImplementation((): Promise> => { return Promise.resolve({ data: null }); }), - put: jest.fn().mockImplementation((url: string, data: any): Promise> => { - const platformGroupMatch = url.match(/\/platform-groups\/(\d+)/); - if (platformGroupMatch) { - return Promise.resolve({ data: { ...mockPlatformGroup, ...data } }); - } - const platformMatch = url.match(/\/platforms\/(\d+)/); - if (platformMatch) { - return Promise.resolve({ data: { ...mockPlatform, ...data } }); - } + put: jest.fn().mockImplementation((): Promise> => { return Promise.resolve({ data: null }); }), - delete: jest.fn().mockImplementation((url: string): Promise> => { + delete: jest.fn().mockImplementation((): Promise> => { return Promise.resolve({ data: { success: true } }); }) }; diff --git a/src/test-utils/mocks/contexts.tsx b/src/test-utils/mocks/contexts.tsx deleted file mode 100644 index c9ad7347..00000000 --- a/src/test-utils/mocks/contexts.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import React, { PropsWithChildren } from 'react'; -import { MeContextStateConsumer, MeContext } from '../../contexts/MeContext'; -import { placeholderUser } from '../../models/user/user'; -import User from '../../models/user/user'; -import { CategoriesContextState } from '../../contexts/CategoriesContext'; -import Category from '../../models/category'; -import { Provider } from 'react-redux'; -import configureStore from 'redux-mock-store'; -import { mockCategory } from './models/category'; -import { mockPagination } from './pagination'; - -// Mock appState -// eslint-disable-next-line @typescript-eslint/no-explicit-any -(global as any).appState = { - state: { - persistent: { - tokenData: null - }, - session: { - loadingCount: 0 - } - }, - dispatch: jest.fn() -}; - -// Mock functions -export const mockSetFilter = jest.fn(); - -// Base mock context state creator -export const createBaseMockContextState = (data: T[]) => mockPagination({ - loadedData: data, - initialLoadComplete: true, - refreshing: false, - hasAnotherPage: false, - initiated: true, - noResults: false, - expands: [], - order: {}, - filter: {}, - search: {}, - limit: 20, - loadAll: false, - setFilter: jest.fn(), - setSearch: jest.fn(), - setOrder: jest.fn(), - // eslint-disable-next-line @typescript-eslint/no-unused-vars - addModel: jest.fn((_model: T) => {}), - // eslint-disable-next-line @typescript-eslint/no-unused-vars - removeModel: jest.fn((_model: T) => {}), - getModel: jest.fn((id: number) => data.find(item => item.id === id) || null), - params: {}, - loadNext: jest.fn(), - refreshData: jest.fn() -}); - -// Mock CategoriesContext -export const mockCategoriesContextValue = { - ...createBaseMockContextState([ - mockCategory({ id: 1, name: 'Test Category', can_be_primary: true }) - ]) -}; - -export const mockCategoriesContextValueLoading = { - ...createBaseMockContextState([]), - initialLoadComplete: false, - isLoading: true -}; - -export const mockCategoriesContextValueEmpty = { - ...createBaseMockContextState([]), - noResults: true -}; - -// Mock CategoriesContext -jest.mock('../../contexts/CategoriesContext', () => ({ - __esModule: true, - CategoriesContext: React.createContext(mockCategoriesContextValue) -})); - -// Create mock store -const mockStore = configureStore([]); - -type MeContextProviderProps = { - initialState?: { - me: { - user: User; - networkError: boolean; - isLoggedIn: boolean; - isLoading: boolean; - }; - }; - hideLoadingSpace?: boolean; -} - -// MeContext Provider -export const MeContextProvider: React.FC> = ({ children, initialState, hideLoadingSpace }) => { - const store = mockStore(initialState || { - me: { - user: placeholderUser(), - networkError: false, - isLoggedIn: false, - isLoading: false - } - }); - - const [meContext, setMeContext] = React.useState({ - me: initialState?.me?.user || placeholderUser(), - networkError: initialState?.me?.networkError || false, - isLoggedIn: initialState?.me?.isLoggedIn || false, - isLoading: initialState?.me?.isLoading || false, - }); - - const fullContext = { - ...meContext, - setMe: (user: User) => { - setMeContext(prev => ({ - ...prev, - me: user, - isLoggedIn: !!user.id, - isLoading: false - })); - store.dispatch({ type: 'SET_USER', payload: user }); - }, - } as MeContextStateConsumer; - - return ( - - - {(!meContext.isLoading || hideLoadingSpace) ? children : - (meContext.networkError ? -
Network Error
: -
Loading...
- ) - } -
-
- ); -}; \ No newline at end of file diff --git a/src/test-utils/mocks/contexts/base.ts b/src/test-utils/mocks/contexts/base.ts new file mode 100644 index 00000000..8afec445 --- /dev/null +++ b/src/test-utils/mocks/contexts/base.ts @@ -0,0 +1,49 @@ +import { mockPagination } from '../pagination'; +import BaseModel from '../../../models/base-model'; + +// Mock appState +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(global as any).appState = { + state: { + persistent: { + tokenData: null + }, + session: { + loadingCount: 0 + } + }, + dispatch: jest.fn() +}; + +// Mock functions +export const mockSetFilter = jest.fn(); + +// Base mock context state creator +export const createBaseMockContextState = (data: T[]) => mockPagination({ + loadedData: data, + initialLoadComplete: true, + refreshing: false, + hasAnotherPage: false, + initiated: true, + noResults: false, + expands: [], + order: {}, + filter: {}, + search: {}, + limit: 20, + loadAll: false, + setFilter: jest.fn(), + setSearch: jest.fn(), + setOrder: jest.fn(), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + addModel: jest.fn((_model: T) => {}), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + removeModel: jest.fn((_model: T) => {}), + getModel: jest.fn((id: number) => { + const found = data.find(item => typeof item.id === 'number' && item.id === id); + return found || null; + }), + params: {}, + loadNext: jest.fn(), + refreshData: jest.fn() +}); \ No newline at end of file diff --git a/src/test-utils/mocks/contexts/categories.ts b/src/test-utils/mocks/contexts/categories.ts new file mode 100644 index 00000000..461d3ab0 --- /dev/null +++ b/src/test-utils/mocks/contexts/categories.ts @@ -0,0 +1,29 @@ +import React from 'react'; +import { CategoriesContextState } from '../../../contexts/CategoriesContext'; +import Category from '../../../models/category'; +import { mockCategory } from '../models/category'; +import { createBaseMockContextState } from './base'; + +// Mock CategoriesContext +export const mockCategoriesContextValue = { + ...createBaseMockContextState([ + mockCategory({ id: 1, name: 'Test Category', can_be_primary: true }) + ]) +}; + +export const mockCategoriesContextValueLoading = { + ...createBaseMockContextState([]), + initialLoadComplete: false, + isLoading: true +}; + +export const mockCategoriesContextValueEmpty = { + ...createBaseMockContextState([]), + noResults: true +}; + +// Mock CategoriesContext +jest.mock('../../../contexts/CategoriesContext', () => ({ + __esModule: true, + CategoriesContext: React.createContext(mockCategoriesContextValue) +})); \ No newline at end of file diff --git a/src/test-utils/mocks/contexts/index.ts b/src/test-utils/mocks/contexts/index.ts new file mode 100644 index 00000000..5f741350 --- /dev/null +++ b/src/test-utils/mocks/contexts/index.ts @@ -0,0 +1,12 @@ +// Base context utilities +export { createBaseMockContextState, mockSetFilter } from './base'; + +// Categories context mocks +export { + mockCategoriesContextValue, + mockCategoriesContextValueLoading, + mockCategoriesContextValueEmpty +} from './categories'; + +// MeContext provider and types +export { MeContextProvider, type MeContextProviderProps } from './me'; \ No newline at end of file diff --git a/src/test-utils/mocks/contexts/me.tsx b/src/test-utils/mocks/contexts/me.tsx new file mode 100644 index 00000000..53e44e2d --- /dev/null +++ b/src/test-utils/mocks/contexts/me.tsx @@ -0,0 +1,75 @@ +import React, { PropsWithChildren } from 'react'; +import { MeContextStateConsumer, MeContext } from '../../../contexts/MeContext'; +import { placeholderUser } from '../../../models/user/user'; +import User from '../../../models/user/user'; +import { Provider } from 'react-redux'; +import configureStore from 'redux-mock-store'; + +// Create mock store +const mockStore = configureStore([]); + +// Mock MeContext value +export const mockMeContextValue = { + me: placeholderUser(), + networkError: false, + isLoggedIn: false, + isLoading: false, + setMe: jest.fn() +}; + +export type MeContextProviderProps = { + initialState?: { + me: { + user: User; + networkError: boolean; + isLoggedIn: boolean; + isLoading: boolean; + }; + }; + hideLoadingSpace?: boolean; +} + +// MeContext Provider +export const MeContextProvider: React.FC> = ({ children, initialState, hideLoadingSpace }) => { + const store = mockStore(initialState || { + me: { + user: placeholderUser(), + networkError: false, + isLoggedIn: false, + isLoading: false + } + }); + + const [meContext, setMeContext] = React.useState({ + me: initialState?.me?.user || placeholderUser(), + networkError: initialState?.me?.networkError || false, + isLoggedIn: initialState?.me?.isLoggedIn || false, + isLoading: initialState?.me?.isLoading || false, + }); + + const fullContext = { + ...meContext, + setMe: (user: User) => { + setMeContext(prev => ({ + ...prev, + me: user, + isLoggedIn: !!user.id, + isLoading: false + })); + store.dispatch({ type: 'SET_USER', payload: user }); + }, + } as MeContextStateConsumer; + + return ( + + + {(!meContext.isLoading || hideLoadingSpace) ? children : + (meContext.networkError ? +
Network Error
: +
Loading...
+ ) + } +
+
+ ); +}; \ No newline at end of file diff --git a/src/test-utils/mocks/index.ts b/src/test-utils/mocks/index.ts deleted file mode 100644 index 77f4f57e..00000000 --- a/src/test-utils/mocks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './pagination'; \ No newline at end of file diff --git a/src/testing/wrappers.tsx b/src/testing/wrappers.tsx deleted file mode 100644 index e41519fa..00000000 --- a/src/testing/wrappers.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { configure } from 'enzyme'; -import Adapter from '@cfaester/enzyme-adapter-react-18'; - -configure({adapter: new Adapter()}); -