Skip to content

Commit a8af79d

Browse files
Corey Robertsonelasticmachine
andauthored
[7.5] [Canvas] Fixes bugs with autoplay and refresh (#53149) (#54302)
* [Canvas] Fixes bugs with autoplay and refresh (#53149) * Fixes bugs with autoplay and refresh * Fix typecheck Co-authored-by: Elastic Machine <[email protected]> * Fix incorrect mocking Co-authored-by: Elastic Machine <[email protected]>
1 parent db3e12e commit a8af79d

File tree

5 files changed

+353
-26
lines changed

5 files changed

+353
-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: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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(
8+
'../../../../../../../../src/legacy/ui/public/state_management/state_storage/hashed_item_store.js'
9+
);
10+
jest.mock('ui/new_platform'); // actions/elements has some dependencies on ui/new_platform.
11+
jest.mock('../../../lib/app_state');
12+
13+
import { workpadRefresh } from '../workpad_refresh';
14+
import { inFlightComplete } from '../../actions/resolved_args';
15+
// @ts-ignore untyped local
16+
import { setRefreshInterval } from '../../actions/workpad';
17+
import { setRefreshInterval as setAppStateRefreshInterval } from '../../../lib/app_state';
18+
19+
import { createTimeInterval } from '../../../lib/time_interval';
20+
21+
const next = jest.fn();
22+
const dispatch = jest.fn();
23+
const getState = jest.fn();
24+
25+
const middleware = workpadRefresh({ dispatch, getState })(next);
26+
27+
const refreshState = {
28+
transient: {
29+
refresh: {
30+
interval: 5000,
31+
},
32+
},
33+
};
34+
35+
const noRefreshState = {
36+
transient: {
37+
refresh: {
38+
interval: 0,
39+
},
40+
},
41+
};
42+
43+
const inFlightState = {
44+
transient: {
45+
refresh: {
46+
interval: 5000,
47+
},
48+
inFlight: true,
49+
},
50+
};
51+
52+
describe('workpad refresh middleware', () => {
53+
beforeEach(() => {
54+
jest.resetAllMocks();
55+
});
56+
57+
describe('onInflightComplete', () => {
58+
it('refreshes if interval gt 0', () => {
59+
jest.useFakeTimers();
60+
getState.mockReturnValue(refreshState);
61+
62+
middleware(inFlightComplete());
63+
64+
jest.runAllTimers();
65+
66+
expect(dispatch).toHaveBeenCalled();
67+
});
68+
69+
it('does not reset interval if another action occurs', () => {
70+
jest.useFakeTimers();
71+
getState.mockReturnValue(refreshState);
72+
73+
middleware(inFlightComplete());
74+
75+
jest.advanceTimersByTime(refreshState.transient.refresh.interval - 1);
76+
77+
expect(dispatch).not.toHaveBeenCalled();
78+
middleware(inFlightComplete());
79+
80+
jest.advanceTimersByTime(1);
81+
82+
expect(dispatch).toHaveBeenCalled();
83+
});
84+
85+
it('does not refresh if interval is 0', () => {
86+
jest.useFakeTimers();
87+
getState.mockReturnValue(noRefreshState);
88+
89+
middleware(inFlightComplete());
90+
91+
jest.runAllTimers();
92+
expect(dispatch).not.toHaveBeenCalled();
93+
});
94+
});
95+
96+
describe('setRefreshInterval', () => {
97+
it('does nothing if refresh interval is unchanged', () => {
98+
getState.mockReturnValue(refreshState);
99+
100+
jest.useFakeTimers();
101+
const interval = 1;
102+
middleware(setRefreshInterval(interval));
103+
jest.runAllTimers();
104+
105+
expect(setAppStateRefreshInterval).not.toBeCalled();
106+
});
107+
108+
it('sets the app refresh interval', () => {
109+
getState.mockReturnValue(noRefreshState);
110+
next.mockImplementation(() => {
111+
getState.mockReturnValue(refreshState);
112+
});
113+
114+
jest.useFakeTimers();
115+
const interval = 1;
116+
middleware(setRefreshInterval(interval));
117+
118+
expect(setAppStateRefreshInterval).toBeCalledWith(createTimeInterval(interval));
119+
jest.runAllTimers();
120+
});
121+
122+
it('starts a refresh for the new interval', () => {
123+
getState.mockReturnValue(refreshState);
124+
jest.useFakeTimers();
125+
126+
const interval = 1000;
127+
128+
middleware(inFlightComplete());
129+
130+
jest.runTimersToTime(refreshState.transient.refresh.interval - 1);
131+
expect(dispatch).not.toBeCalled();
132+
133+
getState.mockReturnValue(noRefreshState);
134+
next.mockImplementation(() => {
135+
getState.mockReturnValue(refreshState);
136+
});
137+
middleware(setRefreshInterval(interval));
138+
jest.runTimersToTime(1);
139+
140+
expect(dispatch).not.toBeCalled();
141+
142+
jest.runTimersToTime(interval);
143+
expect(dispatch).toBeCalled();
144+
});
145+
});
146+
147+
describe('inFlight in progress', () => {
148+
it('requeues the refresh when inflight is active', () => {
149+
jest.useFakeTimers();
150+
getState.mockReturnValue(inFlightState);
151+
152+
middleware(inFlightComplete());
153+
jest.runTimersToTime(refreshState.transient.refresh.interval);
154+
155+
expect(dispatch).not.toBeCalled();
156+
157+
getState.mockReturnValue(refreshState);
158+
jest.runAllTimers();
159+
160+
expect(dispatch).toBeCalled();
161+
});
162+
});
163+
});

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)