From e7b5f3d84401ca0861b8d18588277e1d11e188ba Mon Sep 17 00:00:00 2001 From: "Eyo O. Eyo" <7893459+eokoneyo@users.noreply.github.com> Date: Mon, 20 Jan 2025 15:20:14 +0100 Subject: [PATCH] fix tty and related tests (#206919) ## Summary Culled from https://github.com/elastic/kibana/pull/206411 This PR is informed from the work that's being done for the migration to React 18, whilst trying out kibana with react 18 we had couple of test fail relating to this particular component, details here [[job]](https://buildkite.com/elastic/kibana-pull-request/builds/266993#0194655f-1466-4ee3-80ed-54e398b09492) [[logs]](https://buildkite.com/organizations/elastic/pipelines/kibana-pull-request/builds/266993/jobs/0194655f-1466-4ee3-80ed-54e398b09492/artifacts/01946583-34ed-444a-bc55-10e684c325ef), it's worth mentioning the way the component was written causes unnecessary re-renders that doesn't actually make the interval for the tty playspeed exactly constant. The approach taken here is such that there's no need to depend on state change to cause the next line to be written, now we setup a timer interval just once for the entire duration that the tty is playing, and said timer only ever gets cleaned up on pause. P.S. This fix does not utilize any APIs from react 18 so it's backward compatible with our current version of react. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios Co-authored-by: Elastic Machine --- .../components/tty_player/hooks.test.tsx | 58 ++++++++++--------- .../public/components/tty_player/hooks.ts | 27 +++++---- 2 files changed, 47 insertions(+), 38 deletions(-) diff --git a/x-pack/solutions/security/plugins/session_view/public/components/tty_player/hooks.test.tsx b/x-pack/solutions/security/plugins/session_view/public/components/tty_player/hooks.test.tsx index 5531210e10a3c..802b833c3e83f 100644 --- a/x-pack/solutions/security/plugins/session_view/public/components/tty_player/hooks.test.tsx +++ b/x-pack/solutions/security/plugins/session_view/public/components/tty_player/hooks.test.tsx @@ -63,38 +63,38 @@ describe('TTYPlayer/hooks', () => { }); it('mounts and renders the first line of output', async () => { - const { result: xTermResult } = renderHook((props) => useXtermPlayer(props), { + const { result: xTermResult } = renderHook(useXtermPlayer, { initialProps, }); const { terminal, currentLine, seekToLine } = xTermResult.current; // there is a minor delay in updates to xtermjs after writeln is called. - jest.advanceTimersByTime(100); + await act(async () => jest.advanceTimersByTime(100)); // check that first line rendered in xtermjs expect(terminal.buffer.active.getLine(0)?.translateToString(true)).toBe('256'); expect(currentLine).toBe(0); - act(() => { + await act(async () => { seekToLine(VIM_LINE_START); // line where vim output starts }); - jest.advanceTimersByTime(100); + await act(async () => jest.advanceTimersByTime(100)); expect(terminal.buffer.active.getLine(0)?.translateToString(true)).toBe('#!/bin/env bash'); }); it('allows the user to seek to any line of output', async () => { - const { result: xTermResult } = renderHook((props) => useXtermPlayer(props), { + const { result: xTermResult } = renderHook(useXtermPlayer, { initialProps, }); - act(() => { + await act(async () => { xTermResult.current.seekToLine(VIM_LINE_START); // line where vim output starts }); - jest.advanceTimersByTime(100); + await act(async () => jest.advanceTimersByTime(100)); const { terminal, currentLine } = xTermResult.current; @@ -103,64 +103,68 @@ describe('TTYPlayer/hooks', () => { }); it('allows the user to play', async () => { - const { result, rerender } = renderHook((props) => useXtermPlayer(props), { + const { result, rerender } = renderHook(useXtermPlayer, { initialProps, }); rerender({ ...initialProps, isPlaying: true }); - act(() => { - jest.advanceTimersByTime(DEFAULT_TTY_PLAYSPEED_MS * 10); - }); + await act(async () => jest.advanceTimersByTime(DEFAULT_TTY_PLAYSPEED_MS * 10)); expect(result.current.currentLine).toBe(10); }); it('allows the user to stop', async () => { - const { result, rerender } = renderHook((props) => useXtermPlayer(props), { + const { result, rerender } = renderHook(useXtermPlayer, { initialProps, }); rerender({ ...initialProps, isPlaying: true }); - act(() => { - jest.advanceTimersByTime(DEFAULT_TTY_PLAYSPEED_MS * 10); - }); + + await act(async () => jest.advanceTimersByTime(DEFAULT_TTY_PLAYSPEED_MS * 10)); + + expect(result.current.currentLine).toBe(10); + rerender({ ...initialProps, isPlaying: false }); - act(() => { - jest.advanceTimersByTime(DEFAULT_TTY_PLAYSPEED_MS * 10); - }); + + await act(async () => jest.advanceTimersByTime(DEFAULT_TTY_PLAYSPEED_MS * 10)); + expect(result.current.currentLine).toBe(10); // should not have advanced }); it('should stop when it reaches the end of the array of lines', async () => { - const { result, rerender } = renderHook((props) => useXtermPlayer(props), { + const { result, rerender } = renderHook(useXtermPlayer, { initialProps, }); rerender({ ...initialProps, isPlaying: true }); - act(() => { - jest.advanceTimersByTime(DEFAULT_TTY_PLAYSPEED_MS * initialProps.lines.length + 100); - }); + + await act(async () => + jest.advanceTimersByTime(DEFAULT_TTY_PLAYSPEED_MS * initialProps.lines.length + 100) + ); + expect(result.current.currentLine).toBe(initialProps.lines.length - 1); }); it('should not print the first line twice after playback starts', async () => { - const { result, rerender } = renderHook((props) => useXtermPlayer(props), { + const { result, rerender } = renderHook(useXtermPlayer, { initialProps, }); rerender({ ...initialProps, isPlaying: true }); - act(() => { + + await act(async () => { // advance render loop jest.advanceTimersByTime(DEFAULT_TTY_PLAYSPEED_MS); }); + rerender({ ...initialProps, isPlaying: false }); expect(result.current.terminal.buffer.active.getLine(0)?.translateToString(true)).toBe('256'); }); it('ensure the first few render loops have printed the right lines', async () => { - const { result, rerender } = renderHook((props) => useXtermPlayer(props), { + const { result, rerender } = renderHook(useXtermPlayer, { initialProps, }); @@ -168,7 +172,7 @@ describe('TTYPlayer/hooks', () => { rerender({ ...initialProps, isPlaying: true }); - act(() => { + await act(async () => { // advance render loop jest.advanceTimersByTime(DEFAULT_TTY_PLAYSPEED_MS * LOOPS); }); @@ -193,7 +197,7 @@ describe('TTYPlayer/hooks', () => { }); it('will allow a plain text search highlight on the last line printed', async () => { - const { result: xTermResult } = renderHook((props) => useXtermPlayer(props), { + const { result: xTermResult } = renderHook(useXtermPlayer, { initialProps, }); diff --git a/x-pack/solutions/security/plugins/session_view/public/components/tty_player/hooks.ts b/x-pack/solutions/security/plugins/session_view/public/components/tty_player/hooks.ts index a1f039f9caad0..7f67a49ab4659 100644 --- a/x-pack/solutions/security/plugins/session_view/public/components/tty_player/hooks.ts +++ b/x-pack/solutions/security/plugins/session_view/public/components/tty_player/hooks.ts @@ -318,21 +318,26 @@ export const useXtermPlayer = ({ useEffect(() => { if (isPlaying) { - const timer = setTimeout(() => { - if (!hasNextPage && currentLine === lines.length - 1) { - setIsPlaying(false); - } else { - const nextLine = Math.min(lines.length - 1, currentLine + 1); - render(nextLine, false); - setCurrentLine(nextLine); - } - }, playSpeed); + const timer = setInterval( + () => + setCurrentLine((_currentLine) => { + if (!hasNextPage && _currentLine === lines.length - 1) { + setIsPlaying(false); + return _currentLine; + } else { + const nextLine = Math.min(lines.length - 1, _currentLine + 1); + render(nextLine, false); + return nextLine; + } + }), + playSpeed + ); return () => { - clearTimeout(timer); + clearInterval(timer); }; } - }, [lines, currentLine, isPlaying, playSpeed, render, hasNextPage, fetchNextPage, setIsPlaying]); + }, [lines, isPlaying, playSpeed, render, hasNextPage, fetchNextPage, setIsPlaying]); const seekToLine = useCallback( (index: any) => {