Add JobOutput tests

This commit is contained in:
Marliana Lara 2019-08-08 13:36:45 -04:00
parent b2922792bc
commit 475645f604
No known key found for this signature in database
GPG Key ID: 38C73B40DFA809EE
8 changed files with 8892 additions and 58 deletions

View File

@ -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",

View File

@ -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 {
<OutputHeader>{job.name}</OutputHeader>
<OutputToolbar>
<MenuControls
onScrollTop={this.handleScrollTop}
onScrollBottom={this.handleScrollBottom}
onScrollFirst={this.handleScrollFirst}
onScrollLast={this.handleScrollLast}
onScrollNext={this.handleScrollNext}
onScrollPrevious={this.handleScrollPrevious}
/>
@ -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}
/>
);

View File

@ -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('<JobOutput />', () => {
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('<JobOutput />', () => {
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(<JobOutput job={mockDetails} />);
// wait until not loading
await waitForElement(wrapper, 'EmptyStateBody', (e) => e.length === 0);
wrapper = mountWithContexts(<JobOutput job={mockJob} />);
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(<JobOutput job={mockJob} />);
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(<JobOutput job={mockJob} />);
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(<JobOutput job={mockJob} />);
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(<JobOutput job={mockJob} />);
await waitForElement(wrapper, 'ContentError', e => e.length === 1);
done();
});
});

View File

