Merge branch 'group-tests' into 'develop'

Group tests

See merge request soapbox-pub/soapbox!2502
environments/review-develop-3zknud/deployments/3342
Chewbacca 1 year ago
commit a35d95e9e3

@ -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('<GroupHeader />', () => {
describe('without a group', () => {
it('should render the blankslate', () => {
render(<GroupHeader group={null} />);
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(<GroupHeader group={group} />);
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(<GroupHeader group={group} />);
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();
});
});
});

@ -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('<GroupMemberListItem />', () => {
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(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
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(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
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(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
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(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
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(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
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(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin={false} />);
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(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
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(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
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(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
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(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
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(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
await waitFor(async() => {
expect(screen.queryAllByTestId('icon-button')).toHaveLength(0);
});
});
});
});

@ -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('<GroupTagListItem />', () => {
describe('tag name', () => {
const name = 'hello';
it('should render the tag name', () => {
const group = buildGroup();
const tag = buildGroupTag({ name });
render(<GroupTagListItem group={group} tag={tag} isPinnable />);
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(<GroupTagListItem group={group} tag={tag} isPinnable />);
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(<GroupTagListItem group={group} tag={tag} isPinnable />);
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(<GroupTagListItem group={group} tag={tag} isPinnable />);
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(<GroupTagListItem group={group} tag={tag} isPinnable />);
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(<GroupTagListItem group={group} tag={tag} isPinnable />);
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(<GroupTagListItem group={group} tag={tag} isPinnable />);
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(<GroupTagListItem group={group} tag={tag} isPinnable />);
expect(screen.queryAllByTestId('pin-icon')).toHaveLength(0);
});
});
});
});
});
});

