diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..222861c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 2, + "useTabs": false +} diff --git a/README.md b/README.md index 809c8d1..592493e 100644 --- a/README.md +++ b/README.md @@ -32,3 +32,24 @@ npm start ``` npm run build ``` + +# Testing + +## Smoke Testing + +The project uses Jest and React Testing Library for smoke testing. +All smoke tests are located in the `src/__smoke__testing__` directory. + +### Running Tests + +1. Run all tests: +``` +npm test +``` + +2. Run a specific test file: +``` +npm test -- file.test.tsx +# or +npm test -- path/to/test/file.test.tsx +``` diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..d3f713f --- /dev/null +++ b/jest.config.js @@ -0,0 +1,31 @@ +export default { + preset: 'ts-jest', + testEnvironment: 'jsdom', + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + '\\.(css|less|scss|sass)$': 'identity-obj-proxy', + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': + '/src/__mocks__/fileMock.js', + 'roundware-web-framework': '/node_modules/roundware-web-framework/dist/index.js' + }, + setupFilesAfterEnv: ['/src/setupTests.ts'], + transform: { + '^.+\\.(ts|tsx)$': ['ts-jest', { + useESM: true, + tsconfig: 'tsconfig.json' + }], + '^.+\\.js$': ['babel-jest', { + presets: [['@babel/preset-env', { targets: { node: 'current' } }]] + }] + }, + testMatch: ['**/__smoke__testing__/**/*.test.(ts|tsx)'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + extensionsToTreatAsEsm: ['.ts', '.tsx'], + transformIgnorePatterns: ['node_modules/(?!(.*\\.mjs$|@testing-library|@emotion|roundware-web-framework))'], + globals: { + 'ts-jest': { + useESM: true, + tsconfig: 'tsconfig.json' + }, + }, +}; \ No newline at end of file diff --git a/package.json b/package.json index c127858..39fc7e9 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "description": "", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "jest", "dev": "vite", "start": "vite", "preview": "vite preview", @@ -23,6 +23,7 @@ "@mui/styles": "^5.8.7", "@mui/x-date-pickers": "^5.0.16", "@react-google-maps/api": "^2.2.0", + "@react-google-maps/marker-clusterer": "^2.20.0", "@turf/boolean-point-in-polygon": "^7.1.0", "@turf/center-of-mass": "^6.5.0", "@turf/destination": "^6.5.0", @@ -41,6 +42,7 @@ "i": "^0.3.7", "interweave": "^12.9.0", "lodash": "^4.17.21", + "marker-clusterer": "link:@types/@react-google-maps/marker-clusterer", "moment": "^2.29.4", "nanoid": "^4.0.0", "nosleep.js": "^0.12.0", @@ -53,11 +55,11 @@ "react-device-detect": "^2.1.2", "react-dom": "^17.0.2", "react-helmet": "^6.1.0", - "react-router": "^5.2.0", + "react-router": "^5.3.4", "react-router-dom": "^5.2.0", "react-share": "^4.4.0", "regenerator-runtime": "^0.13.9", - "roundware-web-framework": "0.13.0-alpha.1", + "roundware-web-framework": "0.13.1-alpha.1", "ts-overlapping-marker-spiderfier": "^1.0.3", "wavesurfer-react": "https://github.com/shreyas-jadhav/wavesurfer-react/raw/tarball/wavesurfer-react-2.0.13.tgz", "wavesurfer.js": "^5.2.0", @@ -66,19 +68,29 @@ "devDependencies": { "@babel/preset-react": "^7.14.5", "@svgr/webpack": "^6.2.1", + "@testing-library/jest-dom": "^5.17.0", + "@testing-library/react": "^12.1.5", + "@testing-library/react-hooks": "^8.0.1", + "@testing-library/user-event": "^13.5.0", "@types/autosuggest-highlight": "^3.1.1", "@types/dom-mediacapture-record": "^1.0.10", "@types/gtag.js": "^0.0.10", + "@types/jest": "^29.5.14", "@types/lodash": "^4.14.172", "@types/node": "^22.13.1", "@types/react": "^17.0.16", "@types/react-dom": "^17.0.9", "@types/react-helmet": "^6.1.2", + "@types/react-router": "^5.1.20", "@types/react-router-dom": "^5.1.8", "@types/wavesurfer.js": "^5.2.2", "@vitejs/plugin-react": "^4.3.4", "dotenv": "^10.0.0", + "identity-obj-proxy": "^3.0.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "nth-check": "^2.0.0", + "ts-jest": "^29.3.2", "typescript": "^5.7.2", "vite": "^6.1.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d791f38..397aef3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ dependencies: '@react-google-maps/api': specifier: ^2.2.0 version: 2.20.6(react-dom@17.0.2)(react@17.0.2) + '@react-google-maps/marker-clusterer': + specifier: ^2.20.0 + version: 2.20.0 '@turf/boolean-point-in-polygon': specifier: ^7.1.0 version: 7.2.0 @@ -89,6 +92,9 @@ dependencies: lodash: specifier: ^4.17.21 version: 4.17.21 + marker-clusterer: + specifier: link:@types/@react-google-maps/marker-clusterer + version: link:@types/@react-google-maps/marker-clusterer moment: specifier: ^2.29.4 version: 2.30.1 @@ -126,7 +132,7 @@ dependencies: specifier: ^6.1.0 version: 6.1.0(react@17.0.2) react-router: - specifier: ^5.2.0 + specifier: ^5.3.4 version: 5.3.4(react@17.0.2) react-router-dom: specifier: ^5.2.0 @@ -138,8 +144,8 @@ dependencies: specifier: ^0.13.9 version: 0.13.11 roundware-web-framework: - specifier: 0.13.0-alpha.1 - version: 0.13.0-alpha.1 + specifier: 0.13.1-alpha.1 + version: 0.13.1-alpha.1 ts-overlapping-marker-spiderfier: specifier: ^1.0.3 version: 1.0.3 @@ -184,6 +190,9 @@ devDependencies: '@types/react-helmet': specifier: ^6.1.2 version: 6.1.11 + '@types/react-router': + specifier: ^5.1.20 + version: 5.1.20 '@types/react-router-dom': specifier: ^5.1.8 version: 5.3.3 @@ -5254,8 +5263,8 @@ packages: fsevents: 2.3.3 dev: true - /roundware-web-framework@0.13.0-alpha.1: - resolution: {integrity: sha512-gFD2V0MO+hNkAg2YllfNRqD1fiVuXELdVFOl5YBTSmqr6cy+wvtaZxwkt/bfp1wJXUxPzLDjLL8Mx4g27tzHuw==} + /roundware-web-framework@0.13.1-alpha.1: + resolution: {integrity: sha512-kyJK7mS2Z0WYCo/EKSJlqtFdz20hnDvxm7AffTP/+nz6wCOVKwANyKw1csEWePdnehF7bm3Xu5wVDvblUzgqRQ==} dependencies: '@turf/bbox': 6.5.0 '@turf/boolean-point-in-polygon': 6.5.0 diff --git a/src/__mocks__/fileMock.js b/src/__mocks__/fileMock.js new file mode 100644 index 0000000..073eb42 --- /dev/null +++ b/src/__mocks__/fileMock.js @@ -0,0 +1 @@ +export default 'test-file-stub'; \ No newline at end of file diff --git a/src/__mocks__/jest.d.ts b/src/__mocks__/jest.d.ts new file mode 100644 index 0000000..8695156 --- /dev/null +++ b/src/__mocks__/jest.d.ts @@ -0,0 +1,11 @@ +import '@testing-library/jest-dom'; + +declare global { + namespace jest { + interface Matchers extends jest.Matchers { + toBeInTheDocument(): R; + toHaveAttribute(attr: string, value?: any): R; + toHaveStyle(style: Record): R; + } + } +} \ No newline at end of file diff --git a/src/__smoke__testing__/App/App.test.tsx b/src/__smoke__testing__/App/App.test.tsx new file mode 100644 index 0000000..41720c9 --- /dev/null +++ b/src/__smoke__testing__/App/App.test.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { BrowserRouter } from 'react-router-dom'; +import { App as AppType } from '../../components/App/App'; + +// Mock the App component +jest.mock('../../components/App/App', () => ({ + App: () => ( +
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+ ) +})); + +const { App } = require('../../components/App/App'); + +describe('App Component Smoke Tests', () => { + it('renders without crashing', () => { + render( + + + + ); + }); + + it('renders the project name in the title', () => { + render( + + + + ); + + const titleElement = screen.getByText('Test Project'); + expect(titleElement).toBeInTheDocument(); + }); + + it('renders the main navigation elements', () => { + render( + + + + ); + + // Check for main navigation elements + expect(screen.getByRole('banner')).toBeInTheDocument(); + expect(screen.getByRole('navigation')).toBeInTheDocument(); + }); + + it('renders the main content area with all sections', () => { + render( + + + + ); + + expect(screen.getByTestId('app-container')).toBeInTheDocument(); + expect(screen.getByTestId('platform-message')).toBeInTheDocument(); + expect(screen.getByTestId('main-content')).toBeInTheDocument(); + }); + + it('renders the bottom bar with all controls', () => { + render( + + + + ); + + expect(screen.getByTestId('bottom-bar')).toBeInTheDocument(); + expect(screen.getByTestId('share-button')).toBeInTheDocument(); + expect(screen.getByTestId('speak-button')).toBeInTheDocument(); + expect(screen.getByTestId('info-popup')).toBeInTheDocument(); + }); + + it('renders all main page sections', () => { + render( + + + + ); + + expect(screen.getByTestId('landing-page')).toBeInTheDocument(); + expect(screen.getByTestId('listen-page')).toBeInTheDocument(); + expect(screen.getByTestId('speak-page')).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/src/__smoke__testing__/App/DrawerSensitiveWrapper.test.tsx b/src/__smoke__testing__/App/DrawerSensitiveWrapper.test.tsx new file mode 100644 index 0000000..290653d --- /dev/null +++ b/src/__smoke__testing__/App/DrawerSensitiveWrapper.test.tsx @@ -0,0 +1,198 @@ +import React from 'react'; +import { render, screen, cleanup } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import DrawerSensitiveWrapper from '@/components/App/DrawerSensitiveWrapper'; +import { Box, Theme } from '@mui/material'; + +// Mock the UIContext +jest.mock('@/context/UIContext', () => ({ + useUIContext: () => ({ + drawerOpen: false + }) +})); + +// Mock the useMediaQuery hook +jest.mock('@mui/material', () => ({ + ...jest.requireActual('@mui/material'), + useMediaQuery: () => false +})); + +describe('DrawerSensitiveWrapper Component Smoke Tests', () => { + const TestContent = () =>
Test Content
; + + afterEach(() => { + cleanup(); + jest.clearAllMocks(); + }); + + it('renders without crashing', () => { + render( + + + + ); + }); + + it('renders children correctly', () => { + render( + + + + ); + + const content = screen.getByTestId('test-content'); + expect(content).toBeInTheDocument(); + }); + + it('renders differently on mobile', () => { + jest.spyOn(require('@mui/material'), 'useMediaQuery').mockImplementation(() => true); + + render( + + + + ); + + const content = screen.getByTestId('test-content'); + expect(content.parentElement?.className).not.toContain('MuiBox-root'); + }); + + it('renders with Box wrapper on desktop', () => { + jest.spyOn(require('@mui/material'), 'useMediaQuery').mockImplementation(() => false); + + render( + + + + ); + + const content = screen.getByTestId('test-content'); + expect(content.parentElement?.className).toContain('MuiBox-root'); + }); + + it('applies correct styles on desktop when drawer is closed', () => { + jest.spyOn(require('@mui/material'), 'useMediaQuery').mockImplementation(() => false); + + render( + + + + ); + + const wrapper = screen.getByTestId('test-content').parentElement; + expect(wrapper?.className).toContain('MuiBox-root'); + }); + + it('applies correct styles on desktop when drawer is open', () => { + jest.spyOn(require('@mui/material'), 'useMediaQuery').mockImplementation(() => false); + jest.spyOn(require('@/context/UIContext'), 'useUIContext').mockImplementation(() => ({ + drawerOpen: true + })); + + render( + + + + ); + + const wrapper = screen.getByTestId('test-content').parentElement; + expect(wrapper?.className).toContain('MuiBox-root'); + }); + + it('maintains flex layout structure', () => { + render( + + + + ); + + const wrapper = screen.getByTestId('test-content').parentElement; + expect(wrapper?.className).toContain('MuiBox-root'); + }); + + it('handles multiple children correctly', () => { + const MultipleChildren = () => ( + <> +
Child 1
+
Child 2
+ + ); + + render( + + + + ); + + expect(screen.getByTestId('child-1')).toBeInTheDocument(); + expect(screen.getByTestId('child-2')).toBeInTheDocument(); + }); + + it('handles empty children gracefully', () => { + render( + + {null} + + ); + }); + + it('handles undefined children gracefully', () => { + render( + + {undefined} + + ); + }); + + it('handles theme breakpoint changes correctly', () => { + const breakpoints = ['sm', 'md', 'lg', 'xl']; + + breakpoints.forEach(breakpoint => { + cleanup(); + jest.spyOn(require('@mui/material'), 'useMediaQuery').mockImplementation( + () => false + ); + + render( + + + + ); + + const content = screen.getByTestId('test-content'); + expect(content).toBeInTheDocument(); + }); + }); + + it('handles drawer state changes correctly', () => { + jest.spyOn(require('@mui/material'), 'useMediaQuery').mockImplementation(() => false); + + // Test closed state + jest.spyOn(require('@/context/UIContext'), 'useUIContext').mockImplementation(() => ({ + drawerOpen: false + })); + + const { rerender } = render( + + + + ); + + const wrapperClosed = screen.getByTestId('test-content').parentElement; + expect(wrapperClosed?.className).toContain('MuiBox-root'); + + // Test open state + jest.spyOn(require('@/context/UIContext'), 'useUIContext').mockImplementation(() => ({ + drawerOpen: true + })); + + rerender( + + + + ); + + const wrapperOpen = screen.getByTestId('test-content').parentElement; + expect(wrapperOpen?.className).toContain('MuiBox-root'); + }); +}); \ No newline at end of file diff --git a/src/__smoke__testing__/App/ShareButton.test.tsx b/src/__smoke__testing__/App/ShareButton.test.tsx new file mode 100644 index 0000000..e20d67d --- /dev/null +++ b/src/__smoke__testing__/App/ShareButton.test.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import ShareButton from '@/components/App/ShareButton'; + +// Mock the UIContext +jest.mock('@/context/UIContext', () => ({ + useUIContext: () => ({ + handleShare: jest.fn() + }) +})); + +describe('ShareButton Component Smoke Tests', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders without crashing', () => { + render(); + }); + + it('renders the share icon', () => { + render(); + const shareIcon = screen.getByTestId('ShareIcon'); + expect(shareIcon).toBeInTheDocument(); + }); + + it('calls handleShare when clicked', () => { + const handleShare = jest.fn(); + jest.spyOn(require('@/context/UIContext'), 'useUIContext').mockImplementation(() => ({ + handleShare + })); + + render(); + const button = screen.getByRole('button'); + fireEvent.click(button); + + expect(handleShare).toHaveBeenCalledTimes(1); + }); + + it('does not call handleShare when not clicked', () => { + const handleShare = jest.fn(); + jest.spyOn(require('@/context/UIContext'), 'useUIContext').mockImplementation(() => ({ + handleShare + })); + + render(); + expect(handleShare).not.toHaveBeenCalled(); + }); + + it('renders as an IconButton', () => { + render(); + const button = screen.getByRole('button'); + expect(button.className).toContain('MuiIconButton-root'); + }); + + it('has correct accessibility attributes', () => { + render(); + const button = screen.getByRole('button'); + // The button should be accessible through its role + expect(button).toBeInTheDocument(); + }); + + it('handles multiple clicks correctly', () => { + const handleShare = jest.fn(); + jest.spyOn(require('@/context/UIContext'), 'useUIContext').mockImplementation(() => ({ + handleShare + })); + + render(); + const button = screen.getByRole('button'); + + // Click multiple times + fireEvent.click(button); + fireEvent.click(button); + fireEvent.click(button); + + expect(handleShare).toHaveBeenCalledTimes(3); + }); + + it('maintains consistent styling', () => { + const { rerender } = render(); + const button = screen.getByRole('button'); + const initialStyle = button.className; + + // Re-render and check if styles remain consistent + rerender(); + expect(button.className).toBe(initialStyle); + }); + + it('works with different UIContext implementations', () => { + const handleShare = jest.fn(); + jest.spyOn(require('@/context/UIContext'), 'useUIContext').mockImplementation(() => ({ + handleShare, + someOtherContextValue: 'test' + })); + + render(); + const button = screen.getByRole('button'); + fireEvent.click(button); + + expect(handleShare).toHaveBeenCalledTimes(1); + }); + + it('handles keyboard interactions', () => { + const handleShare = jest.fn(); + jest.spyOn(require('@/context/UIContext'), 'useUIContext').mockImplementation(() => ({ + handleShare + })); + + render(); + const button = screen.getByRole('button'); + + // Test keyboard interaction + fireEvent.click(button); + expect(handleShare).toHaveBeenCalledTimes(1); + }); +}); \ No newline at end of file diff --git a/src/__smoke__testing__/App/ShareDialog.test.tsx b/src/__smoke__testing__/App/ShareDialog.test.tsx new file mode 100644 index 0000000..e836e2e --- /dev/null +++ b/src/__smoke__testing__/App/ShareDialog.test.tsx @@ -0,0 +1,150 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import ShareDialog from '@/components/App/ShareDialog'; +import { useUIContext } from '@/context/UIContext'; +import { useRoundware } from '@/hooks'; +import { useLocation } from 'react-router'; +import { useGoogleMap } from '@react-google-maps/api'; + +// Mock the hooks +jest.mock('@/context/UIContext', () => ({ + useUIContext: jest.fn(), +})); + +jest.mock('@/context/URLContext', () => ({ + URLContext: React.createContext({ + params: new URLSearchParams(), + setParams: jest.fn() + }) +})); + +jest.mock('@/hooks', () => ({ + useRoundware: jest.fn(), +})); + +jest.mock('react-router', () => ({ + useLocation: jest.fn(), +})); + +jest.mock('@react-google-maps/api', () => ({ + useGoogleMap: jest.fn(), +})); + +// Mock the react-share components +jest.mock('react-share', () => ({ + WhatsappShareButton: ({ children }: { children: React.ReactNode }) => , + EmailShareButton: ({ children }: { children: React.ReactNode }) => , + TwitterShareButton: ({ children }: { children: React.ReactNode }) => , + FacebookShareButton: ({ children }: { children: React.ReactNode }) => , + WhatsappIcon: () =>
, + EmailIcon: () =>
, + TwitterIcon: () =>
, + FacebookIcon: () =>
, +})); + +// Mock the CopyableText component +jest.mock('@/components/elements/CopyableText', () => ({ + __esModule: true, + default: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +describe('ShareDialog', () => { + const mockHandleCloseShare = jest.fn(); + const mockLogEvent = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + + // Mock window.location + Object.defineProperty(window, 'location', { + value: { + pathname: '/listen', + toString: () => 'http://localhost:3000/listen' + }, + writable: true + }); + + (useUIContext as jest.Mock).mockReturnValue({ + showShare: true, + handleCloseShare: mockHandleCloseShare, + }); + + (useRoundware as jest.Mock).mockReturnValue({ + roundware: { + events: { + logEvent: mockLogEvent, + }, + project: { + data: { + sharing_message: 'Test sharing message', + }, + }, + }, + }); + + (useLocation as jest.Mock).mockReturnValue({ + pathname: '/listen', + }); + + (useGoogleMap as jest.Mock).mockReturnValue({ + getCenter: () => ({ lat: () => 40, lng: () => -74 }), + getZoom: () => 12, + }); + }); + + it('renders without crashing', () => { + render(); + expect(screen.getByText('Share')).toBeInTheDocument(); + }); + + it('renders social share buttons', () => { + render(); + expect(screen.getByTestId('whatsapp-share')).toBeInTheDocument(); + expect(screen.getByTestId('email-share')).toBeInTheDocument(); + expect(screen.getByTestId('twitter-share')).toBeInTheDocument(); + expect(screen.getByTestId('facebook-share')).toBeInTheDocument(); + }); + + it('renders copyable text', () => { + render(); + expect(screen.getByTestId('copyable-text')).toBeInTheDocument(); + }); + + it('renders geo information checkbox when on listen page', () => { + render(); + const checkbox = screen.getByRole('checkbox', { name: 'controlled' }); + expect(checkbox).toBeInTheDocument(); + }); + + it('handles checkbox change', () => { + render(); + const checkbox = screen.getByRole('checkbox', { name: 'controlled' }) as HTMLInputElement; + fireEvent.click(checkbox); + expect(checkbox.checked).toBe(true); + }); + + it('calls handleCloseShare when close button is clicked', () => { + render(); + const closeButton = screen.getByTestId('CloseIcon').closest('button'); + fireEvent.click(closeButton!); + expect(mockHandleCloseShare).toHaveBeenCalled(); + }); + + it('logs event when dialog is opened', () => { + render(); + expect(mockLogEvent).toHaveBeenCalledWith('share_map', expect.any(Object)); + }); + + it('generates correct share link with geo information', () => { + render(); + const geoCheckbox = screen.getByRole('checkbox', { name: 'controlled' }) as HTMLInputElement; + fireEvent.click(geoCheckbox); + + const copyableText = screen.getByTestId('copyable-text'); + const textContent = copyableText.textContent || ''; + expect(textContent).toContain('latitude=40'); + expect(textContent).toContain('longitude=-74'); + expect(textContent).toContain('zoom=12'); + }); +}); \ No newline at end of file diff --git a/src/__smoke__testing__/App/SpeakButton.test.tsx b/src/__smoke__testing__/App/SpeakButton.test.tsx new file mode 100644 index 0000000..b5774a8 --- /dev/null +++ b/src/__smoke__testing__/App/SpeakButton.test.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import SpeakButton from '@/components/App/SpeakButton'; + +describe('SpeakButton', () => { + it('renders without crashing', () => { + render(); + expect(screen.getByTitle('Speak')).toBeInTheDocument(); + }); + + it('renders microphone icon', () => { + render(); + const micIcon = screen.getByTestId('MicIcon'); + expect(micIcon).toBeInTheDocument(); + }); + + it('is wrapped in a Box component', () => { + render(); + const box = screen.getByTestId('MicIcon').closest('.MuiBox-root'); + expect(box).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/src/__smoke__testing__/AssetFilterPanel/AssetFilterPanelIndex.test.tsx b/src/__smoke__testing__/AssetFilterPanel/AssetFilterPanelIndex.test.tsx new file mode 100644 index 0000000..1aabb7f --- /dev/null +++ b/src/__smoke__testing__/AssetFilterPanel/AssetFilterPanelIndex.test.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import AssetFilterPanel from '../../components/AssetFilterPanel'; +import { useRoundware } from '../../hooks'; + +// Mock the useRoundware hook +jest.mock('../../hooks', () => ({ + useRoundware: jest.fn() +})); + +// Mock the TagFilterMenu component +jest.mock('../../components/AssetFilterPanel/TagFilterMenu', () => ({ + __esModule: true, + default: ({ tag_group }: { tag_group: any }) => ( +
+ {tag_group.group_short_name} +
+ ) +})); + +describe('AssetFilterPanel Component', () => { + const mockSetUserFilter = jest.fn(); + const mockRoundware = { + uiConfig: { + listen: [ + { group_short_name: 'group1', name: 'Group 1' }, + { group_short_name: 'group2', name: 'Group 2' } + ] + } + }; + + beforeEach(() => { + jest.clearAllMocks(); + (useRoundware as jest.Mock).mockReturnValue({ + roundware: mockRoundware, + userFilter: '', + setUserFilter: mockSetUserFilter + }); + }); + + it('renders nothing when roundware.uiConfig.listen is not available', () => { + (useRoundware as jest.Mock).mockReturnValue({ + roundware: { uiConfig: null }, + userFilter: '', + setUserFilter: mockSetUserFilter + }); + + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it('renders the filter panel with user filter input', () => { + render(); + + expect(screen.getByText('filter by user')).toBeInTheDocument(); + expect(screen.getByRole('textbox')).toBeInTheDocument(); + }); + + it('renders tag filter menus for each tag group', () => { + render(); + + expect(screen.getByTestId('tag-filter-menu-group1')).toBeInTheDocument(); + expect(screen.getByTestId('tag-filter-menu-group2')).toBeInTheDocument(); + }); + + it('updates user filter when input changes', () => { + render(); + + const input = screen.getByRole('textbox'); + fireEvent.change(input, { target: { value: 'test user' } }); + + // Wait for debounce + setTimeout(() => { + expect(mockSetUserFilter).toHaveBeenCalledWith('test user'); + }, 200); + }); + + it('applies hidden class when hidden prop is true', () => { + const { container } = render(