Skip to content

Commit d69f78d

Browse files
Corey Robertsonelasticmachine
andcommitted
[Canvas] Fixes bugs with autoplay and refresh (#53149)
* Fixes bugs with autoplay and refresh * Fix typecheck Co-authored-by: Elastic Machine <[email protected]>
1 parent 107ddbb commit d69f78d

File tree

5 files changed

+351
-26
lines changed

5 files changed

+351
-26
lines changed

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: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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+
// @ts-ignore Untyped local
14+
import { routerProvider } from '../../../lib/router_provider';
15+
16+
const next = jest.fn();
17+
const dispatch = jest.fn();
18+
const getState = jest.fn();
19+
const routerMock = { navigateTo: jest.fn() };
20+
routerProvider.mockReturnValue(routerMock);
21+
22+
const middleware = workpadAutoplay({ dispatch, getState })(next);
23+
24+
const workpadState = {
25+
persistent: {
26+
workpad: {
27+
id: 'workpad-id',
28+
pages: ['page1', 'page2', 'page3'],
29+
page: 0,
30+
},
31+
},
32+
};
33+
34+
const autoplayState = {
35+
...workpadState,
36+
transient: {
37+
autoplay: {
38+
inFlight: false,
39+
enabled: true,
40+
interval: 5000,
41+
},
42+
fullscreen: true,
43+
},
44+
};
45+
46+
const autoplayDisabledState = {
47+
...workpadState,
48+
transient: {
49+
autoplay: {
50+
inFlight: false,
51+
enabled: false,
52+
interval: 5000,
53+
},
54+
},
55+
};
56+
57+
const action = {};
58+
59+
describe('workpad autoplay middleware', () => {
60+
beforeEach(() => {
61+
dispatch.mockClear();
62+
jest.resetAllMocks();
63+
});
64+
65+
describe('app state', () => {
66+
it('sets the app state to the interval from state when enabled', () => {
67+
getState.mockReturnValue(autoplayState);
68+
middleware(action);
69+
70+
expect(setAutoplayInterval).toBeCalledWith(
71+
createTimeInterval(autoplayState.transient.autoplay.interval)
72+
);
73+
});
74+
75+
it('sets the app state to null when not enabled', () => {
76+
getState.mockReturnValue(autoplayDisabledState);
77+
middleware(action);
78+
79+
expect(setAutoplayInterval).toBeCalledWith(null);
80+
});
81+
});
82+
83+
describe('autoplay navigation', () => {
84+
it('navigates forward after interval', () => {
85+
jest.useFakeTimers();
86+
getState.mockReturnValue(autoplayState);
87+
middleware(action);
88+
89+
jest.advanceTimersByTime(autoplayState.transient.autoplay.interval + 1);
90+
91+
expect(routerMock.navigateTo).toBeCalledWith('loadWorkpad', {
92+
id: workpadState.persistent.workpad.id,
93+
page: workpadState.persistent.workpad.page + 2, // (index + 1) + 1 more for 1 indexed page number
94+
});
95+
96+
jest.useRealTimers();
97+
});
98+
99+
it('navigates from last page back to front', () => {
100+
jest.useFakeTimers();
101+
const onLastPageState = { ...autoplayState };
102+
onLastPageState.persistent.workpad.page = onLastPageState.persistent.workpad.pages.length - 1;
103+
104+
getState.mockReturnValue(autoplayState);
105+
middleware(action);
106+
107+
jest.advanceTimersByTime(autoplayState.transient.autoplay.interval + 1);
108+
109+
expect(routerMock.navigateTo).toBeCalledWith('loadWorkpad', {
110+
id: workpadState.persistent.workpad.id,
111+
page: 1,
112+
});
113+
114+
jest.useRealTimers();
115+
});
116+
117+
it('continues autoplaying', () => {
118+
jest.useFakeTimers();
119+
getState.mockReturnValue(autoplayState);
120+
middleware(action);
121+
122+
jest.advanceTimersByTime(autoplayState.transient.autoplay.interval * 2 + 1);
123+
expect(routerMock.navigateTo).toBeCalledTimes(2);
124+
jest.useRealTimers();
125+
});
126+
127+
it('does not reset timer between middleware calls', () => {
128+
jest.useFakeTimers();
129+
130+
getState.mockReturnValue(autoplayState);
131+
middleware(action);
132+
133+
// Advance until right before timeout
134+
jest.advanceTimersByTime(autoplayState.transient.autoplay.interval - 1);
135+
136+
// Run middleware again
137+
middleware(action);
138+
139+
// Advance timer
140+
jest.advanceTimersByTime(1);
141+
142+
expect(routerMock.navigateTo).toBeCalled();
143+
});
144+
});
145+
});
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)