diff --git a/client/modules/User/components/CollectionItemRow.jsx b/client/modules/User/components/CollectionItemRow.jsx index e6f3e6d30e..6a6584ccd1 100644 --- a/client/modules/User/components/CollectionItemRow.jsx +++ b/client/modules/User/components/CollectionItemRow.jsx @@ -6,6 +6,7 @@ import { useDispatch } from 'react-redux'; import { removeFromCollection } from '../../IDE/actions/collections'; import { formatDateToString } from '../../../utils/formatDate'; import RemoveIcon from '../../../images/close.svg'; +import { Tooltip } from '../../../common/Tooltip'; const CollectionItemRow = ({ collection, item, isOwner }) => { const { t } = useTranslation(); @@ -48,13 +49,15 @@ const CollectionItemRow = ({ collection, item, isOwner }) => { {sketchOwnerUsername} {isOwner && ( - + + + )} diff --git a/client/modules/User/components/CollectionItemRow.unit.test.jsx b/client/modules/User/components/CollectionItemRow.unit.test.jsx new file mode 100644 index 0000000000..933a5e9d07 --- /dev/null +++ b/client/modules/User/components/CollectionItemRow.unit.test.jsx @@ -0,0 +1,108 @@ +import React from 'react'; +import thunk from 'redux-thunk'; +import configureStore from 'redux-mock-store'; +import { reduxRender, screen, fireEvent, act } from '../../../test-utils'; +import { initialTestState } from '../../../testData/testReduxStore'; +import CollectionItemRow from './CollectionItemRow'; + +jest.mock('../../../i18n'); + +const mockStore = configureStore([thunk]); +const store = mockStore(initialTestState); + +let subjectProps = { + collection: { + id: 'collection-123', + name: 'Test Collection' + }, + item: { + createdAt: '2026-01-15T10:30:00.000Z', + isDeleted: false, + project: { + id: 'project-456', + name: 'My Sketch', + user: { username: 'testuser' }, + visibility: 'Public' + } + }, + isOwner: true +}; + +const subject = () => { + reduxRender( + + + + +
, + { store } + ); +}; + +describe('', () => { + afterEach(() => { + store.clearActions(); + }); + + it('renders the sketch name as a link', () => { + subject(); + const link = screen.getByRole('link', { name: 'My Sketch' }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', '/testuser/sketches/project-456'); + }); + + it('renders the owner username', () => { + subject(); + expect(screen.getByText('testuser')).toBeInTheDocument(); + }); + + it('shows the remove button when user is the owner', () => { + subject(); + expect(screen.getByRole('button', { name: /remove/i })).toBeInTheDocument(); + }); + + describe('when user is not the owner', () => { + beforeAll(() => { + subjectProps = { ...subjectProps, isOwner: false }; + }); + + afterAll(() => { + subjectProps = { ...subjectProps, isOwner: true }; + }); + + it('does not show the remove button', () => { + subject(); + expect( + screen.queryByRole('button', { name: /remove/i }) + ).not.toBeInTheDocument(); + }); + }); + + it('wraps the remove button with a tooltip', async () => { + subject(); + + const button = screen.getByRole('button', { name: /remove/i }); + await act(async () => { + fireEvent.mouseEnter(button); + }); + expect(button).toHaveClass('tooltipped'); + }); + + describe('when the project is deleted', () => { + beforeAll(() => { + subjectProps = { + ...subjectProps, + item: { + ...subjectProps.item, + isDeleted: true, + project: undefined + } + }; + }); + + it('shows the deleted sketch label', () => { + subject(); + expect(screen.getByText('Sketch deleted')).toBeInTheDocument(); + }); + }); +}); diff --git a/client/styles/components/_collection.scss b/client/styles/components/_collection.scss index bc5f9db5db..adfc1caa5c 100644 --- a/client/styles/components/_collection.scss +++ b/client/styles/components/_collection.scss @@ -148,6 +148,22 @@ .collection-row__action-column { width: #{math.div(60, $base-font-size)}rem; position: relative; + + .tooltip-wrapper { + display: inline-flex; + width: auto; + + .tooltipped::after { + right: 0; + left: auto; + } + + .tooltipped-n::before, + .tooltipped::before { + right: #{math.div(10, $base-font-size)}rem; + left: auto; + } + } } .collection-row__remove-button {