diff --git a/awx/ui_next/package-lock.json b/awx/ui_next/package-lock.json
index 7c82f572c3..0e64b85366 100644
--- a/awx/ui_next/package-lock.json
+++ b/awx/ui_next/package-lock.json
@@ -2307,6 +2307,14 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
},
+ "ansi-to-html": {
+ "version": "0.6.11",
+ "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.6.11.tgz",
+ "integrity": "sha512-88XZtrcwrfkyn6fGstHnkaF1kl7hGtNCYh4vSmItgEV+6JnQHryDBf7udF4f2RhTRQmYvJvPcTtqgaqrxzc9oA==",
+ "requires": {
+ "entities": "^1.1.1"
+ }
+ },
"ansi-wrap": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz",
@@ -4242,6 +4250,16 @@
"has-ansi": "^2.0.0",
"strip-ansi": "^3.0.0",
"supports-color": "^2.0.0"
+ },
+ "dependencies": {
+ "has-ansi": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+ "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ }
}
},
"chardet": {
@@ -5680,8 +5698,7 @@
"entities": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
- "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
- "dev": true
+ "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="
},
"enzyme": {
"version": "3.9.0",
@@ -7953,11 +7970,18 @@
}
},
"has-ansi": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
- "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-3.0.0.tgz",
+ "integrity": "sha1-Ngd+8dFfMzSEqn+neihgbxxlWzc=",
"requires": {
- "ansi-regex": "^2.0.0"
+ "ansi-regex": "^3.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
+ }
}
},
"has-flag": {
@@ -8170,8 +8194,7 @@
"html-entities": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz",
- "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=",
- "dev": true
+ "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8="
},
"html-tokenize": {
"version": "2.0.0",
diff --git a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx
index 7850ec98e8..088f64b0ff 100644
--- a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx
+++ b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx
@@ -15,7 +15,7 @@ import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading';
import JobEvent from './JobEvent';
import JobEventSkeleton from './JobEventSkeleton';
-import MenuControls from './shared/MenuControls';
+import MenuControls from './MenuControls';
const OutputHeader = styled.div`
font-weight: var(--pf-global--FontWeight--bold);
@@ -52,7 +52,7 @@ function range(low, high) {
class JobOutput extends Component {
constructor(props) {
super(props);
-
+ this.listRef = React.createRef();
this.state = {
contentError: null,
hasContentLoading: true,
@@ -68,13 +68,14 @@ class JobOutput extends Component {
this.loadJobEvents = this.loadJobEvents.bind(this);
this.rowRenderer = this.rowRenderer.bind(this);
- this.handleScrollTop = this.handleScrollTop.bind(this);
- this.handleScrollBottom = this.handleScrollBottom.bind(this);
+ this.handleScrollFirst = this.handleScrollFirst.bind(this);
+ this.handleScrollLast = this.handleScrollLast.bind(this);
this.handleScrollNext = this.handleScrollNext.bind(this);
this.handleScrollPrevious = this.handleScrollPrevious.bind(this);
this.handleResize = this.handleResize.bind(this);
this.isRowLoaded = this.isRowLoaded.bind(this);
this.loadMoreRows = this.loadMoreRows.bind(this);
+ this.scrollToRow = this.scrollToRow.bind(this);
}
componentDidMount() {
@@ -93,12 +94,12 @@ class JobOutput extends Component {
this.cache.clear(n);
});
if (shouldRecomputeRowHeights) {
- this.listRef.recomputeRowHeights();
+ if (this.listRef.recomputeRowHeights) {
+ this.listRef.recomputeRowHeights();
+ }
}
}
- listRef = React.createRef();
-
async loadJobEvents() {
const { job } = this.props;
@@ -193,25 +194,29 @@ class JobOutput extends Component {
});
}
+ scrollToRow(rowIndex) {
+ this.listRef.scrollToRow(rowIndex);
+ }
+
handleScrollPrevious() {
const startIndex = this.listRef.Grid._renderedRowStartIndex;
const stopIndex = this.listRef.Grid._renderedRowStopIndex;
const scrollRange = stopIndex - startIndex + 1;
- this.listRef.scrollToRow(Math.max(0, startIndex - scrollRange));
+ this.scrollToRow(Math.max(0, startIndex - scrollRange));
}
handleScrollNext() {
const stopIndex = this.listRef.Grid._renderedRowStopIndex;
- this.listRef.scrollToRow(stopIndex - 1);
+ this.scrollToRow(stopIndex - 1);
}
- handleScrollTop() {
- this.listRef.scrollToRow(0);
+ handleScrollFirst() {
+ this.scrollToRow(0);
}
- handleScrollBottom() {
+ handleScrollLast() {
const { remoteRowCount } = this.state;
- this.listRef.scrollToRow(remoteRowCount - 1);
+ this.scrollToRow(remoteRowCount - 1);
}
handleResize({ width }) {
@@ -239,8 +244,8 @@ class JobOutput extends Component {
{job.name}
@@ -261,13 +266,13 @@ class JobOutput extends Component {
registerChild(ref);
}}
deferredMeasurementCache={this.cache}
- height={height}
+ height={height || 1}
onRowsRendered={onRowsRendered}
rowCount={remoteRowCount}
rowHeight={this.cache.rowHeight}
rowRenderer={this.rowRenderer}
scrollToAlignment="start"
- width={width}
+ width={width || 1}
overscanRowCount={20}
/>
);
diff --git a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.test.jsx b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.test.jsx
index f21745936e..252416d580 100644
--- a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.test.jsx
+++ b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.test.jsx
@@ -1,48 +1,196 @@
import React from 'react';
-
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
-
import JobOutput from './JobOutput';
+import { JobsAPI } from '@api';
+import mockJobData from './data.job.json';
+import mockJobEventsData from './data.job_events.json';
+
+jest.mock('@api');
async function checkOutput(wrapper, expectedLines) {
- await waitForElement(wrapper, 'div[type="job_event"]', (e) => e.length > 1);
+ await waitForElement(wrapper, 'div[type="job_event"]', e => e.length > 1);
const jobEventLines = wrapper.find('div[type="job_event_line_text"]');
const actualLines = [];
jobEventLines.forEach(line => {
actualLines.push(line.text());
});
+ expect(actualLines.length).toEqual(expectedLines.length);
expectedLines.forEach((line, index) => {
expect(actualLines[index]).toEqual(line);
});
}
-describe('', () => {
- const mockDetails = {
- name: 'Foo',
+async function findScrollButtons(wrapper) {
+ const menuControls = await waitForElement(wrapper, 'MenuControls');
+ const scrollFirstButton = menuControls.find(
+ 'button[aria-label="scroll first"]'
+ );
+ const scrollLastButton = menuControls.find(
+ 'button[aria-label="scroll last"]'
+ );
+ const scrollPreviousButton = menuControls.find(
+ 'button[aria-label="scroll previous"]'
+ );
+ return {
+ scrollFirstButton,
+ scrollLastButton,
+ scrollPreviousButton,
};
+}
+
+describe('', () => {
+ let wrapper;
+ const mockJob = mockJobData;
+ const mockJobEvents = mockJobEventsData;
+ const scrollMock = jest.fn();
+
+ beforeEach(() => {
+ JobsAPI.readEvents.mockResolvedValue({
+ data: {
+ count: 100,
+ next: null,
+ previous: null,
+ results: mockJobEvents.results,
+ },
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ wrapper.unmount();
+ });
test('initially renders succesfully', async done => {
- const wrapper = mountWithContexts();
- // wait until not loading
- await waitForElement(wrapper, 'EmptyStateBody', (e) => e.length === 0);
+ wrapper = mountWithContexts();
+ await waitForElement(wrapper, 'JobEvent', el => el.length > 0);
+ await checkOutput(wrapper, [
+ '',
+ 'PLAY [all] *********************************************************************11:37:25',
+ '',
+ 'TASK [debug] *******************************************************************11:37:25',
+ 'ok: [localhost] => (item=1) => {',
+ ' "msg": "This is a debug message: 1"',
+ '}',
+ 'ok: [localhost] => (item=2) => {',
+ ' "msg": "This is a debug message: 2"',
+ '}',
+ 'ok: [localhost] => (item=3) => {',
+ ' "msg": "This is a debug message: 3"',
+ '}',
+ 'ok: [localhost] => (item=4) => {',
+ ' "msg": "This is a debug message: 4"',
+ '}',
+ 'ok: [localhost] => (item=5) => {',
+ ' "msg": "This is a debug message: 5"',
+ '}',
+ 'ok: [localhost] => (item=6) => {',
+ ' "msg": "This is a debug message: 6"',
+ '}',
+ 'ok: [localhost] => (item=7) => {',
+ ' "msg": "This is a debug message: 7"',
+ '}',
+ 'ok: [localhost] => (item=8) => {',
+ ' "msg": "This is a debug message: 8"',
+ '}',
+ 'ok: [localhost] => (item=9) => {',
+ ' "msg": "This is a debug message: 9"',
+ '}',
+ 'ok: [localhost] => (item=10) => {',
+ ' "msg": "This is a debug message: 10"',
+ '}',
+ 'ok: [localhost] => (item=11) => {',
+ ' "msg": "This is a debug message: 11"',
+ '}',
+ 'ok: [localhost] => (item=12) => {',
+ ' "msg": "This is a debug message: 12"',
+ '}',
+ 'ok: [localhost] => (item=13) => {',
+ ' "msg": "This is a debug message: 13"',
+ '}',
+ 'ok: [localhost] => (item=14) => {',
+ ' "msg": "This is a debug message: 14"',
+ '}',
+ 'ok: [localhost] => (item=15) => {',
+ ' "msg": "This is a debug message: 15"',
+ '}',
+ 'ok: [localhost] => (item=16) => {',
+ ' "msg": "This is a debug message: 16"',
+ '}',
+ ]);
- // await checkOutput(wrapper, [
- // '',
- // 'PLAY [localhost] ***************************************************************08:00:52',
- // '',
- // 'TASK [Gathering Facts] *********************************************************08:00:52',
- // 'ok: [localhost]',
- // '',
- // 'TASK [Check Slack accounts against ldap] ***************************************08:00:53',
- // 'changed: [localhost]',
- // '',
- // 'TASK [E-mail output] ***********************************************************08:00:58',
- // 'skipping: [localhost]',
- // '',
- // 'PLAY RECAP *********************************************************************08:00:58',
- // 'localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0 ',
- // '',
- // ]);
+ expect(wrapper.find('JobOutput').length).toBe(1);
+ done();
+ });
+
+ test('should call scrollToRow with expected index when scroll "previous" button is clicked', async done => {
+ const handleScrollPrevious = jest.spyOn(
+ JobOutput.prototype,
+ 'handleScrollPrevious'
+ );
+ wrapper = mountWithContexts();
+ await waitForElement(wrapper, 'JobEvent', el => el.length > 0);
+ const { scrollLastButton, scrollPreviousButton } = await findScrollButtons(
+ wrapper
+ );
+ wrapper.find('JobOutput').instance().scrollToRow = scrollMock;
+
+ scrollLastButton.simulate('click');
+ scrollPreviousButton.simulate('click');
+
+ expect(handleScrollPrevious).toHaveBeenCalled();
+ expect(scrollMock).toHaveBeenCalledTimes(2);
+ expect(scrollMock.mock.calls).toEqual([[100], [0]]);
+ done();
+ });
+
+ test('should call scrollToRow with expected indices on when scroll "first" and "last" buttons are clicked', async done => {
+ const handleScrollFirst = jest.spyOn(
+ JobOutput.prototype,
+ 'handleScrollFirst'
+ );
+ wrapper = mountWithContexts();
+ await waitForElement(wrapper, 'JobEvent', el => el.length > 0);
+ const { scrollFirstButton, scrollLastButton } = await findScrollButtons(
+ wrapper
+ );
+ wrapper.find('JobOutput').instance().scrollToRow = scrollMock;
+
+ scrollFirstButton.simulate('click');
+ scrollLastButton.simulate('click');
+ scrollFirstButton.simulate('click');
+
+ expect(handleScrollFirst).toHaveBeenCalled();
+ expect(scrollMock).toHaveBeenCalledTimes(3);
+ expect(scrollMock.mock.calls).toEqual([[0], [100], [0]]);
+ done();
+ });
+
+ test('should call scrollToRow with expected index on when scroll "last" button is clicked', async done => {
+ const handleScrollLast = jest.spyOn(
+ JobOutput.prototype,
+ 'handleScrollLast'
+ );
+ wrapper = mountWithContexts();
+ await waitForElement(wrapper, 'EmptyStateBody', e => e.length === 0);
+ wrapper
+ .find('JobOutput')
+ .instance()
+ .handleResize({ width: 100 });
+ const { scrollLastButton } = await findScrollButtons(wrapper);
+ wrapper.find('JobOutput').instance().scrollToRow = scrollMock;
+
+ scrollLastButton.simulate('click');
+
+ expect(handleScrollLast).toHaveBeenCalled();
+ expect(scrollMock).toHaveBeenCalledTimes(1);
+ expect(scrollMock.mock.calls).toEqual([[100]]);
+ done();
+ });
+
+ test('should throw error', async done => {
+ JobsAPI.readEvents = () => Promise.reject(new Error());
+ wrapper = mountWithContexts();
+ await waitForElement(wrapper, 'ContentError', e => e.length === 1);
done();
});
});
diff --git a/awx/ui_next/src/screens/Job/JobOutput/shared/MenuControls.jsx b/awx/ui_next/src/screens/Job/JobOutput/MenuControls.jsx
similarity index 68%
rename from awx/ui_next/src/screens/Job/JobOutput/shared/MenuControls.jsx
rename to awx/ui_next/src/screens/Job/JobOutput/MenuControls.jsx
index 1e7e3ffdcf..fbfb5def55 100644
--- a/awx/ui_next/src/screens/Job/JobOutput/shared/MenuControls.jsx
+++ b/awx/ui_next/src/screens/Job/JobOutput/MenuControls.jsx
@@ -23,8 +23,8 @@ const Button = styled(PFButton)`
`;
const MenuControls = ({
- onScrollTop,
- onScrollBottom,
+ onScrollFirst,
+ onScrollLast,
onScrollNext,
onScrollPrevious,
}) => (
@@ -32,16 +32,20 @@ const MenuControls = ({
-