Skip to content

Commit b209d66

Browse files
author
Corey Robertson
committed
Fixes bugs with autoplay and refresh
1 parent 09faf19 commit b209d66

5 files changed

Lines changed: 350 additions & 26 deletions

File tree

x-pack/legacy/plugins/canvas/public/lib/app_state.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export function setFullscreen(payload: boolean) {
9292
}
9393
}
9494

95-
export function setAutoplayInterval(payload: string) {
95+
export function setAutoplayInterval(payload: string | null) {
9696
const appState = getAppState();
9797
const appValue = appState[AppStateKeys.AUTOPLAY_INTERVAL];
9898

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
jest.mock('../../../lib/app_state');
8+
jest.mock('../../../lib/router_provider');
9+
10+
import { workpadAutoplay } from '../workpad_autoplay';
11+
import { setAutoplayInterval } from '../../../lib/app_state';
12+
import { createTimeInterval } from '../../../lib/time_interval';
13+
import { routerProvider } from '../../../lib/router_provider';
14+
15+
const next = jest.fn();
16+
const dispatch = jest.fn();
17+
const getState = jest.fn();
18+
const routerMock = { navigateTo: jest.fn() };
19+
routerProvider.mockReturnValue(routerMock);
20+
21+
const middleware = workpadAutoplay({ dispatch, getState })(next);
22+
23+
const workpadState = {
24+
persistent: {
25+
workpad: {
26+
id: 'workpad-id',
27+
pages: ['page1', 'page2', 'page3'],
28+
page: 0,
29+
},
30+
},
31+
};
32+
33+
const autoplayState = {
34+
...workpadState,
35+
transient: {
36+
autoplay: {
37+
inFlight: false,
38+
enabled: true,
39+
interval: 5000,
40+
},
41+
fullscreen: true,
42+
},
43+
};
44+
45+
const autoplayDisabledState = {
46+
...workpadState,
47+
transient: {
48+
autoplay: {
49+
inFlight: false,
50+
enabled: false,
51+
interval: 5000,
52+
},
53+
},
54+
};
55+
56+
const action = {};
57+
58+
describe('workpad autoplay middleware', () => {
59+
beforeEach(() => {
60+
dispatch.mockClear();
61+
jest.resetAllMocks();
62+
});
63+
64+
describe('app state', () => {
65+
it('sets the app state to the interval from state when enabled', () => {
66+
getState.mockReturnValue(autoplayState);
67+
middleware(action);
68+
69+
expect(setAutoplayInterval).toBeCalledWith(
70+
createTimeInterval(autoplayState.transient.autoplay.interval)
71+
);
72+
});
73+
74+
it('sets the app state to null when not enabled', () => {
75+
getState.mockReturnValue(autoplayDisabledState);
76+
middleware(action);
77+
78+
expect(setAutoplayInterval).toBeCalledWith(null);
79+
});
80+
});
81+
82+
describe('autoplay navigation', () => {
83+
it('navigates forward after interval', () => {
84+
jest.useFakeTimers();
85+
getState.mockReturnValue(autoplayState);
86+
middleware(action);
87+
88+
jest.advanceTimersByTime(autoplayState.transient.autoplay.interval + 1);
89+
90+
expect(routerMock.navigateTo).toBeCalledWith('loadWorkpad', {
91+
id: workpadState.persistent.workpad.id,
92+
page: workpadState.persistent.workpad.page + 2, // (index + 1) + 1 more for 1 indexed page number
93+
});
94+
95+
jest.useRealTimers();
96+
});
97+
98+
it('navigates from last page back to front', () => {
99+
jest.useFakeTimers();
100+
const onLastPageState = { ...autoplayState };
101+
onLastPageState.persistent.workpad.page = onLastPageState.persistent.workpad.pages.length - 1;
102+
103+
getState.mockReturnValue(autoplayState);
104+
middleware(action);
105+
106+
jest.advanceTimersByTime(autoplayState.transient.autoplay.interval + 1);
107+
108+
expect(routerMock.navigateTo).toBeCalledWith('loadWorkpad', {
109+
id: workpadState.persistent.workpad.id,
110+
page: 1,
111+
});
112+
113+
jest.useRealTimers();
114+
});
115+
116+
it('continues autoplaying', () => {
117+
jest.useFakeTimers();
118+
getState.mockReturnValue(autoplayState);
119+
middleware(action);
120+
121+
jest.advanceTimersByTime(autoplayState.transient.autoplay.interval * 2 + 1);
122+
expect(routerMock.navigateTo).toBeCalledTimes(2);
123+
jest.useRealTimers();
124+
});
125+
126+
it('does not reset timer between middleware calls', () => {
127+
jest.useFakeTimers();
128+
129+
getState.mockReturnValue(autoplayState);
130+
middleware(action);
131+
132+
// Advance until right before timeout
133+
jest.advanceTimersByTime(autoplayState.transient.autoplay.interval - 1);
134+
135+
// Run middleware again
136+
middleware(action);
137+
138+
// Advance timer
139+
jest.advanceTimersByTime(1);
140+
141+
expect(routerMock.navigateTo).toBeCalled();
142+
});
143+
});
144+
});
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
jest.mock('../../../legacy');
8+
jest.mock('ui/new_platform'); // actions/elements has some dependencies on ui/new_platform.
9+
jest.mock('../../../lib/app_state');
10+
11+
import { workpadRefresh } from '../workpad_refresh';
12+
import { inFlightComplete } from '../../actions/resolved_args';
13+
// @ts-ignore untyped local
14+
import { setRefreshInterval } from '../../actions/workpad';
15+
import { setRefreshInterval as setAppStateRefreshInterval } from '../../../lib/app_state';
16+
17+
import { createTimeInterval } from '../../../lib/time_interval';
18+
19+
const next = jest.fn();
20+
const dispatch = jest.fn();
21+
const getState = jest.fn();
22+
23+
const middleware = workpadRefresh({ dispatch, getState })(next);
24+
25+
const refreshState = {
26+
transient: {
27+
refresh: {
28+
interval: 5000,
29+
},
30+
},
31+
};
32+
33+
const noRefreshState = {
34+
transient: {
35+
refresh: {
36+
interval: 0,
37+
},
38+
},
39+
};
40+
41+
const inFlightState = {
42+
transient: {
43+
refresh: {
44+
interval: 5000,
45+
},
46+
inFlight: true,
47+
},
48+
};
49+
50+
describe('workpad refresh middleware', () => {
51+
beforeEach(() => {
52+
jest.resetAllMocks();
53+
});
54+
55+
describe('onInflightComplete', () => {
56+
it('refreshes if interval gt 0', () => {
57+
jest.useFakeTimers();
58+
getState.mockReturnValue(refreshState);
59+
60+
middleware(inFlightComplete());
61+
62+
jest.runAllTimers();
63+
64+
expect(dispatch).toHaveBeenCalled();
65+
});
66+
67+
it('does not reset interval if another action occurs', () => {
68+
jest.useFakeTimers();
69+
getState.mockReturnValue(refreshState);
70+
71+
middleware(inFlightComplete());
72+
73+
jest.advanceTimersByTime(refreshState.transient.refresh.interval - 1);
74+
75+
expect(dispatch).not.toHaveBeenCalled();
76+
middleware(inFlightComplete());
77+
78+
jest.advanceTimersByTime(1);
79+
80+
expect(dispatch).toHaveBeenCalled();
81+
});
82+
83+
it('does not refresh if interval is 0', () => {
84+
jest.useFakeTimers();
85+
getState.mockReturnValue(noRefreshState);
86+
87+
middleware(inFlightComplete());
88+
89+
jest.runAllTimers();
90+
expect(dispatch).not.toHaveBeenCalled();
91+
});
92+
});
93+
94+
describe('setRefreshInterval', () => {
95+
it('does nothing if refresh interval is unchanged', () => {
96+
getState.mockReturnValue(refreshState);
97+
98+
jest.useFakeTimers();
99+
const interval = 1;
100+
middleware(setRefreshInterval(interval));
101+
jest.runAllTimers();
102+
103+
expect(setAppStateRefreshInterval).not.toBeCalled();
104+
});
105+
106+
it('sets the app refresh interval', () => {
107+
getState.mockReturnValue(noRefreshState);
108+
next.mockImplementation(() => {
109+
getState.mockReturnValue(refreshState);
110+
});
111+
112+
jest.useFakeTimers();
113+
const interval = 1;
114+
middleware(setRefreshInterval(interval));
115+
116+
expect(setAppStateRefreshInterval).toBeCalledWith(createTimeInterval(interval));
117+
jest.runAllTimers();
118+
});
119+
120+
it('starts a refresh for the new interval', () => {
121+
getState.mockReturnValue(refreshState);
122+
jest.useFakeTimers();
123+
124+
const interval = 1000;
125+
126+
middleware(inFlightComplete());
127+
128+
jest.runTimersToTime(refreshState.transient.refresh.interval - 1);
129+
expect(dispatch).not.toBeCalled();
130+
131+
getState.mockReturnValue(noRefreshState);
132+
next.mockImplementation(() => {
133+
getState.mockReturnValue(refreshState);
134+
});
135+
middleware(setRefreshInterval(interval));
136+
jest.runTimersToTime(1);
137+
138+
expect(dispatch).not.toBeCalled();
139+
140+
jest.runTimersToTime(interval);
141+
expect(dispatch).toBeCalled();
142+
});
143+
});
144+
145+
describe('inFlight in progress', () => {
146+
it('requeues the refresh when inflight is active', () => {
147+
jest.useFakeTimers();
148+
getState.mockReturnValue(inFlightState);
149+
150+
middleware(inFlightComplete());
151+
jest.runTimersToTime(refreshState.transient.refresh.interval);
152+
153+
expect(dispatch).not.toBeCalled();
154+
155+
getState.mockReturnValue(refreshState);
156+
jest.runAllTimers();
157+
158+
expect(dispatch).toBeCalled();
159+
});
160+
});
161+
});

x-pack/legacy/plugins/canvas/public/state/middleware/workpad_autoplay.js renamed to x-pack/legacy/plugins/canvas/public/state/middleware/workpad_autoplay.ts

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { inFlightComplete } from '../actions/resolved_args';
7+
import { Middleware } from 'redux';
8+
import { State } from '../../../types';
89
import { getFullscreen } from '../selectors/app';
910
import { getInFlight } from '../selectors/resolved_args';
1011
import { getWorkpad, getPages, getSelectedPageIndex, getAutoplay } from '../selectors/workpad';
12+
// @ts-ignore untyped local
1113
import { routerProvider } from '../../lib/router_provider';
1214
import { setAutoplayInterval } from '../../lib/app_state';
1315
import { createTimeInterval } from '../../lib/time_interval';
1416

15-
export const workpadAutoplay = ({ getState }) => next => {
16-
let playTimeout;
17+
export const workpadAutoplay: Middleware<{}, State> = ({ getState }) => next => {
18+
let playTimeout: number | undefined;
1719
let displayInterval = 0;
1820

1921
const router = routerProvider();
@@ -42,18 +44,22 @@ export const workpadAutoplay = ({ getState }) => next => {
4244
}
4345
}
4446

47+
stopAutoUpdate();
4548
startDelayedUpdate();
4649
}
4750

4851
function stopAutoUpdate() {
4952
clearTimeout(playTimeout); // cancel any pending update requests
53+
playTimeout = undefined;
5054
}
5155

5256
function startDelayedUpdate() {
53-
stopAutoUpdate();
54-
playTimeout = setTimeout(() => {
55-
updateWorkpad();
56-
}, displayInterval);
57+
if (!playTimeout) {
58+
stopAutoUpdate();
59+
playTimeout = window.setTimeout(() => {
60+
updateWorkpad();
61+
}, displayInterval);
62+
}
5763
}
5864

5965
return action => {
@@ -68,21 +74,14 @@ export const workpadAutoplay = ({ getState }) => next => {
6874
if (autoplay.enabled) {
6975
setAutoplayInterval(createTimeInterval(autoplay.interval));
7076
} else {
71-
setAutoplayInterval(0);
77+
setAutoplayInterval(null);
7278
}
7379

74-
// when in-flight requests are finished, update the workpad after a given delay
75-
if (action.type === inFlightComplete.toString() && shouldPlay) {
76-
startDelayedUpdate();
77-
} // create new update request
78-
79-
// This middleware creates or destroys an interval that will cause workpad elements to update
80-
// clear any pending timeout
81-
stopAutoUpdate();
82-
8380
// if interval is larger than 0, start the delayed update
8481
if (shouldPlay) {
8582
startDelayedUpdate();
83+
} else {
84+
stopAutoUpdate();
8685
}
8786
};
8887
};

0 commit comments

Comments
 (0)