From db91a00aca04f4b27b52309fb4c95852a43735e2 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Fri, 5 May 2023 08:35:11 -0400 Subject: [PATCH] Add more Jest tests for various Group components --- .../__tests__/group-header.test.tsx | 46 +++ .../__tests__/group-member-list-item.test.tsx | 320 ++++++++++++++++++ .../__tests__/group-tag-list-item.test.tsx | 123 +++++++ .../group/components/group-header.tsx | 14 +- .../components/group-member-list-item.tsx | 7 +- .../group/components/group-tag-list-item.tsx | 8 +- app/soapbox/jest/factory.ts | 28 +- 7 files changed, 537 insertions(+), 9 deletions(-) create mode 100644 app/soapbox/features/group/components/__tests__/group-header.test.tsx create mode 100644 app/soapbox/features/group/components/__tests__/group-member-list-item.test.tsx create mode 100644 app/soapbox/features/group/components/__tests__/group-tag-list-item.test.tsx diff --git a/app/soapbox/features/group/components/__tests__/group-header.test.tsx b/app/soapbox/features/group/components/__tests__/group-header.test.tsx new file mode 100644 index 000000000..03f171e14 --- /dev/null +++ b/app/soapbox/features/group/components/__tests__/group-header.test.tsx @@ -0,0 +1,46 @@ +import React from 'react'; + +import { buildGroup } from 'soapbox/jest/factory'; +import { render, screen } from 'soapbox/jest/test-helpers'; +import { Group } from 'soapbox/types/entities'; + +import GroupHeader from '../group-header'; + +let group: Group; + +describe('', () => { + describe('without a group', () => { + it('should render the blankslate', () => { + render(); + expect(screen.getByTestId('group-header-missing')).toBeInTheDocument(); + }); + }); + + describe('when the Group has been deleted', () => { + it('only shows name, header, and avatar', () => { + group = buildGroup({ display_name: 'my group', deleted_at: new Date().toISOString() }); + render(); + + expect(screen.queryAllByTestId('group-header-missing')).toHaveLength(0); + expect(screen.queryAllByTestId('group-actions')).toHaveLength(0); + expect(screen.queryAllByTestId('group-meta')).toHaveLength(0); + expect(screen.getByTestId('group-header-image')).toBeInTheDocument(); + expect(screen.getByTestId('group-avatar')).toBeInTheDocument(); + expect(screen.getByTestId('group-name')).toBeInTheDocument(); + }); + }); + + describe('with a valid Group', () => { + it('only shows all fields', () => { + group = buildGroup({ display_name: 'my group', deleted_at: null }); + render(); + + expect(screen.queryAllByTestId('group-header-missing')).toHaveLength(0); + expect(screen.getByTestId('group-actions')).toBeInTheDocument(); + expect(screen.getByTestId('group-meta')).toBeInTheDocument(); + expect(screen.getByTestId('group-header-image')).toBeInTheDocument(); + expect(screen.getByTestId('group-avatar')).toBeInTheDocument(); + expect(screen.getByTestId('group-name')).toBeInTheDocument(); + }); + }); +}); \ No newline at end of file diff --git a/app/soapbox/features/group/components/__tests__/group-member-list-item.test.tsx b/app/soapbox/features/group/components/__tests__/group-member-list-item.test.tsx new file mode 100644 index 000000000..abecc3287 --- /dev/null +++ b/app/soapbox/features/group/components/__tests__/group-member-list-item.test.tsx @@ -0,0 +1,320 @@ +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { __stub } from 'soapbox/api'; +import { buildGroup, buildGroupMember, buildGroupRelationship } from 'soapbox/jest/factory'; +import { render, screen, waitFor } from 'soapbox/jest/test-helpers'; +import { GroupRoles } from 'soapbox/schemas/group-member'; + +import GroupMemberListItem from '../group-member-list-item'; + +describe('', () => { + describe('account rendering', () => { + const accountId = '4'; + const groupMember = buildGroupMember({}, { + id: accountId, + display_name: 'tiger woods', + }); + + beforeEach(() => { + __stub((mock) => { + mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); + }); + }); + + it('should render the users avatar', async () => { + const group = buildGroup({ + relationship: buildGroupRelationship(), + }); + + render(); + + await waitFor(() => { + expect(screen.getByTestId('group-member-list-item')).toHaveTextContent(groupMember.account.display_name); + }); + }); + }); + + describe('role badge', () => { + const accountId = '4'; + const group = buildGroup(); + + describe('when the user is an Owner', () => { + const groupMember = buildGroupMember({ role: GroupRoles.OWNER }, { + id: accountId, + display_name: 'tiger woods', + }); + + beforeEach(() => { + __stub((mock) => { + mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); + }); + }); + + it('should render the correct badge', async () => { + render(); + + await waitFor(() => { + expect(screen.getByTestId('role-badge')).toHaveTextContent('owner'); + }); + }); + }); + + describe('when the user is an Admin', () => { + const groupMember = buildGroupMember({ role: GroupRoles.ADMIN }, { + id: accountId, + display_name: 'tiger woods', + }); + + beforeEach(() => { + __stub((mock) => { + mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); + }); + }); + + it('should render the correct badge', async () => { + render(); + + await waitFor(() => { + expect(screen.getByTestId('role-badge')).toHaveTextContent('admin'); + }); + }); + }); + + describe('when the user is an User', () => { + const groupMember = buildGroupMember({ role: GroupRoles.USER }, { + id: accountId, + display_name: 'tiger woods', + }); + + beforeEach(() => { + __stub((mock) => { + mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); + }); + }); + + it('should render no correct badge', async () => { + render(); + + await waitFor(() => { + expect(screen.queryAllByTestId('role-badge')).toHaveLength(0); + }); + }); + }); + }); + + describe('as a Group owner', () => { + const group = buildGroup({ + relationship: buildGroupRelationship({ + role: GroupRoles.OWNER, + member: true, + }), + }); + + describe('when the user has role of "user"', () => { + const accountId = '4'; + const groupMember = buildGroupMember({}, { + id: accountId, + display_name: 'tiger woods', + username: 'tiger', + }); + + beforeEach(() => { + __stub((mock) => { + mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); + }); + }); + + describe('when "canPromoteToAdmin is true', () => { + it('should render dropdown with correct Owner actions', async () => { + const user = userEvent.setup(); + + render(); + + await waitFor(async() => { + await user.click(screen.getByTestId('icon-button')); + }); + + const dropdownMenu = screen.getByTestId('dropdown-menu'); + expect(dropdownMenu).toHaveTextContent('Assign admin role'); + expect(dropdownMenu).toHaveTextContent('Kick @tiger from group'); + expect(dropdownMenu).toHaveTextContent('Ban from group'); + }); + }); + + describe('when "canPromoteToAdmin is false', () => { + it('should prevent promoting user to Admin', async () => { + const user = userEvent.setup(); + + render(); + + await waitFor(async() => { + await user.click(screen.getByTestId('icon-button')); + await user.click(screen.getByTitle('Assign admin role')); + }); + + expect(screen.getByTestId('toast')).toHaveTextContent('Admin limit reached'); + }); + }); + }); + + describe('when the user has role of "admin"', () => { + const accountId = '4'; + const groupMember = buildGroupMember( + { + role: GroupRoles.ADMIN, + }, + { + id: accountId, + display_name: 'tiger woods', + username: 'tiger', + }, + ); + + beforeEach(() => { + __stub((mock) => { + mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); + }); + }); + + it('should render dropdown with correct Owner actions', async () => { + const user = userEvent.setup(); + + render(); + + await waitFor(async() => { + await user.click(screen.getByTestId('icon-button')); + }); + + const dropdownMenu = screen.getByTestId('dropdown-menu'); + expect(dropdownMenu).toHaveTextContent('Remove admin role'); + expect(dropdownMenu).toHaveTextContent('Kick @tiger from group'); + expect(dropdownMenu).toHaveTextContent('Ban from group'); + }); + }); + }); + + describe('as a Group admin', () => { + const group = buildGroup({ + relationship: buildGroupRelationship({ + role: GroupRoles.ADMIN, + member: true, + }), + }); + + describe('when the user has role of "user"', () => { + const accountId = '4'; + const groupMember = buildGroupMember({}, { + id: accountId, + display_name: 'tiger woods', + username: 'tiger', + }); + + beforeEach(() => { + __stub((mock) => { + mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); + }); + }); + + it('should render dropdown with correct Admin actions', async () => { + const user = userEvent.setup(); + + render(); + + await waitFor(async() => { + await user.click(screen.getByTestId('icon-button')); + }); + + const dropdownMenu = screen.getByTestId('dropdown-menu'); + expect(dropdownMenu).not.toHaveTextContent('Assign admin role'); + expect(dropdownMenu).toHaveTextContent('Kick @tiger from group'); + expect(dropdownMenu).toHaveTextContent('Ban from group'); + }); + }); + + describe('when the user has role of "admin"', () => { + const accountId = '4'; + const groupMember = buildGroupMember( + { + role: GroupRoles.ADMIN, + }, + { + id: accountId, + display_name: 'tiger woods', + username: 'tiger', + }, + ); + + beforeEach(() => { + __stub((mock) => { + mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); + }); + }); + + it('should not render the dropdown', async () => { + render(); + + await waitFor(async() => { + expect(screen.queryAllByTestId('icon-button')).toHaveLength(0); + }); + }); + }); + + describe('when the user has role of "owner"', () => { + const accountId = '4'; + const groupMember = buildGroupMember( + { + role: GroupRoles.OWNER, + }, + { + id: accountId, + display_name: 'tiger woods', + username: 'tiger', + }, + ); + + beforeEach(() => { + __stub((mock) => { + mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); + }); + }); + + it('should not render the dropdown', async () => { + render(); + + await waitFor(async() => { + expect(screen.queryAllByTestId('icon-button')).toHaveLength(0); + }); + }); + }); + }); + + describe('as a Group user', () => { + const group = buildGroup({ + relationship: buildGroupRelationship({ + role: GroupRoles.USER, + member: true, + }), + }); + const accountId = '4'; + const groupMember = buildGroupMember({}, { + id: accountId, + display_name: 'tiger woods', + username: 'tiger', + }); + + beforeEach(() => { + __stub((mock) => { + mock.onGet(`/api/v1/accounts/${accountId}`).reply(200, groupMember.account); + }); + }); + + it('should not render the dropdown', async () => { + render(); + + await waitFor(async() => { + expect(screen.queryAllByTestId('icon-button')).toHaveLength(0); + }); + }); + }); +}); \ No newline at end of file diff --git a/app/soapbox/features/group/components/__tests__/group-tag-list-item.test.tsx b/app/soapbox/features/group/components/__tests__/group-tag-list-item.test.tsx new file mode 100644 index 000000000..4418fff86 --- /dev/null +++ b/app/soapbox/features/group/components/__tests__/group-tag-list-item.test.tsx @@ -0,0 +1,123 @@ +import React from 'react'; + +import { buildGroup, buildGroupTag, buildGroupRelationship } from 'soapbox/jest/factory'; +import { render, screen } from 'soapbox/jest/test-helpers'; +import { GroupRoles } from 'soapbox/schemas/group-member'; + +import GroupTagListItem from '../group-tag-list-item'; + +describe('', () => { + describe('tag name', () => { + const name = 'hello'; + + it('should render the tag name', () => { + const group = buildGroup(); + const tag = buildGroupTag({ name }); + render(); + + expect(screen.getByTestId('group-tag-list-item')).toHaveTextContent(`#${name}`); + }); + + describe('when the tag is "visible"', () => { + const group = buildGroup(); + const tag = buildGroupTag({ name, visible: true }); + + it('renders the default name', () => { + render(); + expect(screen.getByTestId('group-tag-name')).toHaveClass('text-gray-900'); + }); + }); + + describe('when the tag is not "visible" and user is Owner', () => { + const group = buildGroup({ + relationship: buildGroupRelationship({ + role: GroupRoles.OWNER, + member: true, + }), + }); + const tag = buildGroupTag({ + name, + visible: false, + }); + + it('renders the subtle name', () => { + render(); + expect(screen.getByTestId('group-tag-name')).toHaveClass('text-gray-400'); + }); + }); + + describe('when the tag is not "visible" and user is Admin or User', () => { + const group = buildGroup({ + relationship: buildGroupRelationship({ + role: GroupRoles.ADMIN, + member: true, + }), + }); + const tag = buildGroupTag({ + name, + visible: false, + }); + + it('renders the subtle name', () => { + render(); + expect(screen.getByTestId('group-tag-name')).toHaveClass('text-gray-900'); + }); + }); + }); + + describe('pinning', () => { + describe('as an owner', () => { + const group = buildGroup({ + relationship: buildGroupRelationship({ + role: GroupRoles.OWNER, + member: true, + }), + }); + + describe('when the tag is visible', () => { + const tag = buildGroupTag({ visible: true }); + + it('renders the pin icon', () => { + render(); + expect(screen.getByTestId('pin-icon')).toBeInTheDocument(); + }); + }); + + describe('when the tag is not visible', () => { + const tag = buildGroupTag({ visible: false }); + + it('does not render the pin icon', () => { + render(); + expect(screen.queryAllByTestId('pin-icon')).toHaveLength(0); + }); + }); + + describe('as a non-owner', () => { + const group = buildGroup({ + relationship: buildGroupRelationship({ + role: GroupRoles.ADMIN, + member: true, + }), + }); + + describe('when the tag is visible', () => { + const tag = buildGroupTag({ visible: true }); + + it('does not render the pin icon', () => { + render(); + expect(screen.queryAllByTestId('pin-icon')).toHaveLength(0); + }); + }); + + describe('when the tag is not visible', () => { + const tag = buildGroupTag({ visible: false }); + + it('does not render the pin icon', () => { + render(); + expect(screen.queryAllByTestId('pin-icon')).toHaveLength(0); + }); + }); + }); + }); + }); +}); \ No newline at end of file diff --git a/app/soapbox/features/group/components/group-header.tsx b/app/soapbox/features/group/components/group-header.tsx index 9626906d5..ad22e1e13 100644 --- a/app/soapbox/features/group/components/group-header.tsx +++ b/app/soapbox/features/group/components/group-header.tsx @@ -34,7 +34,7 @@ const GroupHeader: React.FC = ({ group }) => { if (!group) { return ( -
+
@@ -107,7 +107,10 @@ const GroupHeader: React.FC = ({ group }) => { } return ( -
+
{isHeaderMissing ? ( ) : header} @@ -120,7 +123,7 @@ const GroupHeader: React.FC = ({ group }) => {
{renderHeader()} -
+
= ({ group }) => { size='xl' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} + data-testid='group-name' /> {!isDeleted && ( <> - + @@ -154,7 +158,7 @@ const GroupHeader: React.FC = ({ group }) => { /> - + diff --git a/app/soapbox/features/group/components/group-member-list-item.tsx b/app/soapbox/features/group/components/group-member-list-item.tsx index d2226879a..f9b18735d 100644 --- a/app/soapbox/features/group/components/group-member-list-item.tsx +++ b/app/soapbox/features/group/components/group-member-list-item.tsx @@ -180,7 +180,11 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => { } return ( - +
@@ -188,6 +192,7 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => { {(isMemberOwner || isMemberAdmin) ? ( { require('@tabler/icons/pin.svg') } iconClassName='h-5 w-5 text-primary-500 dark:text-accent-blue' + data-testid='pin-icon' /> ); @@ -123,13 +124,18 @@ const GroupTagListItem = (props: IGroupMemberListItem) => { }; return ( - + #{tag.name} diff --git a/app/soapbox/jest/factory.ts b/app/soapbox/jest/factory.ts index 07f4bc7d2..35ea063e0 100644 --- a/app/soapbox/jest/factory.ts +++ b/app/soapbox/jest/factory.ts @@ -1,23 +1,34 @@ import { v4 as uuidv4 } from 'uuid'; import { + accountSchema, adSchema, cardSchema, - groupSchema, + groupMemberSchema, groupRelationshipSchema, + groupSchema, groupTagSchema, relationshipSchema, + type Account, type Ad, type Card, type Group, + type GroupMember, type GroupRelationship, type GroupTag, type Relationship, } from 'soapbox/schemas'; +import { GroupRoles } from 'soapbox/schemas/group-member'; // TODO: there's probably a better way to create these factory functions. // This looks promising but didn't work on my first attempt: https://github.com/anatine/zod-plugins/tree/main/packages/zod-mock +function buildAccount(props: Partial = {}): Account { + return accountSchema.parse(Object.assign({ + id: uuidv4(), + }, props)); +} + function buildCard(props: Partial = {}): Card { return cardSchema.parse(Object.assign({ url: 'https://soapbox.test', @@ -39,6 +50,18 @@ function buildGroupRelationship(props: Partial = {}): GroupRe function buildGroupTag(props: Partial = {}): GroupTag { return groupTagSchema.parse(Object.assign({ id: uuidv4(), + name: uuidv4(), + }, props)); +} + +function buildGroupMember( + props: Partial = {}, + accountProps: Partial = {}, +): GroupMember { + return groupMemberSchema.parse(Object.assign({ + id: uuidv4(), + account: buildAccount(accountProps), + role: GroupRoles.USER, }, props)); } @@ -55,10 +78,11 @@ function buildRelationship(props: Partial = {}): Relationship { } export { + buildAd, buildCard, buildGroup, + buildGroupMember, buildGroupRelationship, buildGroupTag, - buildAd, buildRelationship, }; \ No newline at end of file