diff options
Diffstat (limited to 'spec/frontend/logs')
-rw-r--r-- | spec/frontend/logs/components/environment_logs_spec.js | 210 | ||||
-rw-r--r-- | spec/frontend/logs/components/log_control_buttons_spec.js | 50 | ||||
-rw-r--r-- | spec/frontend/logs/mock_data.js | 70 | ||||
-rw-r--r-- | spec/frontend/logs/stores/actions_spec.js | 332 | ||||
-rw-r--r-- | spec/frontend/logs/stores/mutations_spec.js | 136 |
5 files changed, 503 insertions, 295 deletions
diff --git a/spec/frontend/logs/components/environment_logs_spec.js b/spec/frontend/logs/components/environment_logs_spec.js index 26542c3d046..c638b4c05f9 100644 --- a/spec/frontend/logs/components/environment_logs_spec.js +++ b/spec/frontend/logs/components/environment_logs_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import { GlDropdown, GlDropdownItem, GlSearchBoxByClick } from '@gitlab/ui'; +import { GlSprintf, GlDropdown, GlDropdownItem, GlSearchBoxByClick } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; import EnvironmentLogs from '~/logs/components/environment_logs.vue'; @@ -20,9 +20,18 @@ import { jest.mock('~/lib/utils/scroll_utils'); +const module = 'environmentLogs'; + +jest.mock('lodash/throttle', () => + jest.fn(func => { + return func; + }), +); + describe('EnvironmentLogs', () => { let EnvironmentLogsComponent; let store; + let dispatch; let wrapper; let state; @@ -32,14 +41,6 @@ describe('EnvironmentLogs', () => { clusterApplicationsDocumentationPath: mockDocumentationPath, }; - const actionMocks = { - setInitData: jest.fn(), - setSearch: jest.fn(), - showPodLogs: jest.fn(), - showEnvironment: jest.fn(), - fetchEnvironments: jest.fn(), - }; - const updateControlBtnsMock = jest.fn(); const findEnvironmentsDropdown = () => wrapper.find('.js-environments-dropdown'); @@ -47,24 +48,25 @@ describe('EnvironmentLogs', () => { const findSearchBar = () => wrapper.find('.js-logs-search'); const findTimeRangePicker = () => wrapper.find({ ref: 'dateTimePicker' }); const findInfoAlert = () => wrapper.find('.js-elasticsearch-alert'); - const findLogControlButtons = () => wrapper.find({ name: 'log-control-buttons-stub' }); + + const findInfiniteScroll = () => wrapper.find({ ref: 'infiniteScroll' }); const findLogTrace = () => wrapper.find('.js-log-trace'); + const findLogFooter = () => wrapper.find({ ref: 'logFooter' }); + const getInfiniteScrollAttr = attr => parseInt(findInfiniteScroll().attributes(attr), 10); const mockSetInitData = () => { state.pods.options = mockPods; state.environments.current = mockEnvName; [state.pods.current] = state.pods.options; - state.logs.isComplete = false; - state.logs.lines = mockLogsResult; + state.logs.lines = []; }; - const mockShowPodLogs = podName => { + const mockShowPodLogs = () => { state.pods.options = mockPods; - [state.pods.current] = podName; + [state.pods.current] = mockPods; - state.logs.isComplete = false; state.logs.lines = mockLogsResult; }; @@ -83,10 +85,21 @@ describe('EnvironmentLogs', () => { methods: { update: updateControlBtnsMock, }, + props: { + scrollDownButtonDisabled: false, + }, }, - }, - methods: { - ...actionMocks, + GlInfiniteScroll: { + name: 'gl-infinite-scroll', + template: ` + <div> + <slot name="header"></slot> + <slot name="items"></slot> + <slot></slot> + </div> + `, + }, + GlSprintf, }, }); }; @@ -95,12 +108,14 @@ describe('EnvironmentLogs', () => { store = createStore(); state = store.state.environmentLogs; EnvironmentLogsComponent = Vue.extend(EnvironmentLogs); + + jest.spyOn(store, 'dispatch').mockResolvedValue(); + + dispatch = store.dispatch; }); afterEach(() => { - actionMocks.setInitData.mockReset(); - actionMocks.showPodLogs.mockReset(); - actionMocks.fetchEnvironments.mockReset(); + store.dispatch.mockReset(); if (wrapper) { wrapper.destroy(); @@ -124,14 +139,14 @@ describe('EnvironmentLogs', () => { expect(findTimeRangePicker().is(DateTimePicker)).toBe(true); // log trace - expect(findLogTrace().isEmpty()).toBe(false); + expect(findInfiniteScroll().exists()).toBe(true); + expect(findLogTrace().exists()).toBe(true); }); it('mounted inits data', () => { initWrapper(); - expect(actionMocks.setInitData).toHaveBeenCalledTimes(1); - expect(actionMocks.setInitData).toHaveBeenLastCalledWith({ + expect(dispatch).toHaveBeenCalledWith(`${module}/setInitData`, { timeRange: expect.objectContaining({ default: true, }), @@ -139,18 +154,15 @@ describe('EnvironmentLogs', () => { podName: null, }); - expect(actionMocks.fetchEnvironments).toHaveBeenCalledTimes(1); - expect(actionMocks.fetchEnvironments).toHaveBeenLastCalledWith(mockEnvironmentsEndpoint); + expect(dispatch).toHaveBeenCalledWith(`${module}/fetchEnvironments`, mockEnvironmentsEndpoint); }); describe('loading state', () => { beforeEach(() => { state.pods.options = []; - state.logs = { - lines: [], - isLoading: true, - }; + state.logs.lines = []; + state.logs.isLoading = true; state.environments = { options: [], @@ -183,6 +195,18 @@ describe('EnvironmentLogs', () => { expect(updateControlBtnsMock).not.toHaveBeenCalled(); }); + it('shows an infinite scroll with height and no content', () => { + expect(getInfiniteScrollAttr('max-list-height')).toBeGreaterThan(0); + expect(getInfiniteScrollAttr('fetched-items')).toBe(0); + }); + + it('shows an infinite scroll container with equal height and max-height ', () => { + const height = getInfiniteScrollAttr('max-list-height'); + + expect(height).toEqual(expect.any(Number)); + expect(findInfiniteScroll().attributes('style')).toMatch(`height: ${height}px;`); + }); + it('shows a logs trace', () => { expect(findLogTrace().text()).toBe(''); expect( @@ -193,14 +217,12 @@ describe('EnvironmentLogs', () => { }); }); - describe('legacy environment', () => { + describe('k8s environment', () => { beforeEach(() => { state.pods.options = []; - state.logs = { - lines: [], - isLoading: false, - }; + state.logs.lines = []; + state.logs.isLoading = false; state.environments = { options: mockEnvironments, @@ -226,9 +248,16 @@ describe('EnvironmentLogs', () => { describe('state with data', () => { beforeEach(() => { - actionMocks.setInitData.mockImplementation(mockSetInitData); - actionMocks.showPodLogs.mockImplementation(mockShowPodLogs); - actionMocks.fetchEnvironments.mockImplementation(mockFetchEnvs); + dispatch.mockImplementation(actionName => { + if (actionName === `${module}/setInitData`) { + mockSetInitData(); + } else if (actionName === `${module}/showPodLogs`) { + mockShowPodLogs(); + } else if (actionName === `${module}/fetchEnvironments`) { + mockFetchEnvs(); + mockShowPodLogs(); + } + }); initWrapper(); }); @@ -236,10 +265,6 @@ describe('EnvironmentLogs', () => { afterEach(() => { scrollDown.mockReset(); updateControlBtnsMock.mockReset(); - - actionMocks.setInitData.mockReset(); - actionMocks.showPodLogs.mockReset(); - actionMocks.fetchEnvironments.mockReset(); }); it('displays an enabled search bar', () => { @@ -249,8 +274,8 @@ describe('EnvironmentLogs', () => { findSearchBar().vm.$emit('input', mockSearch); findSearchBar().vm.$emit('submit'); - expect(actionMocks.setSearch).toHaveBeenCalledTimes(1); - expect(actionMocks.setSearch).toHaveBeenCalledWith(mockSearch); + expect(dispatch).toHaveBeenCalledWith(`${module}/setInitData`, expect.any(Object)); + expect(dispatch).toHaveBeenCalledWith(`${module}/setSearch`, mockSearch); }); it('displays an enabled time window dropdown', () => { @@ -282,18 +307,21 @@ describe('EnvironmentLogs', () => { }); }); + it('shows infinite scroll with height and no content', () => { + expect(getInfiniteScrollAttr('max-list-height')).toBeGreaterThan(0); + expect(getInfiniteScrollAttr('fetched-items')).toBe(mockTrace.length); + }); + it('populates logs trace', () => { const trace = findLogTrace(); expect(trace.text().split('\n').length).toBe(mockTrace.length); expect(trace.text().split('\n')).toEqual(mockTrace); }); - it('update control buttons state', () => { - expect(updateControlBtnsMock).toHaveBeenCalledTimes(1); - }); + it('populates footer', () => { + const footer = findLogFooter().text(); - it('scrolls to bottom when loaded', () => { - expect(scrollDown).toHaveBeenCalledTimes(1); + expect(footer).toContain(`${mockLogsResult.length} results`); }); describe('when user clicks', () => { @@ -301,33 +329,99 @@ describe('EnvironmentLogs', () => { const items = findEnvironmentsDropdown().findAll(GlDropdownItem); const index = 1; // any env - expect(actionMocks.showEnvironment).toHaveBeenCalledTimes(0); + expect(dispatch).not.toHaveBeenCalledWith(`${module}/showEnvironment`, expect.anything()); items.at(index).vm.$emit('click'); - expect(actionMocks.showEnvironment).toHaveBeenCalledTimes(1); - expect(actionMocks.showEnvironment).toHaveBeenLastCalledWith(mockEnvironments[index].name); + expect(dispatch).toHaveBeenCalledWith( + `${module}/showEnvironment`, + mockEnvironments[index].name, + ); }); it('pod name, trace is refreshed', () => { const items = findPodsDropdown().findAll(GlDropdownItem); const index = 2; // any pod - expect(actionMocks.showPodLogs).toHaveBeenCalledTimes(0); + expect(dispatch).not.toHaveBeenCalledWith(`${module}/showPodLogs`, expect.anything()); items.at(index).vm.$emit('click'); - expect(actionMocks.showPodLogs).toHaveBeenCalledTimes(1); - expect(actionMocks.showPodLogs).toHaveBeenLastCalledWith(mockPods[index]); + expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPods[index]); }); it('refresh button, trace is refreshed', () => { - expect(actionMocks.showPodLogs).toHaveBeenCalledTimes(0); + expect(dispatch).not.toHaveBeenCalledWith(`${module}/showPodLogs`, expect.anything()); findLogControlButtons().vm.$emit('refresh'); - expect(actionMocks.showPodLogs).toHaveBeenCalledTimes(1); - expect(actionMocks.showPodLogs).toHaveBeenLastCalledWith(mockPodName); + expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPodName); + }); + }); + }); + + describe('listeners', () => { + beforeEach(() => { + initWrapper(); + }); + + it('attaches listeners in components', () => { + expect(findInfiniteScroll().vm.$listeners).toEqual({ + topReached: expect.any(Function), + scroll: expect.any(Function), + }); + }); + + it('`topReached` when not loading', () => { + expect(store.dispatch).not.toHaveBeenCalledWith(`${module}/fetchMoreLogsPrepend`, undefined); + + findInfiniteScroll().vm.$emit('topReached'); + + expect(store.dispatch).toHaveBeenCalledWith(`${module}/fetchMoreLogsPrepend`, undefined); + }); + + it('`topReached` does not fetches more logs when already loading', () => { + state.logs.isLoading = true; + findInfiniteScroll().vm.$emit('topReached'); + + expect(store.dispatch).not.toHaveBeenCalledWith(`${module}/fetchMoreLogsPrepend`, undefined); + }); + + it('`topReached` fetches more logs', () => { + state.logs.isLoading = true; + findInfiniteScroll().vm.$emit('topReached'); + + expect(store.dispatch).not.toHaveBeenCalledWith(`${module}/fetchMoreLogsPrepend`, undefined); + }); + + it('`scroll` on a scrollable target results in enabled scroll buttons', () => { + const target = { scrollTop: 10, clientHeight: 10, scrollHeight: 21 }; + + state.logs.isLoading = true; + findInfiniteScroll().vm.$emit('scroll', { target }); + + return wrapper.vm.$nextTick(() => { + expect(findLogControlButtons().props('scrollDownButtonDisabled')).toEqual(false); + }); + }); + + it('`scroll` on a non-scrollable target in disabled scroll buttons', () => { + const target = { scrollTop: 10, clientHeight: 10, scrollHeight: 20 }; + + state.logs.isLoading = true; + findInfiniteScroll().vm.$emit('scroll', { target }); + + return wrapper.vm.$nextTick(() => { + expect(findLogControlButtons().props('scrollDownButtonDisabled')).toEqual(true); + }); + }); + + it('`scroll` on no target results in disabled scroll buttons', () => { + state.logs.isLoading = true; + findInfiniteScroll().vm.$emit('scroll', { target: undefined }); + + return wrapper.vm.$nextTick(() => { + expect(findLogControlButtons().props('scrollDownButtonDisabled')).toEqual(true); }); }); }); diff --git a/spec/frontend/logs/components/log_control_buttons_spec.js b/spec/frontend/logs/components/log_control_buttons_spec.js index f344e8189c3..38e568f569f 100644 --- a/spec/frontend/logs/components/log_control_buttons_spec.js +++ b/spec/frontend/logs/components/log_control_buttons_spec.js @@ -1,15 +1,6 @@ import { shallowMount } from '@vue/test-utils'; import { GlButton } from '@gitlab/ui'; import LogControlButtons from '~/logs/components/log_control_buttons.vue'; -import { - canScroll, - isScrolledToTop, - isScrolledToBottom, - scrollDown, - scrollUp, -} from '~/lib/utils/scroll_utils'; - -jest.mock('~/lib/utils/scroll_utils'); describe('LogControlButtons', () => { let wrapper; @@ -18,8 +9,14 @@ describe('LogControlButtons', () => { const findScrollToBottom = () => wrapper.find('.js-scroll-to-bottom'); const findRefreshBtn = () => wrapper.find('.js-refresh-log'); - const initWrapper = () => { - wrapper = shallowMount(LogControlButtons); + const initWrapper = opts => { + wrapper = shallowMount(LogControlButtons, { + listeners: { + scrollUp: () => {}, + scrollDown: () => {}, + }, + ...opts, + }); }; afterEach(() => { @@ -55,27 +52,16 @@ describe('LogControlButtons', () => { describe('when scrolling actions are enabled', () => { beforeEach(() => { // mock scrolled to the middle of a long page - canScroll.mockReturnValue(true); - isScrolledToBottom.mockReturnValue(false); - isScrolledToTop.mockReturnValue(false); - initWrapper(); - wrapper.vm.update(); return wrapper.vm.$nextTick(); }); - afterEach(() => { - canScroll.mockReset(); - isScrolledToTop.mockReset(); - isScrolledToBottom.mockReset(); - }); - it('click on "scroll to top" scrolls up', () => { expect(findScrollToTop().is('[disabled]')).toBe(false); findScrollToTop().vm.$emit('click'); - expect(scrollUp).toHaveBeenCalledTimes(1); + expect(wrapper.emitted('scrollUp')).toHaveLength(1); }); it('click on "scroll to bottom" scrolls down', () => { @@ -83,25 +69,23 @@ describe('LogControlButtons', () => { findScrollToBottom().vm.$emit('click'); - expect(scrollDown).toHaveBeenCalledTimes(1); // plus one time when trace was loaded + expect(wrapper.emitted('scrollDown')).toHaveLength(1); }); }); describe('when scrolling actions are disabled', () => { beforeEach(() => { - // mock a short page without a scrollbar - canScroll.mockReturnValue(false); - isScrolledToBottom.mockReturnValue(true); - isScrolledToTop.mockReturnValue(true); - - initWrapper(); + initWrapper({ listeners: {} }); + return wrapper.vm.$nextTick(); }); it('buttons are disabled', () => { - wrapper.vm.update(); return wrapper.vm.$nextTick(() => { - expect(findScrollToTop().is('[disabled]')).toBe(true); - expect(findScrollToBottom().is('[disabled]')).toBe(true); + expect(findScrollToTop().exists()).toBe(false); + expect(findScrollToBottom().exists()).toBe(false); + // This should be enabled when gitlab-ui contains: + // https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/1149 + // expect(findScrollToBottom().is('[disabled]')).toBe(true); }); }); }); diff --git a/spec/frontend/logs/mock_data.js b/spec/frontend/logs/mock_data.js index 4c092a84b36..1a84d6edd12 100644 --- a/spec/frontend/logs/mock_data.js +++ b/spec/frontend/logs/mock_data.js @@ -1,14 +1,18 @@ -export const mockProjectPath = 'root/autodevops-deploy'; +const mockProjectPath = 'root/autodevops-deploy'; + export const mockEnvName = 'production'; export const mockEnvironmentsEndpoint = `${mockProjectPath}/environments.json`; export const mockEnvId = '99'; export const mockDocumentationPath = '/documentation.md'; +export const mockLogsEndpoint = '/dummy_logs_path.json'; +export const mockCursor = 'MOCK_CURSOR'; +export const mockNextCursor = 'MOCK_NEXT_CURSOR'; const makeMockEnvironment = (id, name, advancedQuerying) => ({ id, project_path: mockProjectPath, name, - logs_api_path: '/dummy_logs_path.json', + logs_api_path: mockLogsEndpoint, enable_advanced_logs_querying: advancedQuerying, }); @@ -28,58 +32,22 @@ export const mockPods = [ ]; export const mockLogsResult = [ - { - timestamp: '2019-12-13T13:43:18.2760123Z', - message: '10.36.0.1 - - [16/Oct/2019:06:29:48 UTC] "GET / HTTP/1.1" 200 13', - }, - { timestamp: '2019-12-13T13:43:18.2760123Z', message: '- -> /' }, - { - timestamp: '2019-12-13T13:43:26.8420123Z', - message: '10.36.0.1 - - [16/Oct/2019:06:29:57 UTC] "GET / HTTP/1.1" 200 13', - }, - { timestamp: '2019-12-13T13:43:26.8420123Z', message: '- -> /' }, - { - timestamp: '2019-12-13T13:43:28.3710123Z', - message: '10.36.0.1 - - [16/Oct/2019:06:29:58 UTC] "GET / HTTP/1.1" 200 13', - }, - { timestamp: '2019-12-13T13:43:28.3710123Z', message: '- -> /' }, - { - timestamp: '2019-12-13T13:43:36.8860123Z', - message: '10.36.0.1 - - [16/Oct/2019:06:30:07 UTC] "GET / HTTP/1.1" 200 13', - }, - { timestamp: '2019-12-13T13:43:36.8860123Z', message: '- -> /' }, - { - timestamp: '2019-12-13T13:43:38.4000123Z', - message: '10.36.0.1 - - [16/Oct/2019:06:30:08 UTC] "GET / HTTP/1.1" 200 13', - }, - { timestamp: '2019-12-13T13:43:38.4000123Z', message: '- -> /' }, - { - timestamp: '2019-12-13T13:43:46.8420123Z', - message: '10.36.0.1 - - [16/Oct/2019:06:30:17 UTC] "GET / HTTP/1.1" 200 13', - }, - { timestamp: '2019-12-13T13:43:46.8430123Z', message: '- -> /' }, - { - timestamp: '2019-12-13T13:43:48.3240123Z', - message: '10.36.0.1 - - [16/Oct/2019:06:30:18 UTC] "GET / HTTP/1.1" 200 13', - }, - { timestamp: '2019-12-13T13:43:48.3250123Z', message: '- -> /' }, + { timestamp: '2019-12-13T13:43:18.2760123Z', message: 'Log 1' }, + { timestamp: '2019-12-13T13:43:18.2760123Z', message: 'Log 2' }, + { timestamp: '2019-12-13T13:43:26.8420123Z', message: 'Log 3' }, ]; export const mockTrace = [ - 'Dec 13 13:43:18.276Z | 10.36.0.1 - - [16/Oct/2019:06:29:48 UTC] "GET / HTTP/1.1" 200 13', - 'Dec 13 13:43:18.276Z | - -> /', - 'Dec 13 13:43:26.842Z | 10.36.0.1 - - [16/Oct/2019:06:29:57 UTC] "GET / HTTP/1.1" 200 13', - 'Dec 13 13:43:26.842Z | - -> /', - 'Dec 13 13:43:28.371Z | 10.36.0.1 - - [16/Oct/2019:06:29:58 UTC] "GET / HTTP/1.1" 200 13', - 'Dec 13 13:43:28.371Z | - -> /', - 'Dec 13 13:43:36.886Z | 10.36.0.1 - - [16/Oct/2019:06:30:07 UTC] "GET / HTTP/1.1" 200 13', - 'Dec 13 13:43:36.886Z | - -> /', - 'Dec 13 13:43:38.400Z | 10.36.0.1 - - [16/Oct/2019:06:30:08 UTC] "GET / HTTP/1.1" 200 13', - 'Dec 13 13:43:38.400Z | - -> /', - 'Dec 13 13:43:46.842Z | 10.36.0.1 - - [16/Oct/2019:06:30:17 UTC] "GET / HTTP/1.1" 200 13', - 'Dec 13 13:43:46.843Z | - -> /', - 'Dec 13 13:43:48.324Z | 10.36.0.1 - - [16/Oct/2019:06:30:18 UTC] "GET / HTTP/1.1" 200 13', - 'Dec 13 13:43:48.325Z | - -> /', + 'Dec 13 13:43:18.276Z | Log 1', + 'Dec 13 13:43:18.276Z | Log 2', + 'Dec 13 13:43:26.842Z | Log 3', ]; +export const mockResponse = { + pod_name: mockPodName, + pods: mockPods, + logs: mockLogsResult, + cursor: mockNextCursor, +}; + export const mockSearch = 'foo +bar'; diff --git a/spec/frontend/logs/stores/actions_spec.js b/spec/frontend/logs/stores/actions_spec.js index 6309126159e..1512797e1bc 100644 --- a/spec/frontend/logs/stores/actions_spec.js +++ b/spec/frontend/logs/stores/actions_spec.js @@ -10,6 +10,7 @@ import { showPodLogs, fetchEnvironments, fetchLogs, + fetchMoreLogsPrepend, } from '~/logs/stores/actions'; import { defaultTimeRange } from '~/monitoring/constants'; @@ -18,7 +19,6 @@ import axios from '~/lib/utils/axios_utils'; import flash from '~/flash'; import { - mockProjectPath, mockPodName, mockEnvironmentsEndpoint, mockEnvironments, @@ -26,6 +26,10 @@ import { mockLogsResult, mockEnvName, mockSearch, + mockLogsEndpoint, + mockResponse, + mockCursor, + mockNextCursor, } from '../mock_data'; jest.mock('~/flash'); @@ -52,6 +56,8 @@ describe('Logs Store actions', () => { let state; let mock; + const latestGetParams = () => mock.history.get[mock.history.get.length - 1].params; + convertToFixedRange.mockImplementation(range => { if (range === defaultTimeRange) { return { ...mockDefaultRange }; @@ -75,10 +81,16 @@ describe('Logs Store actions', () => { describe('setInitData', () => { it('should commit environment and pod name mutation', () => - testAction(setInitData, { environmentName: mockEnvName, podName: mockPodName }, state, [ - { type: types.SET_PROJECT_ENVIRONMENT, payload: mockEnvName }, - { type: types.SET_CURRENT_POD_NAME, payload: mockPodName }, - ])); + testAction( + setInitData, + { timeRange: mockFixedRange, environmentName: mockEnvName, podName: mockPodName }, + state, + [ + { type: types.SET_TIME_RANGE, payload: mockFixedRange }, + { type: types.SET_PROJECT_ENVIRONMENT, payload: mockEnvName }, + { type: types.SET_CURRENT_POD_NAME, payload: mockPodName }, + ], + )); }); describe('setSearch', () => { @@ -140,183 +152,245 @@ describe('Logs Store actions', () => { }); }); - describe('fetchLogs', () => { + describe('when the backend responds succesfully', () => { + let expectedMutations; + let expectedActions; + beforeEach(() => { mock = new MockAdapter(axios); + mock.onGet(mockLogsEndpoint).reply(200, mockResponse); + mock.onGet(mockLogsEndpoint).replyOnce(202); // mock reactive cache + + state.environments.options = mockEnvironments; + state.environments.current = mockEnvName; }); afterEach(() => { mock.reset(); }); - it('should commit logs and pod data when there is pod name defined', () => { - state.environments.options = mockEnvironments; - state.environments.current = mockEnvName; - state.pods.current = mockPodName; - - const endpoint = '/dummy_logs_path.json'; - - mock - .onGet(endpoint, { - params: { - pod_name: mockPodName, - ...mockDefaultRange, - }, - }) - .reply(200, { - pod_name: mockPodName, - pods: mockPods, - logs: mockLogsResult, - }); - - mock.onGet(endpoint).replyOnce(202); // mock reactive cache - - return testAction( - fetchLogs, - null, - state, - [ + describe('fetchLogs', () => { + beforeEach(() => { + expectedMutations = [ { type: types.REQUEST_PODS_DATA }, { type: types.REQUEST_LOGS_DATA }, { type: types.SET_CURRENT_POD_NAME, payload: mockPodName }, { type: types.RECEIVE_PODS_DATA_SUCCESS, payload: mockPods }, - { type: types.RECEIVE_LOGS_DATA_SUCCESS, payload: mockLogsResult }, - ], - [], - ); - }); + { + type: types.RECEIVE_LOGS_DATA_SUCCESS, + payload: { logs: mockLogsResult, cursor: mockNextCursor }, + }, + ]; - it('should commit logs and pod data when there is pod name defined and a non-default date range', () => { - state.projectPath = mockProjectPath; - state.environments.options = mockEnvironments; - state.environments.current = mockEnvName; - state.pods.current = mockPodName; - state.timeRange.current = mockFixedRange; + expectedActions = []; + }); - const endpoint = '/dummy_logs_path.json'; + it('should commit logs and pod data when there is pod name defined', () => { + state.pods.current = mockPodName; - mock - .onGet(endpoint, { - params: { + return testAction(fetchLogs, null, state, expectedMutations, expectedActions, () => { + expect(latestGetParams()).toMatchObject({ pod_name: mockPodName, - start: mockFixedRange.start, - end: mockFixedRange.end, - }, - }) - .reply(200, { - pod_name: mockPodName, - pods: mockPods, - logs: mockLogsResult, + }); }); + }); - return testAction( - fetchLogs, - null, - state, - [ - { type: types.REQUEST_PODS_DATA }, - { type: types.REQUEST_LOGS_DATA }, - { type: types.SET_CURRENT_POD_NAME, payload: mockPodName }, - { type: types.RECEIVE_PODS_DATA_SUCCESS, payload: mockPods }, - { type: types.RECEIVE_LOGS_DATA_SUCCESS, payload: mockLogsResult }, - ], - [], - ); - }); - - it('should commit logs and pod data when there is pod name and search and a faulty date range', () => { - state.environments.options = mockEnvironments; - state.environments.current = mockEnvName; - state.pods.current = mockPodName; - state.search = mockSearch; - state.timeRange.current = 'INVALID_TIME_RANGE'; - - const endpoint = '/dummy_logs_path.json'; + it('should commit logs and pod data when there is pod name defined and a non-default date range', () => { + state.pods.current = mockPodName; + state.timeRange.current = mockFixedRange; + state.logs.cursor = mockCursor; - mock - .onGet(endpoint, { - params: { + return testAction(fetchLogs, null, state, expectedMutations, expectedActions, () => { + expect(latestGetParams()).toEqual({ pod_name: mockPodName, - search: mockSearch, - }, - }) - .reply(200, { - pod_name: mockPodName, - pods: mockPods, - logs: mockLogsResult, + start: mockFixedRange.start, + end: mockFixedRange.end, + cursor: mockCursor, + }); }); + }); - mock.onGet(endpoint).replyOnce(202); // mock reactive cache + it('should commit logs and pod data when there is pod name and search and a faulty date range', () => { + state.pods.current = mockPodName; + state.search = mockSearch; + state.timeRange.current = 'INVALID_TIME_RANGE'; - return testAction( - fetchLogs, - null, - state, - [ - { type: types.REQUEST_PODS_DATA }, - { type: types.REQUEST_LOGS_DATA }, - { type: types.SET_CURRENT_POD_NAME, payload: mockPodName }, - { type: types.RECEIVE_PODS_DATA_SUCCESS, payload: mockPods }, - { type: types.RECEIVE_LOGS_DATA_SUCCESS, payload: mockLogsResult }, - ], - [], - () => { + return testAction(fetchLogs, null, state, expectedMutations, expectedActions, () => { + expect(latestGetParams()).toEqual({ + pod_name: mockPodName, + search: mockSearch, + }); // Warning about time ranges was issued expect(flash).toHaveBeenCalledTimes(1); expect(flash).toHaveBeenCalledWith(expect.any(String), 'warning'); - }, - ); + }); + }); + + it('should commit logs and pod data when no pod name defined', () => { + state.timeRange.current = mockDefaultRange; + + return testAction(fetchLogs, null, state, expectedMutations, expectedActions, () => { + expect(latestGetParams()).toEqual({}); + }); + }); }); - it('should commit logs and pod data when no pod name defined', done => { - state.environments.options = mockEnvironments; - state.environments.current = mockEnvName; + describe('fetchMoreLogsPrepend', () => { + beforeEach(() => { + expectedMutations = [ + { type: types.REQUEST_LOGS_DATA_PREPEND }, + { + type: types.RECEIVE_LOGS_DATA_PREPEND_SUCCESS, + payload: { logs: mockLogsResult, cursor: mockNextCursor }, + }, + ]; - const endpoint = '/dummy_logs_path.json'; + expectedActions = []; + }); - mock.onGet(endpoint, { params: { ...mockDefaultRange } }).reply(200, { - pod_name: mockPodName, - pods: mockPods, - logs: mockLogsResult, + it('should commit logs and pod data when there is pod name defined', () => { + state.pods.current = mockPodName; + + expectedActions = []; + + return testAction( + fetchMoreLogsPrepend, + null, + state, + expectedMutations, + expectedActions, + () => { + expect(latestGetParams()).toMatchObject({ + pod_name: mockPodName, + }); + }, + ); }); - mock.onGet(endpoint).replyOnce(202); // mock reactive cache - testAction( + it('should commit logs and pod data when there is pod name defined and a non-default date range', () => { + state.pods.current = mockPodName; + state.timeRange.current = mockFixedRange; + state.logs.cursor = mockCursor; + + return testAction( + fetchMoreLogsPrepend, + null, + state, + expectedMutations, + expectedActions, + () => { + expect(latestGetParams()).toEqual({ + pod_name: mockPodName, + start: mockFixedRange.start, + end: mockFixedRange.end, + cursor: mockCursor, + }); + }, + ); + }); + + it('should commit logs and pod data when there is pod name and search and a faulty date range', () => { + state.pods.current = mockPodName; + state.search = mockSearch; + state.timeRange.current = 'INVALID_TIME_RANGE'; + + return testAction( + fetchMoreLogsPrepend, + null, + state, + expectedMutations, + expectedActions, + () => { + expect(latestGetParams()).toEqual({ + pod_name: mockPodName, + search: mockSearch, + }); + // Warning about time ranges was issued + expect(flash).toHaveBeenCalledTimes(1); + expect(flash).toHaveBeenCalledWith(expect.any(String), 'warning'); + }, + ); + }); + + it('should commit logs and pod data when no pod name defined', () => { + state.timeRange.current = mockDefaultRange; + + return testAction( + fetchMoreLogsPrepend, + null, + state, + expectedMutations, + expectedActions, + () => { + expect(latestGetParams()).toEqual({}); + }, + ); + }); + + it('should not commit logs or pod data when it has reached the end', () => { + state.logs.isComplete = true; + state.logs.cursor = null; + + return testAction( + fetchMoreLogsPrepend, + null, + state, + [], // no mutations done + [], // no actions dispatched + () => { + expect(mock.history.get).toHaveLength(0); + }, + ); + }); + }); + }); + + describe('when the backend responds with an error', () => { + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet(mockLogsEndpoint).reply(500); + }); + + afterEach(() => { + mock.reset(); + }); + + it('fetchLogs should commit logs and pod errors', () => { + state.environments.options = mockEnvironments; + state.environments.current = mockEnvName; + + return testAction( fetchLogs, null, state, [ { type: types.REQUEST_PODS_DATA }, { type: types.REQUEST_LOGS_DATA }, - { type: types.SET_CURRENT_POD_NAME, payload: mockPodName }, - { type: types.RECEIVE_PODS_DATA_SUCCESS, payload: mockPods }, - { type: types.RECEIVE_LOGS_DATA_SUCCESS, payload: mockLogsResult }, + { type: types.RECEIVE_PODS_DATA_ERROR }, + { type: types.RECEIVE_LOGS_DATA_ERROR }, ], [], - done, + () => { + expect(mock.history.get[0].url).toBe(mockLogsEndpoint); + }, ); }); - it('should commit logs and pod errors when backend fails', () => { + it('fetchMoreLogsPrepend should commit logs and pod errors', () => { state.environments.options = mockEnvironments; state.environments.current = mockEnvName; - const endpoint = `/${mockProjectPath}/-/logs/elasticsearch.json?environment_name=${mockEnvName}`; - mock.onGet(endpoint).replyOnce(500); - return testAction( - fetchLogs, + fetchMoreLogsPrepend, null, state, [ - { type: types.REQUEST_PODS_DATA }, - { type: types.REQUEST_LOGS_DATA }, - { type: types.RECEIVE_PODS_DATA_ERROR }, - { type: types.RECEIVE_LOGS_DATA_ERROR }, + { type: types.REQUEST_LOGS_DATA_PREPEND }, + { type: types.RECEIVE_LOGS_DATA_PREPEND_ERROR }, ], [], () => { - expect(flash).toHaveBeenCalledTimes(1); + expect(mock.history.get[0].url).toBe(mockLogsEndpoint); }, ); }); diff --git a/spec/frontend/logs/stores/mutations_spec.js b/spec/frontend/logs/stores/mutations_spec.js index dcb358c7d5b..eae838a31d4 100644 --- a/spec/frontend/logs/stores/mutations_spec.js +++ b/spec/frontend/logs/stores/mutations_spec.js @@ -9,6 +9,8 @@ import { mockPodName, mockLogsResult, mockSearch, + mockCursor, + mockNextCursor, } from '../mock_data'; describe('Logs Store Mutations', () => { @@ -73,27 +75,47 @@ describe('Logs Store Mutations', () => { it('starts loading for logs', () => { mutations[types.REQUEST_LOGS_DATA](state); - expect(state.logs).toEqual( - expect.objectContaining({ - lines: [], - isLoading: true, - isComplete: false, - }), - ); + expect(state.timeRange.current).toEqual({ + start: expect.any(String), + end: expect.any(String), + }); + + expect(state.logs).toEqual({ + lines: [], + cursor: null, + isLoading: true, + isComplete: false, + }); }); }); describe('RECEIVE_LOGS_DATA_SUCCESS', () => { - it('receives logs lines', () => { - mutations[types.RECEIVE_LOGS_DATA_SUCCESS](state, mockLogsResult); + it('receives logs lines and cursor', () => { + mutations[types.RECEIVE_LOGS_DATA_SUCCESS](state, { + logs: mockLogsResult, + cursor: mockCursor, + }); - expect(state.logs).toEqual( - expect.objectContaining({ - lines: mockLogsResult, - isLoading: false, - isComplete: true, - }), - ); + expect(state.logs).toEqual({ + lines: mockLogsResult, + isLoading: false, + cursor: mockCursor, + isComplete: false, + }); + }); + + it('receives logs lines and a null cursor to indicate the end', () => { + mutations[types.RECEIVE_LOGS_DATA_SUCCESS](state, { + logs: mockLogsResult, + cursor: null, + }); + + expect(state.logs).toEqual({ + lines: mockLogsResult, + isLoading: false, + cursor: null, + isComplete: true, + }); }); }); @@ -101,13 +123,77 @@ describe('Logs Store Mutations', () => { it('receives log data error and stops loading', () => { mutations[types.RECEIVE_LOGS_DATA_ERROR](state); - expect(state.logs).toEqual( - expect.objectContaining({ - lines: [], - isLoading: false, - isComplete: true, - }), - ); + expect(state.logs).toEqual({ + lines: [], + isLoading: false, + cursor: null, + isComplete: false, + }); + }); + }); + + describe('REQUEST_LOGS_DATA_PREPEND', () => { + it('receives logs lines and cursor', () => { + mutations[types.REQUEST_LOGS_DATA_PREPEND](state); + + expect(state.logs.isLoading).toBe(true); + }); + }); + + describe('RECEIVE_LOGS_DATA_PREPEND_SUCCESS', () => { + it('receives logs lines and cursor', () => { + mutations[types.RECEIVE_LOGS_DATA_PREPEND_SUCCESS](state, { + logs: mockLogsResult, + cursor: mockCursor, + }); + + expect(state.logs).toEqual({ + lines: mockLogsResult, + isLoading: false, + cursor: mockCursor, + isComplete: false, + }); + }); + + it('receives additional logs lines and a new cursor', () => { + mutations[types.RECEIVE_LOGS_DATA_PREPEND_SUCCESS](state, { + logs: mockLogsResult, + cursor: mockCursor, + }); + + mutations[types.RECEIVE_LOGS_DATA_PREPEND_SUCCESS](state, { + logs: mockLogsResult, + cursor: mockNextCursor, + }); + + expect(state.logs).toEqual({ + lines: [...mockLogsResult, ...mockLogsResult], + isLoading: false, + cursor: mockNextCursor, + isComplete: false, + }); + }); + + it('receives logs lines and a null cursor to indicate is complete', () => { + mutations[types.RECEIVE_LOGS_DATA_PREPEND_SUCCESS](state, { + logs: mockLogsResult, + cursor: null, + }); + + expect(state.logs).toEqual({ + lines: mockLogsResult, + isLoading: false, + cursor: null, + isComplete: true, + }); + }); + }); + + describe('RECEIVE_LOGS_DATA_PREPEND_ERROR', () => { + it('receives logs lines and cursor', () => { + mutations[types.RECEIVE_LOGS_DATA_PREPEND_ERROR](state); + + expect(state.logs.isLoading).toBe(false); }); }); @@ -121,6 +207,7 @@ describe('Logs Store Mutations', () => { describe('SET_TIME_RANGE', () => { it('sets a default range', () => { + expect(state.timeRange.selected).toEqual(expect.any(Object)); expect(state.timeRange.current).toEqual(expect.any(Object)); }); @@ -131,12 +218,13 @@ describe('Logs Store Mutations', () => { }; mutations[types.SET_TIME_RANGE](state, mockRange); + expect(state.timeRange.selected).toEqual(mockRange); expect(state.timeRange.current).toEqual(mockRange); }); }); describe('REQUEST_PODS_DATA', () => { - it('receives log data error and stops loading', () => { + it('receives pods data', () => { mutations[types.REQUEST_PODS_DATA](state); expect(state.pods).toEqual( |