From 7255645c4f46b4903e168764945dc0b5b81a5b01 Mon Sep 17 00:00:00 2001
From: Rajnish2105 <166371359+Rajnish2105@users.noreply.github.com>
Date: Wed, 25 Dec 2024 20:11:54 +0530
Subject: [PATCH 1/2] Refactored useWhatPage hook to provide mobileTitle
---
.../IDE/components/Header/MobileNav.jsx | 42 +--
client/modules/IDE/hooks/useWhatPage.js | 46 ++-
.../User/pages/AccountView.unit.test.jsx | 357 ++++++++++++++++++
3 files changed, 421 insertions(+), 24 deletions(-)
create mode 100644 client/modules/User/pages/AccountView.unit.test.jsx
diff --git a/client/modules/IDE/components/Header/MobileNav.jsx b/client/modules/IDE/components/Header/MobileNav.jsx
index 37fa16bed3..3da2323329 100644
--- a/client/modules/IDE/components/Header/MobileNav.jsx
+++ b/client/modules/IDE/components/Header/MobileNav.jsx
@@ -1,4 +1,4 @@
-import React, { useContext, useMemo, useState } from 'react';
+import React, { useContext, useState } from 'react';
import styled from 'styled-components';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
@@ -204,30 +204,30 @@ const MobileNav = () => {
const project = useSelector((state) => state.project);
const user = useSelector((state) => state.user);
- const { t } = useTranslation();
+ // const { t } = useTranslation();
const editorLink = useSelector(selectSketchPath);
- const pageName = useWhatPage();
+ const { pageName, title } = useWhatPage();
// TODO: remove the switch and use a props like mobileTitle
- function resolveTitle() {
- switch (pageName) {
- case 'login':
- return t('LoginView.Login');
- case 'signup':
- return t('LoginView.SignUp');
- case 'account':
- return t('AccountView.Settings');
- case 'examples':
- return t('Nav.File.Examples');
- case 'myStuff':
- return 'My Stuff';
- default:
- return project.name;
- }
- }
-
- const title = useMemo(resolveTitle, [pageName, project.name]);
+ // function resolveTitle() {
+ // switch (pageName) {
+ // case 'login':
+ // return t('LoginView.Login');
+ // case 'signup':
+ // return t('LoginView.SignUp');
+ // case 'account':
+ // return t('AccountView.Settings');
+ // case 'examples':
+ // return t('Nav.File.Examples');
+ // case 'myStuff':
+ // return 'My Stuff';
+ // default:
+ // return project.name;
+ // }
+ // }
+
+ // const title = useMemo(resolveTitle, [pageName, project.name]);
const Logo = AsteriskIcon;
return (
diff --git a/client/modules/IDE/hooks/useWhatPage.js b/client/modules/IDE/hooks/useWhatPage.js
index 8126c596c0..9011db72bb 100644
--- a/client/modules/IDE/hooks/useWhatPage.js
+++ b/client/modules/IDE/hooks/useWhatPage.js
@@ -1,14 +1,33 @@
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+
+const getPageTitle = (pageName, t) => {
+ switch (pageName) {
+ case 'login':
+ return t('LoginView.Login');
+ case 'signup':
+ return t('LoginView.SignUp');
+ case 'account':
+ return t('AccountView.Settings');
+ case 'examples':
+ return t('Nav.File.Examples');
+ case 'myStuff':
+ return 'My Stuff';
+ case 'home':
+ default:
+ return null; // Will use project.name as fallback in Nav
+ }
+};
/**
- *
- * @returns {"home" | "myStuff" | "login" | "signup" | "account" | "examples"}
+ * @returns {{ page: "home" | "myStuff" | "login" | "signup" | "account" | "examples", title: string }}
*/
const useWhatPage = () => {
const username = useSelector((state) => state.user.username);
const { pathname } = useLocation();
+ const { t } = useTranslation();
const pageName = useMemo(() => {
const myStuffPattern = new RegExp(
@@ -24,7 +43,28 @@ const useWhatPage = () => {
return 'home';
}, [pathname, username]);
- return pageName;
+ const title = useMemo(() => getPageTitle(pageName, t), [pageName, t]);
+
+ // For backwards compatibility
+ const returnValue = {
+ pageName,
+ title
+ };
+
+ // Add a console warning in development
+ if (process.env.NODE_ENV === 'development') {
+ // Make the object look like a string for old code
+ Object.defineProperty(returnValue, 'toString', {
+ value: () => {
+ console.warn(
+ 'Deprecated: useWhatPage() now returns an object. Please update your code to use { page, title }.'
+ );
+ return pageName;
+ }
+ });
+ }
+
+ return returnValue;
};
export default useWhatPage;
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();
+ });
+ });
+});
From e5252b2d6cd490827c8cc7306c9cb9113c61ad5d Mon Sep 17 00:00:00 2001
From: Rajnish2105 <166371359+Rajnish2105@users.noreply.github.com>
Date: Wed, 25 Dec 2024 20:18:59 +0530
Subject: [PATCH 2/2] Removed accidental test insertion on user/pages for
AccountView
---
.../User/pages/AccountView.unit.test.jsx | 357 ------------------
1 file changed, 357 deletions(-)
delete mode 100644 client/modules/User/pages/AccountView.unit.test.jsx
diff --git a/client/modules/User/pages/AccountView.unit.test.jsx b/client/modules/User/pages/AccountView.unit.test.jsx
deleted file mode 100644
index f80229b11b..0000000000
--- a/client/modules/User/pages/AccountView.unit.test.jsx
+++ /dev/null
@@ -1,357 +0,0 @@
-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();
- });
- });
-});