@ -23,8 +23,8 @@ const Button = styled(PFButton)`
`;
const MenuControls = ({
onScrollTop,
onScrollBottom,
onScrollFirst,
onScrollLast,
onScrollNext,
onScrollPrevious,
}) => (
@ -32,16 +32,20 @@ const MenuControls = ({
<Button variant="plain">
<PlusIcon />
</Button>
<Button onClick={onScrollPrevious} variant="plain">
<Button
aria-label="scroll previous"
onClick={onScrollPrevious}
variant="plain"
>
<AngleUpIcon />
</Button>
<Button onClick={onScrollNext} variant="plain">
<Button aria-label="scroll next" onClick={onScrollNext} variant="plain">
<AngleDownIcon />
</Button>
<Button onClick={onScrollTop} variant="plain">
<Button aria-label="scroll first" onClick={onScrollFirst} variant="plain">
<AngleDoubleUpIcon />
</Button>
<Button onClick={onScrollBottom} variant="plain">
<Button aria-label="scroll last" onClick={onScrollLast} variant="plain">
<AngleDoubleDownIcon />
</Button>
</Wrapper>

View File

@ -0,0 +1,194 @@
{
"id": 2,
"type": "job",
"url": "/api/v2/jobs/2/",
"related": {
"created_by": "/api/v2/users/1/",
"labels": "/api/v2/jobs/2/labels/",
"inventory": "/api/v2/inventories/1/",
"project": "/api/v2/projects/6/",
"extra_credentials": "/api/v2/jobs/2/extra_credentials/",
"credentials": "/api/v2/jobs/2/credentials/",
"unified_job_template": "/api/v2/job_templates/7/",
"stdout": "/api/v2/jobs/2/stdout/",
"job_events": "/api/v2/jobs/2/job_events/",
"job_host_summaries": "/api/v2/jobs/2/job_host_summaries/",
"activity_stream": "/api/v2/jobs/2/activity_stream/",
"notifications": "/api/v2/jobs/2/notifications/",
"create_schedule": "/api/v2/jobs/2/create_schedule/",
"job_template": "/api/v2/job_templates/7/",
"cancel": "/api/v2/jobs/2/cancel/",
"project_update": "/api/v2/project_updates/4/",
"relaunch": "/api/v2/jobs/2/relaunch/"
},
"summary_fields": {
"inventory": {
"id": 1,
"name": "Demo Inventory",
"description": "",
"has_active_failures": false,
"total_hosts": 1,
"hosts_with_active_failures": 0,
"total_groups": 0,
"groups_with_active_failures": 0,
"has_inventory_sources": false,
"total_inventory_sources": 0,
"inventory_sources_with_failures": 0,
"organization_id": 1,
"kind": ""
},
"project": {
"id": 6,
"name": "Demo Project",
"description": "",
"status": "successful",
"scm_type": "git"
},
"project_update": {
"id": 4,
"name": "Demo Project",
"description": "",
"status": "successful",
"failed": false
},
"job_template": {
"id": 7,
"name": "Demo Job Template",
"description": ""
},
"unified_job_template": {
"id": 7,
"name": "Demo Job Template",
"description": "",
"unified_job_type": "job"
},
"instance_group": {
"id": 1,
"name": "tower"
},
"created_by": {
"id": 1,
"username": "admin",
"first_name": "",
"last_name": ""
},
"user_capabilities": {
"delete": true,
"start": true
},
"labels": {
"count": 0,
"results": []
},
"extra_credentials": [],
"credentials": [
{
"id": 1,
"name": "Demo Credential",
"description": "",
"kind": "ssh",
"cloud": false
}
]
},
"created": "2019-08-08T19:24:05.344276Z",
"modified": "2019-08-08T19:24:18.162949Z",
"name": "Demo Job Template",
"description": "",
"job_type": "run",
"inventory": 1,
"project": 6,
"playbook": "chatty_tasks.yml",
"forks": 0,
"limit": "",
"verbosity": 0,
"extra_vars": "{\"num_messages\": 94}",
"job_tags": "",
"force_handlers": false,
"skip_tags": "",
"start_at_task": "",
"timeout": 0,
"use_fact_cache": false,
"unified_job_template": 7,
"launch_type": "manual",
"status": "successful",
"failed": false,
"started": "2019-08-08T19:24:18.329589Z",
"finished": "2019-08-08T19:24:50.119995Z",
"elapsed": 31.79,
"job_args": "[\"bwrap\", \"--unshare-pid\", \"--dev-bind\", \"/\", \"/\", \"--proc\", \"/proc\", \"--bind\", \"/tmp/ansible_runner_pi_pzufy15c/ansible_runner_pi_r_aeukpy/tmpvsg8ly2y\", \"/etc/ssh\", \"--bind\", \"/tmp/ansible_runner_pi_pzufy15c/ansible_runner_pi_r_aeukpy/tmpq_grmdym\", \"/projects\", \"--bind\", \"/tmp/ansible_runner_pi_pzufy15c/ansible_runner_pi_r_aeukpy/tmpfq8ea2z6\", \"/tmp\", \"--bind\", \"/tmp/ansible_runner_pi_pzufy15c/ansible_runner_pi_r_aeukpy/tmpq6v4y_tt\", \"/var/lib/awx\", \"--bind\", \"/tmp/ansible_runner_pi_pzufy15c/ansible_runner_pi_r_aeukpy/tmpupj_jhhb\", \"/var/log\", \"--ro-bind\", \"/venv/ansible\", \"/venv/ansible\", \"--ro-bind\", \"/venv/awx\", \"/venv/awx\", \"--bind\", \"/projects/_6__demo_project\", \"/projects/_6__demo_project\", \"--bind\", \"/tmp/awx_2_a4b1afiw\", \"/tmp/awx_2_a4b1afiw\", \"--chdir\", \"/projects/_6__demo_project\", \"ansible-playbook\", \"-u\", \"admin\", \"-i\", \"/tmp/awx_2_a4b1afiw/tmppb57i4_e\", \"-e\", \"@/tmp/awx_2_a4b1afiw/env/extravars\", \"chatty_tasks.yml\"]",
"job_cwd": "/projects/_6__demo_project",
"job_env": {
"HOSTNAME": "awx",
"MAKEFLAGS": "w",
"RABBITMQ_USER": "guest",
"OS": "Operating System: Docker for Mac",
"LC_ALL": "en_US.UTF-8",
"RABBITMQ_VHOST": "/",
"SDB_HOST": "0.0.0.0",
"MAKELEVEL": "2",
"VIRTUAL_ENV": "/venv/ansible",
"MFLAGS": "-w",
"PATH": "/venv/ansible/bin:/venv/awx/bin:/venv/awx/bin:/usr/local/n/versions/node/10.15.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"RABBITMQ_PASS": "**********",
"SUPERVISOR_GROUP_NAME": "tower-processes",
"PWD": "/awx_devel",
"LANG": "\"en-us\"",
"PS1": "(awx) ",
"SUPERVISOR_ENABLED": "1",
"SHLVL": "2",
"HOME": "/var/lib/awx",
"LANGUAGE": "en_US:en",
"AWX_GROUP_QUEUES": "tower",
"SUPERVISOR_SERVER_URL": "unix:///tmp/supervisor.sock",
"SUPERVISOR_PROCESS_NAME": "awx-dispatcher",
"RABBITMQ_HOST": "rabbitmq",
"CURRENT_UID": "501",
"_": "/venv/awx/bin/python3",
"DJANGO_SETTINGS_MODULE": "awx.settings.development",
"DJANGO_LIVE_TEST_SERVER_ADDRESS": "localhost:9013-9199",
"SDB_NOTIFY_HOST": "docker.for.mac.host.internal",
"TZ": "UTC",
"ANSIBLE_FORCE_COLOR": "True",
"ANSIBLE_HOST_KEY_CHECKING": "False",
"ANSIBLE_INVENTORY_UNPARSED_FAILED": "True",
"ANSIBLE_PARAMIKO_RECORD_HOST_KEYS": "False",
"ANSIBLE_VENV_PATH": "/venv/ansible",
"PROOT_TMP_DIR": "/tmp",
"AWX_PRIVATE_DATA_DIR": "/tmp/awx_2_a4b1afiw",
"ANSIBLE_COLLECTIONS_PATHS": "/tmp/collections",
"PYTHONPATH": "/venv/ansible/lib/python2.7/site-packages:/awx_devel/awx/lib:",
"JOB_ID": "2",
"INVENTORY_ID": "1",
"PROJECT_REVISION": "23f070aad8e2da131d97ea98b42b553ccf0b0b82",
"ANSIBLE_RETRY_FILES_ENABLED": "False",
"MAX_EVENT_RES": "700000",
"ANSIBLE_CALLBACK_PLUGINS": "/awx_devel/awx/plugins/callback",
"AWX_HOST": "https://towerhost",
"ANSIBLE_SSH_CONTROL_PATH_DIR": "/tmp/awx_2_a4b1afiw/cp",
"ANSIBLE_STDOUT_CALLBACK": "awx_display",
"AWX_ISOLATED_DATA_DIR": "/tmp/awx_2_a4b1afiw/artifacts/2"
},
"job_explanation": "",
"execution_node": "awx",
"controller_node": "",
"result_traceback": "",
"event_processing_finished": true,
"job_template": 7,
"passwords_needed_to_start": [],
"allow_simultaneous": false,
"artifacts": {},
"scm_revision": "23f070aad8e2da131d97ea98b42b553ccf0b0b82",
"instance_group": 1,
"diff_mode": false,
"job_slice_number": 0,
"job_slice_count": 1,
"host_status_counts": {
"ok": 1
},
"playbook_counts": {
"play_count": 1,
"task_count": 1
},
"custom_virtualenv": "/venv/ansible"
}

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
export { default } from './MenuControls';