diff --git a/packages/components/cypress/fixtures/tsl-logo.png b/packages/components/cypress/fixtures/tsl-logo.png new file mode 100755 index 00000000..4ee46ba8 Binary files /dev/null and b/packages/components/cypress/fixtures/tsl-logo.png differ diff --git a/packages/components/modules/profiles/web/ProfileSettingsComponent/__tests__/ProfileSettings.cy.tsx b/packages/components/modules/profiles/web/ProfileSettingsComponent/__tests__/ProfileSettings.cy.tsx new file mode 100644 index 00000000..394d9e73 --- /dev/null +++ b/packages/components/modules/profiles/web/ProfileSettingsComponent/__tests__/ProfileSettings.cy.tsx @@ -0,0 +1,214 @@ +import { createTestEnvironment } from '@baseapp-frontend/graphql' + +import { AppRouterContext } from 'next/dist/shared/lib/app-router-context.shared-runtime' + +import '../../../../../styles/tailwind/globals.css' +import { + mockDataWithBanner, + profileSettingsBannerRemoveData, + profileSettingsBannerUpdateData, + profileSettingsImageRemoveData, + profileSettingsImageUpdateData, + profileSettingsMockData, + profileSettingsTextUpdateData, +} from './__mocks__/requests' +import ProfileSettingsForTesting from './__utils__/ProfileSettingsForTesting' + +const nameInput = () => + cy.findByLabelText('Name', { exact: true, selector: 'input' }).as('nameInput') +const usernameInput = () => + cy.findByLabelText(/username/i, { selector: 'input' }).as('usernameInput') +const phoneInput = () => cy.findByLabelText(/phone number/i, { selector: 'input' }).as('phoneInput') +const bioInput = () => cy.findByLabelText(/bio/i).as('bioInput') +const avatarInput = () => + cy + .findByText(/change image/i) + .parent() + .find('input[type="file"]') + .as('avatarInput') +const bannerInput = () => + cy + .findByText(/change banner/i) + .parent() + .find('input[type="file"]') + .as('bannerInput') +const avatarFallbackImage = () => cy.get('img, [title="Avatar Fallback"]').as('avatarFallbackImage') +const avatarImage = () => cy.findByAltText('Avatar image').as('avatarImage') +const bannerImage = () => cy.findByAltText('Home Banner').as('bannerImage') +const changeAvatarButton = () => cy.findByText(/change image/i).as('changeAvatarButton') +const removeAvatarButton = () => + cy.findByRole('button', { name: /remove avatar button/i }).as('removeAvatarButton') +const changeBannerButton = () => cy.findByText(/change banner/i).as('changeBannerButton') +const removeBannerButton = () => + cy.findByLabelText(/remove banner button/i).as('removeBannerButton') +const saveChangesButton = () => + cy.findByRole('button', { name: /save changes/i }).as('saveChangesButton') + +describe('ProfileSettings', () => { + it('should render profile settings form elements with initial data', () => { + const { environment, queueOperationResolver } = createTestEnvironment() + queueOperationResolver({ + queryName: 'ProfileSettingsForTestingQuery', + data: profileSettingsMockData, + }) + cy.mockNextRouter().then((router) => { + cy.mount( + + + , + ) + }) + + cy.findByText('Profile').should('exist') + cy.findByText('Manage your personal information you and other people see.').should('exist') + nameInput().should('have.value', 'John Doe') + usernameInput().should('have.value', 'johndoes') + phoneInput().should('have.value', '+1 (555) 123-4567') + bioInput().should('have.value', 'John Doe is a software engineer at Google.') + avatarFallbackImage().should('exist') + changeAvatarButton().should('exist') + bannerImage().should('exist') + changeBannerButton().should('exist') + saveChangesButton().should('be.disabled') + }) + + it('should show validation errors for invalid input', () => { + const { environment, queueOperationResolver } = createTestEnvironment() + queueOperationResolver({ + queryName: 'ProfileSettingsForTestingQuery', + data: profileSettingsMockData, + }) + cy.mockNextRouter().then((router) => { + cy.mount( + + + , + ) + }) + + cy.step('Test name validation') + nameInput().clear().blur() + cy.findByText('Please enter a name.').should('exist') + cy.step('Test username validation') + usernameInput().clear().type('short').blur() + cy.findByText('Username must be at least 8 characters long.').should('exist') + cy.get('@usernameInput').clear().type('invalid-username').blur() + cy.findByText('Username can only contain letters and numbers').should('exist') + saveChangesButton().should('be.disabled') + }) + + it('should update text fields successfully', () => { + const { environment, resolveMostRecentOperation, queueOperationResolver } = + createTestEnvironment() + queueOperationResolver({ + queryName: 'ProfileSettingsForTestingQuery', + data: profileSettingsMockData, + }) + cy.mockNextRouter().then((router) => { + cy.mount( + + + , + ) + }) + + nameInput().clear().type('Jane Smith') + usernameInput().clear().type('janesmith') + phoneInput().clear().type('+1123456789') + bioInput().clear().type('Jane Smith is a software engineer at Microsoft.') + saveChangesButton().should('not.be.disabled') + cy.step('Submit the form') + cy.get('@saveChangesButton') + .click() + .then(() => { + resolveMostRecentOperation({ data: profileSettingsTextUpdateData }) + }) + cy.findByText('Profile updated').should('exist') + cy.get('@nameInput').should('have.value', 'Jane Smith') + cy.get('@usernameInput').should('have.value', 'janesmith') + cy.get('@phoneInput').should('have.value', '+1 (123) 456-789') + cy.get('@bioInput').should('have.value', 'Jane Smith is a software engineer at Microsoft.') + }) + + it('should update and remove avatar image successfully', () => { + const { environment, resolveMostRecentOperation, queueOperationResolver } = + createTestEnvironment() + queueOperationResolver({ + queryName: 'ProfileSettingsForTestingQuery', + data: profileSettingsMockData, + }) + cy.mockNextRouter().then((router) => { + cy.mount( + + + , + ) + }) + + cy.step('Update the avatar image') + avatarInput().selectFile('cypress/fixtures/tsl-logo.png', { force: true }) + changeAvatarButton().should('exist') + removeAvatarButton().should('exist') + saveChangesButton().should('not.be.disabled') + cy.step('Submit the form') + cy.get('@saveChangesButton') + .click() + .then(() => { + resolveMostRecentOperation({ data: profileSettingsImageUpdateData }) + }) + cy.findByText('Profile updated').should('exist') + avatarImage().should('exist') + cy.step('Remove the avatar image') + cy.findByRole('button', { name: /remove/i }) + .first() + .click() + saveChangesButton().should('not.be.disabled') + cy.step('Submit the form') + cy.get('@saveChangesButton') + .click() + .then(() => { + resolveMostRecentOperation({ data: profileSettingsImageRemoveData }) + }) + cy.findByText('Profile updated').should('exist') + }) + + it('should update and remove banner image successfully', () => { + const { environment, resolveMostRecentOperation, queueOperationResolver } = + createTestEnvironment() + queueOperationResolver({ + queryName: 'ProfileSettingsForTestingQuery', + data: profileSettingsMockData, + }) + cy.mockNextRouter().then((router) => { + cy.mount( + + + , + ) + }) + + cy.step('Update the banner image') + bannerInput().selectFile('cypress/fixtures/tsl-logo.png', { force: true }) + changeBannerButton().should('exist') + removeBannerButton().should('exist') + saveChangesButton().should('not.be.disabled') + cy.step('Submit the form') + cy.get('@saveChangesButton') + .click() + .then(() => { + resolveMostRecentOperation({ data: profileSettingsBannerUpdateData }) + }) + cy.findByText('Profile updated').should('exist') + bannerImage().should('exist') + cy.step('Remove the banner image') + cy.get('@removeBannerButton').click() + cy.get('@saveChangesButton').should('not.be.disabled') + cy.step('Submit the form') + cy.get('@saveChangesButton') + .click() + .then(() => { + resolveMostRecentOperation({ data: profileSettingsBannerRemoveData }) + }) + cy.findByText('Profile updated').should('exist') + }) +}) diff --git a/packages/components/modules/profiles/web/ProfileSettingsComponent/__tests__/__mocks__/requests.ts b/packages/components/modules/profiles/web/ProfileSettingsComponent/__tests__/__mocks__/requests.ts new file mode 100644 index 00000000..64f3b589 --- /dev/null +++ b/packages/components/modules/profiles/web/ProfileSettingsComponent/__tests__/__mocks__/requests.ts @@ -0,0 +1,198 @@ +export const profileSettingsMockData = { + data: { + target: { + __typename: 'Profile', + id: 'profile-123', + status: 'ACTIVE', + name: 'John Doe', + biography: 'John Doe is a software engineer at Google.', + image: null, + bannerImage: null, + isFollowedByMe: false, + followersCount: 0, + followingCount: 0, + canChange: true, + urlPath: { + path: '/johndoes', + }, + owner: { + phoneNumber: '+15551234567', + }, + isBlockedByMe: false, + }, + }, +} + +export const profileSettingsTextUpdateData = { + data: { + target: { + __typename: 'Profile', + id: `profile-123`, + status: 'ACTIVE', + name: 'Jane Smith', + biography: 'Jane Smith is a software engineer at Microsoft.', + image: { + url: null, + }, + bannerImage: { + url: null, + }, + isFollowedByMe: false, + followersCount: 0, + followingCount: 0, + canChange: true, + urlPath: { + path: '/janesmith', + }, + owner: { + phoneNumber: '+1123456789', + }, + isBlockedByMe: false, + }, + }, +} + +export const profileSettingsImageUpdateData = { + data: { + target: { + __typename: 'Profile', + id: `profile-123`, + status: 'ACTIVE', + name: 'John Doe', + biography: 'John Doe is a software engineer at Google.', + image: { + url: 'https://nyc3.digitaloceanspaces.com/baseapp-production-storage/media/user-avatars/5/6/4/resized/50/50/185a04dfdaa512d218cf9b7a5097e3c9.png', + }, + bannerImage: { + url: null, + }, + isFollowedByMe: false, + followersCount: 0, + followingCount: 0, + canChange: true, + urlPath: { + path: '/johndoes', + }, + owner: { + phoneNumber: '+1234567890', + }, + isBlockedByMe: false, + }, + }, +} + +export const profileSettingsImageRemoveData = { + data: { + target: { + __typename: 'Profile', + id: `profile-123`, + status: 'ACTIVE', + name: 'John Doe', + biography: 'John Doe is a software engineer at Google.', + image: { + url: null, + }, + bannerImage: { + url: null, + }, + isFollowedByMe: false, + followersCount: 0, + followingCount: 0, + canChange: true, + urlPath: { + path: '/johndoes', + }, + owner: { + phoneNumber: '+1234567890', + }, + isBlockedByMe: false, + }, + }, +} + +export const profileSettingsBannerUpdateData = { + data: { + target: { + __typename: 'Profile', + id: `profile-123`, + status: 'ACTIVE', + name: 'John Doe', + biography: 'John Doe is a software engineer at Google.', + image: { + url: null, + }, + bannerImage: { + url: 'https://nyc3.digitaloceanspaces.com/baseapp-production-storage/media/user-avatars/5/6/4/resized/50/50/185a04dfdaa512d218cf9b7a5097e3c9.png', + }, + isFollowedByMe: false, + followersCount: 0, + followingCount: 0, + canChange: true, + urlPath: { + path: '/johndoes', + }, + owner: { + phoneNumber: '+1234567890', + }, + isBlockedByMe: false, + }, + }, +} + +export const profileSettingsBannerRemoveData = { + data: { + target: { + __typename: 'Profile', + id: `profile-123`, + status: 'ACTIVE', + name: 'John Doe', + biography: 'John Doe is a software engineer at Google.', + image: { + url: null, + }, + bannerImage: { + url: null, + }, + isFollowedByMe: false, + followersCount: 0, + followingCount: 0, + canChange: true, + urlPath: { + path: '/johndoes', + }, + owner: { + phoneNumber: '+1234567890', + }, + isBlockedByMe: false, + }, + }, +} + +export const mockDataWithBanner = { + data: { + target: { + __typename: 'Profile', + id: 'profile-123', + status: 'ACTIVE', + name: 'John Doe', + biography: 'John Doe is a software engineer at Google.', + image: { + url: 'http://localhost:8000/media/profile_images/5dfe5729-1730-4fe6-b22a-a0f15f65a754.png.96x96_q85.png', + }, + bannerImage: { + url: 'http://localhost:8000/media/banner_images/test-banner.png', + }, + isFollowedByMe: false, + followersCount: 0, + followingCount: 0, + canChange: true, + urlPath: { + path: '/johndoes', + }, + owner: { + phoneNumber: '+1234567890', + }, + isBlockedByMe: false, + }, + }, +} diff --git a/packages/components/modules/profiles/web/ProfileSettingsComponent/__tests__/__utils__/ProfileSettingsForTesting/index.tsx b/packages/components/modules/profiles/web/ProfileSettingsComponent/__tests__/__utils__/ProfileSettingsForTesting/index.tsx new file mode 100644 index 00000000..5bfc4ce7 --- /dev/null +++ b/packages/components/modules/profiles/web/ProfileSettingsComponent/__tests__/__utils__/ProfileSettingsForTesting/index.tsx @@ -0,0 +1,24 @@ +import { graphql, useLazyLoadQuery } from 'react-relay' + +import { ProfileComponentFragment$key } from '../../../../../../../__generated__/ProfileComponentFragment.graphql' +import { ProfileSettingsForTestingQuery } from '../../../../../../../__generated__/ProfileSettingsForTestingQuery.graphql' +import { withComponentCompleteTestProviders } from '../../../../../../tests/web' +import ProfileSettings from '../../../index' +import { ProfileSettingsComponentProps } from '../../../types' + +const ProfileSettingsForTesting = (props?: Partial) => { + const data = useLazyLoadQuery( + graphql` + query ProfileSettingsForTestingQuery @relay_test_operation { + target: node(id: "test-id") { + ...ProfileComponentFragment + } + } + `, + {}, + ) + + return +} + +export default withComponentCompleteTestProviders(ProfileSettingsForTesting) diff --git a/packages/components/modules/profiles/web/ProfileSettingsComponent/index.tsx b/packages/components/modules/profiles/web/ProfileSettingsComponent/index.tsx index fef795a0..0aa791d3 100644 --- a/packages/components/modules/profiles/web/ProfileSettingsComponent/index.tsx +++ b/packages/components/modules/profiles/web/ProfileSettingsComponent/index.tsx @@ -150,6 +150,7 @@ const ProfileSettingsComponent: FC = ({ profile: width={144} height={144} hasError={!!getFieldState('image').error} + alt="Avatar image" /> {getFieldState('image').error && (
@@ -173,6 +174,7 @@ const ProfileSettingsComponent: FC = ({ profile: loading={isMutationInFlight} disabled={isMutationInFlight} onClick={() => handleRemoveImage(PROFILE_FORM_VALUE.image)} + aria-label="Remove avatar button" > Remove @@ -252,6 +254,7 @@ const ProfileSettingsComponent: FC = ({ profile: loading={isMutationInFlight} disabled={isMutationInFlight} sx={{ maxWidth: 'fit-content' }} + aria-label="Remove banner button" > Remove diff --git a/packages/design-system/components/web/avatars/AvatarWithPlaceholder/index.tsx b/packages/design-system/components/web/avatars/AvatarWithPlaceholder/index.tsx index 24dc1936..afd61818 100644 --- a/packages/design-system/components/web/avatars/AvatarWithPlaceholder/index.tsx +++ b/packages/design-system/components/web/avatars/AvatarWithPlaceholder/index.tsx @@ -8,9 +8,10 @@ const AvatarWithPlaceholder: FC = ({ width = 40, height = 40, children, + alt, ...props }) => ( - + {children || ( )} diff --git a/packages/design-system/components/web/avatars/CircledAvatar/index.tsx b/packages/design-system/components/web/avatars/CircledAvatar/index.tsx index 3f793f07..977a9d42 100644 --- a/packages/design-system/components/web/avatars/CircledAvatar/index.tsx +++ b/packages/design-system/components/web/avatars/CircledAvatar/index.tsx @@ -11,6 +11,7 @@ const CircledAvatar: FC = ({ height = 40, hasError, children, + alt, ...props }) => ( @@ -18,6 +19,7 @@ const CircledAvatar: FC = ({ width={width} height={height} sx={({ palette }) => ({ border: `solid 8px ${palette.background.default}` })} + alt={alt} {...props} > {children} diff --git a/packages/design-system/components/web/buttons/FileUploadButton/index.tsx b/packages/design-system/components/web/buttons/FileUploadButton/index.tsx index 136d3405..b78892b2 100644 --- a/packages/design-system/components/web/buttons/FileUploadButton/index.tsx +++ b/packages/design-system/components/web/buttons/FileUploadButton/index.tsx @@ -14,6 +14,7 @@ const FileUploadButton: FC = (props) => { const { sendToast } = useNotification() const { accept, maxSize, ...fileUploadProps } = props const { control, label, name, setFile } = fileUploadProps + return ( <>