import Vue from 'vue'; import VueApollo from 'vue-apollo'; import { GlTable } from '@gitlab/ui'; import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; import TimelogsTable from '~/time_tracking/components/timelogs_table.vue'; import TimelogSourceCell from '~/time_tracking/components/timelog_source_cell.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import { STATUS_OPEN, STATUS_CLOSED, STATUS_MERGED } from '~/issues/constants'; const baseTimelogMock = { timeSpent: 600, project: { fullPath: 'group/project', }, user: { name: 'John Smith', avatarUrl: 'https://example.gitlab.com/john.jpg', webPath: 'https://example.gitlab.com/john', }, spentAt: '2023-03-27T21:00:00Z', note: null, summary: 'Summary from timelog field', issue: { title: 'Issue title', webUrl: 'https://example.gitlab.com/issue_url_a', state: STATUS_OPEN, reference: '#111', }, mergeRequest: null, }; const timelogsMock = [ baseTimelogMock, { timeSpent: 3600, project: { fullPath: 'group/project_b', }, user: { name: 'Paul Reed', avatarUrl: 'https://example.gitlab.com/paul.jpg', webPath: 'https://example.gitlab.com/paul', }, spentAt: '2023-03-28T16:00:00Z', note: { body: 'Summary from the body', }, summary: null, issue: { title: 'Other issue title', webUrl: 'https://example.gitlab.com/issue_url_b', state: STATUS_CLOSED, reference: '#112', }, mergeRequest: null, }, { timeSpent: 27 * 60 * 60, // 27h or 3d 3h (3 days of 8 hours) project: { fullPath: 'group/project_b', }, user: { name: 'Les Gibbons', avatarUrl: 'https://example.gitlab.com/les.jpg', webPath: 'https://example.gitlab.com/les', }, spentAt: '2023-03-28T18:00:00Z', note: null, summary: 'Other timelog summary', issue: null, mergeRequest: { title: 'MR title', webUrl: 'https://example.gitlab.com/mr_url', state: STATUS_MERGED, reference: '!99', }, }, ]; describe('TimelogsTable component', () => { Vue.use(VueApollo); let wrapper; const findTable = () => wrapper.findComponent(GlTable); const findTableRows = () => findTable().find('tbody').findAll('tr'); const findRowSpentAt = (rowIndex) => extendedWrapper(findTableRows().at(rowIndex)).findByTestId('date-container'); const findRowSource = (rowIndex) => findTableRows().at(rowIndex).findComponent(TimelogSourceCell); const findRowUser = (rowIndex) => findTableRows().at(rowIndex).findComponent(UserAvatarLink); const findRowTimeSpent = (rowIndex) => extendedWrapper(findTableRows().at(rowIndex)).findByTestId('time-spent-container'); const findRowSummary = (rowIndex) => extendedWrapper(findTableRows().at(rowIndex)).findByTestId('summary-container'); const mountComponent = (props = {}) => { wrapper = mountExtended(TimelogsTable, { propsData: { entries: timelogsMock, limitToHours: false, ...props, }, stubs: { GlTable }, }); }; describe('when there are no entries', () => { it('show the empty table message and no rows', () => { mountComponent({ entries: [] }); expect(findTable().text()).toContain('There are no records to show'); expect(findTableRows()).toHaveLength(1); }); }); describe('when there are some entries', () => { it('does not show the empty table message and has the correct number of rows', () => { mountComponent(); expect(findTable().text()).not.toContain('There are no records to show'); expect(findTableRows()).toHaveLength(3); }); describe('Spent at column', () => { it('shows the spent at value with in the correct format', () => { mountComponent(); expect(findRowSpentAt(0).text()).toBe('March 27, 2023, 21:00 (UTC: +0000)'); }); }); describe('Source column', () => { it('creates the source cell component passing the right props', () => { mountComponent(); expect(findRowSource(0).props()).toMatchObject({ timelog: timelogsMock[0], }); expect(findRowSource(1).props()).toMatchObject({ timelog: timelogsMock[1], }); expect(findRowSource(2).props()).toMatchObject({ timelog: timelogsMock[2], }); }); }); describe('User column', () => { it('creates the user avatar component passing the right props', () => { mountComponent(); expect(findRowUser(0).props()).toMatchObject({ linkHref: timelogsMock[0].user.webPath, imgSrc: timelogsMock[0].user.avatarUrl, imgSize: 16, imgAlt: timelogsMock[0].user.name, tooltipText: timelogsMock[0].user.name, username: timelogsMock[0].user.name, }); expect(findRowUser(1).props()).toMatchObject({ linkHref: timelogsMock[1].user.webPath, imgSrc: timelogsMock[1].user.avatarUrl, imgSize: 16, imgAlt: timelogsMock[1].user.name, tooltipText: timelogsMock[1].user.name, username: timelogsMock[1].user.name, }); expect(findRowUser(2).props()).toMatchObject({ linkHref: timelogsMock[2].user.webPath, imgSrc: timelogsMock[2].user.avatarUrl, imgSize: 16, imgAlt: timelogsMock[2].user.name, tooltipText: timelogsMock[2].user.name, username: timelogsMock[2].user.name, }); }); }); describe('Time spent column', () => { it('shows the time spent value with the correct format when `limitToHours` is false', () => { mountComponent(); expect(findRowTimeSpent(0).text()).toBe('10m'); expect(findRowTimeSpent(1).text()).toBe('1h'); expect(findRowTimeSpent(2).text()).toBe('3d 3h'); }); it('shows the time spent value with the correct format when `limitToHours` is true', () => { mountComponent({ limitToHours: true }); expect(findRowTimeSpent(0).text()).toBe('10m'); expect(findRowTimeSpent(1).text()).toBe('1h'); expect(findRowTimeSpent(2).text()).toBe('27h'); }); }); describe('Summary column', () => { it('shows the summary from the note when note body is present and not empty', () => { mountComponent({ entries: [{ ...baseTimelogMock, note: { body: 'Summary from note body' } }], }); expect(findRowSummary(0).text()).toBe('Summary from note body'); }); it('shows the summary from the timelog note body is present but empty', () => { mountComponent({ entries: [{ ...baseTimelogMock, note: { body: '' } }], }); expect(findRowSummary(0).text()).toBe('Summary from timelog field'); }); it('shows the summary from the timelog note body is not present', () => { mountComponent({ entries: [baseTimelogMock], }); expect(findRowSummary(0).text()).toBe('Summary from timelog field'); }); }); }); });