gitlab-ce/spec/frontend/todos/components/todos_app_spec.js

298 lines
10 KiB
JavaScript

import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlLoadingIcon, GlTabs } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import setWindowLocation from 'helpers/set_window_location_helper';
import TodosApp from '~/todos/components/todos_app.vue';
import TodoItem from '~/todos/components/todo_item.vue';
import TodosFilterBar from '~/todos/components/todos_filter_bar.vue';
import TodosMarkAllDoneButton from '~/todos/components/todos_mark_all_done_button.vue';
import TodosPagination, { CURSOR_CHANGED_EVENT } from '~/todos/components/todos_pagination.vue';
import getTodosQuery from '~/todos/components/queries/get_todos.query.graphql';
import { getInstrumentTabLabels, getStatusByTab, TODO_WAIT_BEFORE_RELOAD } from '~/todos/constants';
import { mockTracking, unmockTracking } from 'jest/__helpers__/tracking_helper';
import getPendingTodosCount from '~/todos/components/queries/get_pending_todos_count.query.graphql';
import { todosResponse, getPendingTodosCountResponse } from '../mock_data';
Vue.use(VueApollo);
describe('TodosApp', () => {
let wrapper;
const todosQuerySuccessHandler = jest.fn().mockResolvedValue(todosResponse);
const todosCountsQuerySuccessHandler = jest.fn().mockResolvedValue(getPendingTodosCountResponse);
const createComponent = ({
todosQueryHandler = todosQuerySuccessHandler,
todosCountsQueryHandler = todosCountsQuerySuccessHandler,
glFeatures = { todosSnoozing: true },
} = {}) => {
const mockApollo = createMockApollo();
mockApollo.defaultClient.setRequestHandler(getTodosQuery, todosQueryHandler);
mockApollo.defaultClient.setRequestHandler(getPendingTodosCount, todosCountsQueryHandler);
wrapper = shallowMountExtended(TodosApp, {
apolloProvider: mockApollo,
provide: {
glFeatures,
},
});
};
const findTodoItems = () => wrapper.findAllComponents(TodoItem);
const findFirstTodoItem = () => wrapper.findComponent(TodoItem);
const findGlTabs = () => wrapper.findComponent(GlTabs);
const findFilterBar = () => wrapper.findComponent(TodosFilterBar);
const findMarkAllDoneButton = () => wrapper.findComponent(TodosMarkAllDoneButton);
const findRefreshButton = () => wrapper.findByTestId('refresh-todos');
const findPendingTodosCount = () => wrapper.findByTestId('pending-todos-count');
const findTodoItemListContainer = () => wrapper.findByTestId('todo-item-list-container');
const findPagination = () => wrapper.findComponent(TodosPagination);
beforeEach(() => {
gon.features = { todosSnoozing: true };
});
it('should have a tracking event for each tab', () => {
expect(getStatusByTab().length).toBe(getInstrumentTabLabels().length);
});
it('shows a loading state while fetching todos', () => {
createComponent();
expect(findTodoItems().length).toBe(0);
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('renders todo items once the query has resolved', async () => {
createComponent();
await waitForPromises();
expect(findTodoItems().length).toBe(todosResponse.data.currentUser.todos.nodes.length);
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
});
it('shows pagination for the todos', async () => {
createComponent();
await waitForPromises();
expect(findPagination().exists()).toBe(true);
});
it('fetches the todos when pagination changes', async () => {
createComponent();
await waitForPromises();
const newCursor = { first: 50, after: 'cursor-1' };
findPagination().vm.$emit(CURSOR_CHANGED_EVENT, newCursor);
await waitForPromises();
expect(todosQuerySuccessHandler).toHaveBeenLastCalledWith(
expect.objectContaining({
...newCursor,
}),
);
});
it('fetches the todos and counts when filters change', async () => {
createComponent();
const filters = {
groupId: ['1'],
projectId: ['2'],
authorId: ['3'],
type: ['4'],
action: ['assigned'],
sort: 'CREATED_DESC',
};
findFilterBar().vm.$emit('filters-changed', filters);
await waitForPromises();
expect(todosQuerySuccessHandler).toHaveBeenLastCalledWith({
...filters,
state: ['pending'],
first: 20,
last: null,
after: null,
before: null,
});
expect(todosCountsQuerySuccessHandler).toHaveBeenLastCalledWith(filters);
});
it('re-fetches the pending todos count when mark all done button is clicked', async () => {
createComponent();
await waitForPromises();
expect(todosCountsQuerySuccessHandler).toHaveBeenCalledTimes(1);
findMarkAllDoneButton().vm.$emit('change');
expect(todosCountsQuerySuccessHandler).toHaveBeenCalledTimes(2);
});
it('refreshes count and list', async () => {
createComponent();
await waitForPromises();
expect(todosCountsQuerySuccessHandler).toHaveBeenCalledTimes(1);
expect(todosQuerySuccessHandler).toHaveBeenCalledTimes(1);
findRefreshButton().vm.$emit('click', new Event('click'));
expect(todosCountsQuerySuccessHandler).toHaveBeenCalledTimes(2);
expect(todosQuerySuccessHandler).toHaveBeenCalledTimes(2);
});
it('shows the pending todos count once it has been fetched', async () => {
createComponent();
expect(findPendingTodosCount().text()).toBe('-');
await waitForPromises();
expect(findPendingTodosCount().text()).toBe(
String(getPendingTodosCountResponse.data.currentUser.todos.count),
);
});
it('refetches todos when page becomes visible again', async () => {
createComponent();
// Wait and account for initial query
await waitForPromises();
expect(todosQuerySuccessHandler).toHaveBeenCalledTimes(1);
expect(todosCountsQuerySuccessHandler).toHaveBeenCalledTimes(1);
// Make sure we don't refetch when document became hidden
jest.spyOn(document, 'hidden', 'get').mockReturnValue(true);
document.dispatchEvent(new Event('visibilitychange'));
await waitForPromises();
expect(todosQuerySuccessHandler).toHaveBeenCalledTimes(1);
expect(todosCountsQuerySuccessHandler).toHaveBeenCalledTimes(1);
// Expect refetch when document becomes visible
jest.spyOn(document, 'hidden', 'get').mockReturnValue(false);
document.dispatchEvent(new Event('visibilitychange'));
await waitForPromises();
expect(todosQuerySuccessHandler).toHaveBeenCalledTimes(2);
expect(todosCountsQuerySuccessHandler).toHaveBeenCalledTimes(2);
});
it('refetches todos one second after the cursor leaves the list of todos', async () => {
createComponent();
// Wait and account for initial query
await waitForPromises();
expect(todosQuerySuccessHandler).toHaveBeenCalledTimes(1);
expect(todosCountsQuerySuccessHandler).toHaveBeenCalledTimes(1);
// Simulate interacting with a todo item then mousing out of the list zone
findFirstTodoItem().vm.$emit('change');
const list = findTodoItemListContainer();
list.trigger('mouseleave');
// Should refresh the count, but not the list
await waitForPromises();
expect(todosQuerySuccessHandler).toHaveBeenCalledTimes(1);
expect(todosCountsQuerySuccessHandler).toHaveBeenCalledTimes(2);
// Run out the clock
jest.advanceTimersByTime(TODO_WAIT_BEFORE_RELOAD + 50); // 1s + some jitter
// Refreshes the count and the list
await waitForPromises();
expect(todosQuerySuccessHandler).toHaveBeenCalledTimes(2);
expect(todosCountsQuerySuccessHandler).toHaveBeenCalledTimes(3);
});
it('does not refresh todos after the cursor leaves the list of todos if nothing changed', async () => {
createComponent();
// Wait and account for initial query
await waitForPromises();
expect(todosQuerySuccessHandler).toHaveBeenCalledTimes(1);
expect(todosCountsQuerySuccessHandler).toHaveBeenCalledTimes(1);
// Simulate NOT interacting with a todo item then mousing out of the list zone
const list = findTodoItemListContainer();
list.trigger('mouseleave');
// Should not update anything
await waitForPromises();
expect(todosQuerySuccessHandler).toHaveBeenCalledTimes(1);
expect(todosCountsQuerySuccessHandler).toHaveBeenCalledTimes(1);
// Run out the clock
jest.advanceTimersByTime(TODO_WAIT_BEFORE_RELOAD + 50); // 1s + some jitter
// Should not update anything
await waitForPromises();
expect(todosQuerySuccessHandler).toHaveBeenCalledTimes(1);
expect(todosCountsQuerySuccessHandler).toHaveBeenCalledTimes(1);
});
it('passes the default status to the filter bar', () => {
createComponent();
expect(findFilterBar().props('todosStatus')).toEqual(['pending']);
});
it.each`
tabIndex | status | label
${0} | ${['pending']} | ${'status_pending'}
${1} | ${['pending']} | ${'status_snoozed'}
${2} | ${['done']} | ${'status_done'}
${3} | ${['pending', 'done']} | ${'status_all'}
`(
'sets the filter bar status to $status and tracks the event $label when the tab changes to index #$tabIndex',
async ({ tabIndex, status, label }) => {
createComponent();
// Navigate to another tab that isn't the target
findGlTabs().vm.$emit('input', tabIndex === 0 ? 1 : tabIndex - 1);
await nextTick();
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
findGlTabs().vm.$emit('input', tabIndex);
await nextTick();
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'filter_todo_list', {
label,
});
expect(findFilterBar().props('todosStatus')).toEqual(status);
unmockTracking();
},
);
it('syncs tab change to URL while leaving other params unchanged', () => {
setWindowLocation('?group_id=123');
createComponent();
expect(window.location.search).toBe('?group_id=123');
findGlTabs().vm.$emit('input', 1);
expect(window.location.search).toBe('?group_id=123&state=snoozed');
findGlTabs().vm.$emit('input', 2);
expect(window.location.search).toBe('?group_id=123&state=done');
findGlTabs().vm.$emit('input', 3);
expect(window.location.search).toBe('?group_id=123&state=all');
findGlTabs().vm.$emit('input', 0);
expect(window.location.search).toBe('?group_id=123');
});
describe('reading `state` param from URL', () => {
beforeEach(() => {
setWindowLocation('?state=done');
});
it('activates correct tab', () => {
createComponent();
expect(findGlTabs().props('value')).toBe(2);
});
});
});