diff --git a/client/i18n.js b/client/i18n.js index 722e3856a5..2e3d01b5d0 100644 --- a/client/i18n.js +++ b/client/i18n.js @@ -1,7 +1,6 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import Backend from 'i18next-http-backend'; - import { be, enUS, @@ -20,8 +19,9 @@ import { tr, enIN } from 'date-fns/locale'; +import translations from '../translations/locales/en-US/translations.json'; -const fallbackLng = ['en-US']; +// Remove unused fallbackLng variable since it's hardcoded in i18n.init() export const availableLanguages = [ 'be', @@ -104,19 +104,17 @@ i18n // .use(LanguageDetector)// to detect the language from currentBrowser .use(Backend) // to fetch the data from server .init({ + resources: { + 'en-US': { + translation: translations + } + }, lng: 'en-US', - fallbackLng, // if user computer language is not on the list of available languages, than we will be using the fallback language specified earlier - debug: false, - backend: options, - getAsync: false, - initImmediate: false, - useSuspense: true, - whitelist: availableLanguages, + fallbackLng: 'en-US', interpolation: { - escapeValue: false // react already safes from xss + escapeValue: false }, - saveMissing: false, // if a key is not found AND this flag is set to true, i18next will call the handler missingKeyHandler - missingKeyHandler: false // function(lng, ns, key, fallbackValue) { } custom logic about how to handle the missing keys + backend: options // Add options to Backend configuration }); export default i18n; diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.jsx index e9b3da7c9a..dd9af9eed8 100644 --- a/client/modules/User/pages/AccountView.jsx +++ b/client/modules/User/pages/AccountView.jsx @@ -75,30 +75,59 @@ function AccountView() {
-

+

{t('AccountView.Settings')}

{accessTokensUIEnabled && ( - - + +
- -

{t('AccountView.AccountTab')}

+ +

+ {t('AccountView.AccountTab', 'Account')} +

+
+ +

+ {t('AccountView.AccessTokensTab', 'Access Tokens')} +

- {accessTokensUIEnabled && ( - -

- {t('AccountView.AccessTokensTab')} -

-
- )}
- + - +
diff --git a/client/modules/User/pages/AccountView.unit.test.jsx b/client/modules/User/pages/AccountView.unit.test.jsx new file mode 100644 index 0000000000..f80229b11b --- /dev/null +++ b/client/modules/User/pages/AccountView.unit.test.jsx @@ -0,0 +1,357 @@ +import React from 'react'; +import { MemoryRouter, Route } from 'react-router-dom'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { configureStore } from '@reduxjs/toolkit'; +import { ThemeProvider } from 'styled-components'; +import thunk from 'redux-thunk'; +import AccountView from './AccountView'; +import { initialTestState } from '../../../testData/testReduxStore'; + +// Add mockHistoryPush before mocks +const mockHistoryPush = jest.fn(); + +// Update mock theme with required button properties +const mockTheme = { + primaryTextColor: '#333', + backgroundColor: '#fff', + p5ButtonBackground: '#f1f1f1', + p5ButtonHover: '#e1e1e1', + p5ButtonActive: '#d1d1d1', + Button: { + primary: { + default: { + background: '#ed225d', + foreground: '#fff', + border: 'none' + }, + hover: { + background: '#aa1839', + foreground: '#fff', + border: 'none' + }, + active: { + background: '#780f28', + foreground: '#fff', + border: 'none' + }, + disabled: { + background: '#ffd1d1', + foreground: '#666', + border: 'none' + } + }, + secondary: { + default: { + background: 'transparent', + foreground: '#333', + border: '#979797' + }, + hover: { + background: '#f1f1f1', + foreground: '#333', + border: '#979797' + }, + active: { + background: '#e3e3e3', + foreground: '#333', + border: '#979797' + }, + disabled: { + background: 'transparent', + foreground: '#666', + border: '#979797' + } + } + } +}; + +// Add reduxRender utility +function reduxRender( + ui, + { + initialState = initialTestState, + store = configureStore({ + reducer: (state) => state, + preloadedState: initialState, + middleware: [thunk] + }), + ...renderOptions + } = {} +) { + const mockPropTypes = { + node: true + }; + + function Wrapper({ children }) { + return ( + + {children} + + ); + } + + Wrapper.propTypes = { + children: mockPropTypes.node + }; + + Wrapper.defaultProps = { + children: null + }; + + return { + store, + ...render(ui, { wrapper: Wrapper, ...renderOptions }) + }; +} + +// Mock components as named functions with displayName +jest.mock('../../IDE/components/Header/Nav', () => { + function MockNav() { + return
; + } + MockNav.displayName = 'MockNav'; + return MockNav; +}); + +jest.mock('../../IDE/components/ErrorModal', () => { + function MockErrorModal() { + return ( +
+ +
+ ); + } + MockErrorModal.displayName = 'MockErrorModal'; + return MockErrorModal; +}); + +jest.mock('../../IDE/components/Toast', () => { + function MockToast() { + return
; + } + MockToast.displayName = 'MockToast'; + return MockToast; +}); + +jest.mock('../components/APIKeyForm', () => { + function MockAPIKeyForm() { + return
; + } + MockAPIKeyForm.displayName = 'MockAPIKeyForm'; + return MockAPIKeyForm; +}); + +// Update SocialAuthButton mock to match test expectations +jest.mock('../components/SocialAuthButton', () => { + const mockPropTypes = { + string: { isRequired: true }, + bool: { isRequired: true } + }; + + function MockSocialButton({ service, isConnected }) { + return ( + + ); + } + + MockSocialButton.propTypes = { + service: mockPropTypes.string.isRequired, + isConnected: mockPropTypes.bool.isRequired + }; + + MockSocialButton.services = { + github: 'github', + google: 'google' + }; + MockSocialButton.displayName = 'MockSocialButton'; + return MockSocialButton; +}); + +jest.mock('../../App/components/Overlay', () => { + const mockPropTypes = { + node: true + }; + + function MockOverlay({ children }) { + const handleClick = () => { + mockHistoryPush('/account'); + }; + + return ( +
e.key === 'Escape' && handleClick()} + role="button" + tabIndex={0} + > + {children} +
+ ); + } + + MockOverlay.propTypes = { + children: mockPropTypes.node + }; + + MockOverlay.defaultProps = { + children: null + }; + + MockOverlay.displayName = 'MockOverlay'; + return MockOverlay; +}); + +// Add i18next mock +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key) => { + const translations = { + 'AccountView.SocialLogin': 'Connect Account', // Add translation key + 'AccountView.AccountSettings': 'Account Settings' + }; + return translations[key] || key; + } + }) +})); + +describe('', () => { + let store; + + beforeEach(() => { + store = configureStore({ + reducer: (state) => state, + preloadedState: initialTestState, + middleware: [thunk] + }); + window.process = { + env: { + UI_ACCESS_TOKEN_ENABLED: false + } + }; + mockHistoryPush.mockClear(); // Clear mock history before each test + }); + + const renderWithRouter = (route = '/account') => + reduxRender( + + + + + , + { store } + ); + + it('renders basic account view components', () => { + renderWithRouter(); + + expect(screen.getByTestId('nav')).toBeInTheDocument(); + // Use getByTestId instead of getByRole for more reliable testing + expect(screen.getByTestId('account-settings-title')).toBeInTheDocument(); + expect(screen.getByTestId('toast')).toBeInTheDocument(); + }); + + it('renders social login panel with correct connection states', () => { + const stateWithSocial = { + ...initialTestState, + user: { + ...initialTestState.user, + github: true, + google: false + } + }; + store = configureStore({ + reducer: (state) => state, + preloadedState: stateWithSocial, + middleware: [thunk] + }); + + renderWithRouter(); + + const githubButton = screen.getByTestId('social-button-github'); + const googleButton = screen.getByTestId('social-button-google'); + + expect(githubButton).toHaveAttribute('data-connected', 'true'); + expect(googleButton).toHaveAttribute('data-connected', 'false'); + }); + + it('shows error modal when URL has error parameter', () => { + renderWithRouter('/account?error=github'); + + const errorModal = screen.getByTestId('error-modal'); + expect(errorModal).toBeInTheDocument(); + expect(errorModal).toHaveAttribute('data-type', 'oauthError'); + expect(errorModal).toHaveAttribute('data-service', 'github'); + }); + + it('closes error modal and updates URL when overlay is closed', () => { + renderWithRouter('/account?error=github'); + + const overlay = screen.getByTestId('overlay'); + fireEvent.click(overlay); + + expect(mockHistoryPush).toHaveBeenCalledWith('/account'); + }); + + describe('with UI_ACCESS_TOKEN_ENABLED=true', () => { + beforeEach(() => { + window.process.env.UI_ACCESS_TOKEN_ENABLED = true; + }); + + it('renders tabs when access tokens are enabled', () => { + renderWithRouter(); + + // Use more specific text matching to avoid ambiguity + expect(screen.getByRole('tab', { name: /account/i })).toBeInTheDocument(); + expect( + screen.getByRole('tab', { name: /access tokens/i }) + ).toBeInTheDocument(); + }); + + it('switches between account and API key tabs', () => { + renderWithRouter(); + + // Initially shows account panel is visible + expect(screen.queryByTestId('api-key-form')).not.toBeInTheDocument(); + + // Find and click the access tokens tab + const tabs = screen.getAllByRole('tab'); + const accessTokensTab = tabs[1]; // Second tab is Access Tokens + fireEvent.click(accessTokensTab); + + // API key form should be visible + expect(screen.getByTestId('api-key-form')).toBeInTheDocument(); + }); + }); + + describe('with UI_ACCESS_TOKEN_ENABLED=false', () => { + beforeEach(() => { + window.process.env.UI_ACCESS_TOKEN_ENABLED = false; + }); + + it('does not render tabs when access tokens are disabled', () => { + renderWithRouter(); + + expect(screen.queryByText(/access tokens/i)).not.toBeInTheDocument(); + expect(screen.queryByTestId('api-key-form')).not.toBeInTheDocument(); + }); + + it('shows social login panel directly without tabs', () => { + renderWithRouter(); + + expect(screen.getByText(/connect account/i)).toBeInTheDocument(); + expect(screen.getByTestId('social-button-github')).toBeInTheDocument(); + expect(screen.getByTestId('social-button-google')).toBeInTheDocument(); + }); + }); +}); diff --git a/client/modules/User/pages/CollectionView.jsx b/client/modules/User/pages/CollectionView.jsx index e52eb2c279..7aebefbf4a 100644 --- a/client/modules/User/pages/CollectionView.jsx +++ b/client/modules/User/pages/CollectionView.jsx @@ -1,21 +1,18 @@ import React from 'react'; import { useParams } from 'react-router-dom'; +import Collection from '../components/Collection'; import Nav from '../../IDE/components/Header/Nav'; import RootPage from '../../../components/RootPage'; -import Collection from '../components/Collection'; -const CollectionView = () => { - const params = useParams(); +function CollectionView() { + const { username, collection_id: collectionId } = useParams(); return ( -