2018-11-20 20:47:30 +05:30
|
|
|
import MockAdapter from 'axios-mock-adapter';
|
2020-01-01 13:55:28 +05:30
|
|
|
import testAction from 'spec/helpers/vuex_action_helper';
|
|
|
|
import { TEST_HOST } from 'spec/test_constants';
|
2018-11-20 20:47:30 +05:30
|
|
|
import axios from '~/lib/utils/axios_utils';
|
|
|
|
import {
|
|
|
|
setJobEndpoint,
|
2018-12-13 13:39:08 +05:30
|
|
|
setTraceOptions,
|
2018-11-20 20:47:30 +05:30
|
|
|
clearEtagPoll,
|
|
|
|
stopPolling,
|
|
|
|
requestJob,
|
|
|
|
fetchJob,
|
|
|
|
receiveJobSuccess,
|
|
|
|
receiveJobError,
|
|
|
|
scrollTop,
|
|
|
|
scrollBottom,
|
|
|
|
requestTrace,
|
|
|
|
fetchTrace,
|
2020-03-13 15:44:24 +05:30
|
|
|
startPollingTrace,
|
2018-11-20 20:47:30 +05:30
|
|
|
stopPollingTrace,
|
|
|
|
receiveTraceSuccess,
|
|
|
|
receiveTraceError,
|
2019-12-04 20:38:33 +05:30
|
|
|
toggleCollapsibleLine,
|
2018-11-20 20:47:30 +05:30
|
|
|
requestJobsForStage,
|
|
|
|
fetchJobsForStage,
|
|
|
|
receiveJobsForStageSuccess,
|
|
|
|
receiveJobsForStageError,
|
2018-12-13 13:39:08 +05:30
|
|
|
hideSidebar,
|
|
|
|
showSidebar,
|
|
|
|
toggleSidebar,
|
2018-11-20 20:47:30 +05:30
|
|
|
} from '~/jobs/store/actions';
|
|
|
|
import state from '~/jobs/store/state';
|
|
|
|
import * as types from '~/jobs/store/mutation_types';
|
|
|
|
|
|
|
|
describe('Job State actions', () => {
|
|
|
|
let mockedState;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
mockedState = state();
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('setJobEndpoint', () => {
|
|
|
|
it('should commit SET_JOB_ENDPOINT mutation', done => {
|
|
|
|
testAction(
|
|
|
|
setJobEndpoint,
|
|
|
|
'job/872324.json',
|
|
|
|
mockedState,
|
|
|
|
[{ type: types.SET_JOB_ENDPOINT, payload: 'job/872324.json' }],
|
|
|
|
[],
|
|
|
|
done,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2018-12-13 13:39:08 +05:30
|
|
|
describe('setTraceOptions', () => {
|
|
|
|
it('should commit SET_TRACE_OPTIONS mutation', done => {
|
2018-11-20 20:47:30 +05:30
|
|
|
testAction(
|
2018-12-13 13:39:08 +05:30
|
|
|
setTraceOptions,
|
|
|
|
{ pagePath: 'job/872324/trace.json' },
|
2018-11-20 20:47:30 +05:30
|
|
|
mockedState,
|
2018-12-13 13:39:08 +05:30
|
|
|
[{ type: types.SET_TRACE_OPTIONS, payload: { pagePath: 'job/872324/trace.json' } }],
|
2018-11-20 20:47:30 +05:30
|
|
|
[],
|
|
|
|
done,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2018-12-13 13:39:08 +05:30
|
|
|
describe('hideSidebar', () => {
|
|
|
|
it('should commit HIDE_SIDEBAR mutation', done => {
|
|
|
|
testAction(hideSidebar, null, mockedState, [{ type: types.HIDE_SIDEBAR }], [], done);
|
2018-11-20 20:47:30 +05:30
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2018-12-13 13:39:08 +05:30
|
|
|
describe('showSidebar', () => {
|
|
|
|
it('should commit HIDE_SIDEBAR mutation', done => {
|
|
|
|
testAction(showSidebar, null, mockedState, [{ type: types.SHOW_SIDEBAR }], [], done);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('toggleSidebar', () => {
|
|
|
|
describe('when isSidebarOpen is true', () => {
|
|
|
|
it('should dispatch hideSidebar', done => {
|
|
|
|
testAction(toggleSidebar, null, mockedState, [], [{ type: 'hideSidebar' }], done);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('when isSidebarOpen is false', () => {
|
|
|
|
it('should dispatch showSidebar', done => {
|
|
|
|
mockedState.isSidebarOpen = false;
|
|
|
|
|
|
|
|
testAction(toggleSidebar, null, mockedState, [], [{ type: 'showSidebar' }], done);
|
|
|
|
});
|
2018-11-20 20:47:30 +05:30
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('requestJob', () => {
|
|
|
|
it('should commit REQUEST_JOB mutation', done => {
|
|
|
|
testAction(requestJob, null, mockedState, [{ type: types.REQUEST_JOB }], [], done);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('fetchJob', () => {
|
|
|
|
let mock;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
mockedState.jobEndpoint = `${TEST_HOST}/endpoint.json`;
|
|
|
|
mock = new MockAdapter(axios);
|
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
mock.restore();
|
|
|
|
stopPolling();
|
|
|
|
clearEtagPoll();
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('success', () => {
|
|
|
|
it('dispatches requestJob and receiveJobSuccess ', done => {
|
|
|
|
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, { id: 121212, name: 'karma' });
|
|
|
|
|
|
|
|
testAction(
|
|
|
|
fetchJob,
|
|
|
|
null,
|
|
|
|
mockedState,
|
|
|
|
[],
|
|
|
|
[
|
|
|
|
{
|
|
|
|
type: 'requestJob',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
payload: { id: 121212, name: 'karma' },
|
|
|
|
type: 'receiveJobSuccess',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
done,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('error', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('dispatches requestJob and receiveJobError ', done => {
|
|
|
|
testAction(
|
|
|
|
fetchJob,
|
|
|
|
null,
|
|
|
|
mockedState,
|
|
|
|
[],
|
|
|
|
[
|
|
|
|
{
|
|
|
|
type: 'requestJob',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: 'receiveJobError',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
done,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('receiveJobSuccess', () => {
|
|
|
|
it('should commit RECEIVE_JOB_SUCCESS mutation', done => {
|
|
|
|
testAction(
|
|
|
|
receiveJobSuccess,
|
|
|
|
{ id: 121232132 },
|
|
|
|
mockedState,
|
|
|
|
[{ type: types.RECEIVE_JOB_SUCCESS, payload: { id: 121232132 } }],
|
|
|
|
[],
|
|
|
|
done,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('receiveJobError', () => {
|
|
|
|
it('should commit RECEIVE_JOB_ERROR mutation', done => {
|
|
|
|
testAction(receiveJobError, null, mockedState, [{ type: types.RECEIVE_JOB_ERROR }], [], done);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('scrollTop', () => {
|
2018-12-13 13:39:08 +05:30
|
|
|
it('should dispatch toggleScrollButtons action', done => {
|
|
|
|
testAction(scrollTop, null, mockedState, [], [{ type: 'toggleScrollButtons' }], done);
|
2018-11-20 20:47:30 +05:30
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('scrollBottom', () => {
|
2018-12-13 13:39:08 +05:30
|
|
|
it('should dispatch toggleScrollButtons action', done => {
|
|
|
|
testAction(scrollBottom, null, mockedState, [], [{ type: 'toggleScrollButtons' }], done);
|
2018-11-20 20:47:30 +05:30
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('requestTrace', () => {
|
|
|
|
it('should commit REQUEST_TRACE mutation', done => {
|
|
|
|
testAction(requestTrace, null, mockedState, [{ type: types.REQUEST_TRACE }], [], done);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('fetchTrace', () => {
|
|
|
|
let mock;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
mockedState.traceEndpoint = `${TEST_HOST}/endpoint`;
|
|
|
|
mock = new MockAdapter(axios);
|
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
mock.restore();
|
|
|
|
stopPolling();
|
|
|
|
clearEtagPoll();
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('success', () => {
|
2018-12-13 13:39:08 +05:30
|
|
|
it('dispatches requestTrace, receiveTraceSuccess and stopPollingTrace when job is complete', done => {
|
2018-11-20 20:47:30 +05:30
|
|
|
mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, {
|
|
|
|
html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
|
|
|
|
complete: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
testAction(
|
|
|
|
fetchTrace,
|
|
|
|
null,
|
|
|
|
mockedState,
|
|
|
|
[],
|
|
|
|
[
|
|
|
|
{
|
2018-12-13 13:39:08 +05:30
|
|
|
type: 'toggleScrollisInBottom',
|
|
|
|
payload: true,
|
2018-11-20 20:47:30 +05:30
|
|
|
},
|
|
|
|
{
|
|
|
|
payload: {
|
2018-12-05 23:21:45 +05:30
|
|
|
html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
|
|
|
|
complete: true,
|
2018-11-20 20:47:30 +05:30
|
|
|
},
|
|
|
|
type: 'receiveTraceSuccess',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: 'stopPollingTrace',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
done,
|
|
|
|
);
|
|
|
|
});
|
2020-03-13 15:44:24 +05:30
|
|
|
|
|
|
|
describe('when job is incomplete', () => {
|
|
|
|
let tracePayload;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
tracePayload = {
|
|
|
|
html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
|
|
|
|
complete: false,
|
|
|
|
};
|
|
|
|
|
|
|
|
mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, tracePayload);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('dispatches startPollingTrace', done => {
|
|
|
|
testAction(
|
|
|
|
fetchTrace,
|
|
|
|
null,
|
|
|
|
mockedState,
|
|
|
|
[],
|
|
|
|
[
|
|
|
|
{ type: 'toggleScrollisInBottom', payload: true },
|
|
|
|
{ type: 'receiveTraceSuccess', payload: tracePayload },
|
|
|
|
{ type: 'startPollingTrace' },
|
|
|
|
],
|
|
|
|
done,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('does not dispatch startPollingTrace when timeout is non-empty', done => {
|
|
|
|
mockedState.traceTimeout = 1;
|
|
|
|
|
|
|
|
testAction(
|
|
|
|
fetchTrace,
|
|
|
|
null,
|
|
|
|
mockedState,
|
|
|
|
[],
|
|
|
|
[
|
|
|
|
{ type: 'toggleScrollisInBottom', payload: true },
|
|
|
|
{ type: 'receiveTraceSuccess', payload: tracePayload },
|
|
|
|
],
|
|
|
|
done,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
2018-11-20 20:47:30 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
describe('error', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
mock.onGet(`${TEST_HOST}/endpoint/trace.json`).reply(500);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('dispatches requestTrace and receiveTraceError ', done => {
|
|
|
|
testAction(
|
|
|
|
fetchTrace,
|
|
|
|
null,
|
|
|
|
mockedState,
|
|
|
|
[],
|
|
|
|
[
|
|
|
|
{
|
|
|
|
type: 'receiveTraceError',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
done,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
describe('startPollingTrace', () => {
|
|
|
|
let dispatch;
|
|
|
|
let commit;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
jasmine.clock().install();
|
|
|
|
|
|
|
|
dispatch = jasmine.createSpy();
|
|
|
|
commit = jasmine.createSpy();
|
|
|
|
|
|
|
|
startPollingTrace({ dispatch, commit });
|
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
jasmine.clock().uninstall();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should save the timeout id but not call fetchTrace', () => {
|
|
|
|
expect(commit).toHaveBeenCalledWith(types.SET_TRACE_TIMEOUT, 1);
|
|
|
|
expect(dispatch).not.toHaveBeenCalledWith('fetchTrace');
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('after timeout has passed', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
jasmine.clock().tick(4000);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should clear the timeout id and fetchTrace', () => {
|
|
|
|
expect(commit).toHaveBeenCalledWith(types.SET_TRACE_TIMEOUT, 0);
|
|
|
|
expect(dispatch).toHaveBeenCalledWith('fetchTrace');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2018-11-20 20:47:30 +05:30
|
|
|
describe('stopPollingTrace', () => {
|
2020-03-13 15:44:24 +05:30
|
|
|
let origTimeout;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
// Can't use spyOn(window, 'clearTimeout') because this caused unrelated specs to timeout
|
|
|
|
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23838#note_280277727
|
|
|
|
origTimeout = window.clearTimeout;
|
|
|
|
window.clearTimeout = jasmine.createSpy();
|
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
window.clearTimeout = origTimeout;
|
|
|
|
});
|
|
|
|
|
2018-11-20 20:47:30 +05:30
|
|
|
it('should commit STOP_POLLING_TRACE mutation ', done => {
|
2020-03-13 15:44:24 +05:30
|
|
|
const traceTimeout = 7;
|
|
|
|
|
2018-11-20 20:47:30 +05:30
|
|
|
testAction(
|
|
|
|
stopPollingTrace,
|
|
|
|
null,
|
2020-03-13 15:44:24 +05:30
|
|
|
{ ...mockedState, traceTimeout },
|
|
|
|
[{ type: types.SET_TRACE_TIMEOUT, payload: 0 }, { type: types.STOP_POLLING_TRACE }],
|
2018-11-20 20:47:30 +05:30
|
|
|
[],
|
2020-03-13 15:44:24 +05:30
|
|
|
)
|
|
|
|
.then(() => {
|
|
|
|
expect(window.clearTimeout).toHaveBeenCalledWith(traceTimeout);
|
|
|
|
})
|
|
|
|
.then(done)
|
|
|
|
.catch(done.fail);
|
2018-11-20 20:47:30 +05:30
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('receiveTraceSuccess', () => {
|
|
|
|
it('should commit RECEIVE_TRACE_SUCCESS mutation ', done => {
|
|
|
|
testAction(
|
|
|
|
receiveTraceSuccess,
|
|
|
|
'hello world',
|
|
|
|
mockedState,
|
|
|
|
[{ type: types.RECEIVE_TRACE_SUCCESS, payload: 'hello world' }],
|
|
|
|
[],
|
|
|
|
done,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('receiveTraceError', () => {
|
2020-03-13 15:44:24 +05:30
|
|
|
it('should commit stop polling trace', done => {
|
|
|
|
testAction(receiveTraceError, null, mockedState, [], [{ type: 'stopPollingTrace' }], done);
|
2018-11-20 20:47:30 +05:30
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2019-12-04 20:38:33 +05:30
|
|
|
describe('toggleCollapsibleLine', () => {
|
|
|
|
it('should commit TOGGLE_COLLAPSIBLE_LINE mutation ', done => {
|
|
|
|
testAction(
|
|
|
|
toggleCollapsibleLine,
|
|
|
|
{ isClosed: true },
|
|
|
|
mockedState,
|
|
|
|
[{ type: types.TOGGLE_COLLAPSIBLE_LINE, payload: { isClosed: true } }],
|
|
|
|
[],
|
|
|
|
done,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2018-11-20 20:47:30 +05:30
|
|
|
describe('requestJobsForStage', () => {
|
|
|
|
it('should commit REQUEST_JOBS_FOR_STAGE mutation ', done => {
|
|
|
|
testAction(
|
|
|
|
requestJobsForStage,
|
2018-12-05 23:21:45 +05:30
|
|
|
{ name: 'deploy' },
|
2018-11-20 20:47:30 +05:30
|
|
|
mockedState,
|
2018-12-05 23:21:45 +05:30
|
|
|
[{ type: types.REQUEST_JOBS_FOR_STAGE, payload: { name: 'deploy' } }],
|
2018-11-20 20:47:30 +05:30
|
|
|
[],
|
|
|
|
done,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('fetchJobsForStage', () => {
|
|
|
|
let mock;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
mock = new MockAdapter(axios);
|
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
mock.restore();
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('success', () => {
|
2018-12-05 23:21:45 +05:30
|
|
|
it('dispatches requestJobsForStage and receiveJobsForStageSuccess ', done => {
|
|
|
|
mock
|
|
|
|
.onGet(`${TEST_HOST}/jobs.json`)
|
|
|
|
.replyOnce(200, { latest_statuses: [{ id: 121212, name: 'build' }], retried: [] });
|
2018-11-20 20:47:30 +05:30
|
|
|
|
|
|
|
testAction(
|
|
|
|
fetchJobsForStage,
|
2018-12-05 23:21:45 +05:30
|
|
|
{ dropdown_path: `${TEST_HOST}/jobs.json` },
|
2018-11-20 20:47:30 +05:30
|
|
|
mockedState,
|
|
|
|
[],
|
|
|
|
[
|
|
|
|
{
|
|
|
|
type: 'requestJobsForStage',
|
2018-12-05 23:21:45 +05:30
|
|
|
payload: { dropdown_path: `${TEST_HOST}/jobs.json` },
|
2018-11-20 20:47:30 +05:30
|
|
|
},
|
|
|
|
{
|
|
|
|
payload: [{ id: 121212, name: 'build' }],
|
|
|
|
type: 'receiveJobsForStageSuccess',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
done,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('error', () => {
|
|
|
|
beforeEach(() => {
|
2018-12-05 23:21:45 +05:30
|
|
|
mock.onGet(`${TEST_HOST}/jobs.json`).reply(500);
|
2018-11-20 20:47:30 +05:30
|
|
|
});
|
|
|
|
|
2018-12-05 23:21:45 +05:30
|
|
|
it('dispatches requestJobsForStage and receiveJobsForStageError', done => {
|
2018-11-20 20:47:30 +05:30
|
|
|
testAction(
|
|
|
|
fetchJobsForStage,
|
2018-12-05 23:21:45 +05:30
|
|
|
{ dropdown_path: `${TEST_HOST}/jobs.json` },
|
2018-11-20 20:47:30 +05:30
|
|
|
mockedState,
|
|
|
|
[],
|
|
|
|
[
|
|
|
|
{
|
|
|
|
type: 'requestJobsForStage',
|
2018-12-05 23:21:45 +05:30
|
|
|
payload: { dropdown_path: `${TEST_HOST}/jobs.json` },
|
2018-11-20 20:47:30 +05:30
|
|
|
},
|
|
|
|
{
|
|
|
|
type: 'receiveJobsForStageError',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
done,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('receiveJobsForStageSuccess', () => {
|
|
|
|
it('should commit RECEIVE_JOBS_FOR_STAGE_SUCCESS mutation ', done => {
|
|
|
|
testAction(
|
|
|
|
receiveJobsForStageSuccess,
|
|
|
|
[{ id: 121212, name: 'karma' }],
|
|
|
|
mockedState,
|
|
|
|
[{ type: types.RECEIVE_JOBS_FOR_STAGE_SUCCESS, payload: [{ id: 121212, name: 'karma' }] }],
|
|
|
|
[],
|
|
|
|
done,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('receiveJobsForStageError', () => {
|
|
|
|
it('should commit RECEIVE_JOBS_FOR_STAGE_ERROR mutation ', done => {
|
|
|
|
testAction(
|
|
|
|
receiveJobsForStageError,
|
|
|
|
null,
|
|
|
|
mockedState,
|
|
|
|
[{ type: types.RECEIVE_JOBS_FOR_STAGE_ERROR }],
|
|
|
|
[],
|
|
|
|
done,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|