@ -34,7 +34,7 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
if (!group) {
return (
<div className='-mx-4 -mt-4 sm:-mx-6 sm:-mt-6'>
<div className='-mx-4 -mt-4 sm:-mx-6 sm:-mt-6' data-testid='group-header-missing'>
<div>
<div className='relative h-32 w-full bg-gray-200 dark:bg-gray-900/50 md:rounded-t-xl lg:h-48' />
</div>
@ -107,7 +107,10 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
}
return (
<div className='flex h-32 w-full items-center justify-center bg-gray-200 dark:bg-gray-800/30 md:rounded-t-xl lg:h-52'>
<div
data-testid='group-header-image'
className='flex h-32 w-full items-center justify-center bg-gray-200 dark:bg-gray-800/30 md:rounded-t-xl lg:h-52'
>
{isHeaderMissing ? (
<Icon src={require('@tabler/icons/photo-off.svg')} className='h-6 w-6 text-gray-500 dark:text-gray-700' />
) : header}
@ -120,7 +123,7 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
<div className='relative'>
{renderHeader()}
<div className='absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2'>
<div className='absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2' data-testid='group-avatar'>
<a href={group.avatar} onClick={handleAvatarClick} target='_blank'>
<GroupAvatar
group={group}
@ -136,11 +139,12 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
size='xl'
weight='bold'
dangerouslySetInnerHTML={{ __html: group.display_name_html }}
data-testid='group-name'
/>
{!isDeleted && (
<>
<Stack space={1} alignItems='center'>
<Stack data-testid='group-meta' space={1} alignItems='center'>
<HStack className='text-gray-700 dark:text-gray-600' space={2} wrap>
<GroupRelationship group={group} />
<GroupPrivacy group={group} />
@ -154,7 +158,7 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
/>
</Stack>
<HStack alignItems='center' space={2}>
<HStack alignItems='center' space={2} data-testid='group-actions'>
<GroupOptionsButton group={group} />
<GroupActionButton group={group} />
</HStack>

@ -180,7 +180,11 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => {
}
return (
<HStack alignItems='center' justifyContent='between'>
<HStack
alignItems='center'
justifyContent='between'
data-testid='group-member-list-item'
>
<div className='w-full'>
<Account account={member.account} withRelationship={false} />
</div>
@ -188,6 +192,7 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => {
<HStack alignItems='center' space={2}>
{(isMemberOwner || isMemberAdmin) ? (
<span
data-testid='role-badge'
className={
clsx('inline-flex items-center rounded px-2 py-1 text-xs font-medium capitalize', {
'bg-primary-200 text-primary-500': isMemberOwner,

@ -102,6 +102,7 @@ const GroupTagListItem = (props: IGroupMemberListItem) => {
require('@tabler/icons/pin.svg')
}
iconClassName='h-5 w-5 text-primary-500 dark:text-accent-blue'
data-testid='pin-icon'
/>
</Tooltip>
);
@ -123,13 +124,18 @@ const GroupTagListItem = (props: IGroupMemberListItem) => {
};
return (
<HStack alignItems='center' justifyContent='between'>
<HStack
alignItems='center'
justifyContent='between'
data-testid='group-tag-list-item'
>
<Link to={`/group/${group.slug}/tag/${tag.id}`} className='group grow'>
<Stack>
<Text
weight='bold'
theme={(tag.visible || !isOwner) ? 'default' : 'subtle'}
className='group-hover:underline'
data-testid='group-tag-name'
>
#{tag.name}
</Text>
@ -137,7 +143,7 @@ const GroupTagListItem = (props: IGroupMemberListItem) => {
{intl.formatMessage(messages.total)}:
{' '}
<Text size='sm' theme='inherit' weight='semibold' tag='span'>
{shortNumberFormat(tag.groups)}
{shortNumberFormat(tag.uses)}
</Text>
</Text>
</Stack>

@ -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> = {}): Account {
return accountSchema.parse(Object.assign({
id: uuidv4(),
}, props));
}
function buildCard(props: Partial<Card> = {}): Card {
return cardSchema.parse(Object.assign({
url: 'https://soapbox.test',
@ -39,6 +50,18 @@ function buildGroupRelationship(props: Partial<GroupRelationship> = {}): GroupRe
function buildGroupTag(props: Partial<GroupTag> = {}): GroupTag {
return groupTagSchema.parse(Object.assign({
id: uuidv4(),
name: uuidv4(),
}, props));
}
function buildGroupMember(
props: Partial<GroupMember> = {},
accountProps: Partial<Account> = {},
): GroupMember {
return groupMemberSchema.parse(Object.assign({
id: uuidv4(),
account: buildAccount(accountProps),
role: GroupRoles.USER,
}, props));
}
@ -55,10 +78,11 @@ function buildRelationship(props: Partial<Relationship> = {}): Relationship {
}
export {
buildAd,
buildCard,
buildGroup,
buildGroupMember,
buildGroupRelationship,
buildGroupTag,
buildAd,
buildRelationship,
};

@ -123,17 +123,19 @@ const GroupPage: React.FC<IGroupPage> = ({ params, children }) => {
});
}
items.push({
text: intl.formatMessage(messages.members),
to: `/group/${group?.slug}/members`,
name: '/group/:groupSlug/members',
count: pending.length,
},
{
text: intl.formatMessage(messages.media),
to: `/group/${group?.slug}/media`,
name: '/group/:groupSlug/media',
});
items.push(
{
text: intl.formatMessage(messages.media),
to: `/group/${group?.slug}/media`,
name: '/group/:groupSlug/media',
},
{
text: intl.formatMessage(messages.members),
to: `/group/${group?.slug}/members`,
name: '/group/:groupSlug/members',
count: pending.length,
},
);
return items;
}, [features.groupsTags, pending.length]);

@ -5,6 +5,7 @@ const groupTagSchema = z.object({
name: z.string(),
groups: z.number().optional(),
url: z.string().optional(),
uses: z.number().optional(),
pinned: z.boolean().optional().catch(false),
visible: z.boolean().optional().default(true),
});

Loading…
Cancel
Save